ABC384 F - Double Sum 2

· · 题解

前言

题目已经够简洁了,不给题目大意啦。

作者是蒟蒻,原本不会做此题,看题解,结果一篇都看不懂!实在有点儿抽象了,咋用那么多位运算呢,对我这种菜鸡有点儿不有好了。

因此经过自己一整天的琢磨,终于想透了做法,于是准备梳理一遍,顺便给各位想做此题的蒟蒻们一个友好的帮助。

思路开端

此题,如果正着做,为什么觉得时间复杂度永远都只能停留在 O(N^2) 了呢?当然,别的题解有正着做的,可以学一学,但是菜鸡作者不会啊。

俗话说:“正难则反”。好,正难则反!那我们反过来吧,用一个容斥的思想。具体怎么做,就跟着我一步步来吧。

正难则反

回到“正难则反”这个原则,到底要怎么“反”呢?

来,各位,排除掉自己结合自己的话,算个数吧。想一想,下面这个东西好不好算呢?

\sum _ {i = 1} ^ N\sum _ {j = i+1} ^ N ( a_i + a_j )

是不是很好算?对,其实上就是:

\sum _ {i = 1} ^ N (a_i \times (N-1))

这个值,计算起来是 O(n) 的,可以在输入的时候就算出来。

至于我们上面没有考虑到的,也就是自己和自己结合的那种情况,其实也可以直接算啦,就是算 f(a_i \times 2),其实上就是 f(a_i) 啦,因为按照 f 函数的计算原则,那个 \times 2 会被消掉呀。把这些也加进目前的 ans 里,就能生成出最开始的 ans 啦。

接下来只需要减去 ans 里的多余部分就可以了。

先来考虑超时做法

来,我们先来考虑一下那个会超时的 O(N^2) 算法吧。

直接讲不好讲啊,我来举个例子吧。

这里,n = 4a=\{1,4,3,5\}

首先我们可以 O(N^2) 生成出一个 s 数组,存储一下这个 a 数组中两两之间的和。

那么对于这个例子,s=\{5,4,6,7,9,8\}

接下来我们来想一想,一个数值,比方说 24,当然这是随便举的例子啦,它是怎么变成 f(24),即 3 的呢?减少了 21 对不对?

其实上,它拥有一个 2 的整次幂因子,是谁?对,是 2^3,也就是 8

那我们可以用 8 去消掉它多余的部分呀,对不对?是的,其实上就是让 24 减少了 \frac{8-1}{8},是吧?算一算,诶,好像还真是!

但是这个什么 \frac{8-1}{8},感觉怪讨厌的,要是能拆成一些几分之一加起来就好了。这不好办吗?有点儿数学基础的就知道,其实上 \frac{8-1}{8}=\frac{1}{8}+\frac{1}{4}+\frac{1}{2}

那就好处理了,而且刚才所说的 24 其实上不只是 8 这一个 2 的整次幂的倍数嘛,也是 4 以及 2 的倍数呀!分数里边,作为分母的也有 42!这就好办啦,懂了没,各位?

推广一下,也就是说,s 数组里的每个数,都要找到它对应的一些 2^k,当然要保证 k 为正整数,然后根据上面的公式计算需要减少多少,就好啦!

我们可以枚举每个 k,再去 s 数组里一个一个数字比对,就行咯!

但如何不超时呢

可是,上面讨论的都是 O(N^2) 的呀,不能接受!怎么办?

对,现在要讲的就是,如何使我们上面所讨论的那种算法,不超时。

这好办,只需要请出一位嘉宾:余数!

什么,余数?对呀,想起来没有,两个数的和是不是另一个数的倍数,不是可以通过这两个数对这另一个数的余数来判断吗?哇!反应过来没有?

好好好,先别激动,让我阐述一下到底该怎么做。

还是拿上面的那个 a=\{1,4,3,5\} 的例子来说。

我们首先可以让 k=1,那我们要处理的这个数就是 2^12,这里就用 g 代替吧,目前 g2 哟。

当然在此之前要准备好两个数组,一个 sum 数组,一个 cnt 数组,其中 sum_i 存储的是,目前考虑到的所有数中,对 g 取模为 i 的数的总和,而 cnt_i 存储的则是目前考虑到的所有数中,对 g 取模为 i 的数的总个数。一开始要先清空他们两个哦。

接下来开始遍历 a 数组!

首先 i=1,遍历到的 a_i1,这个 12 取模是 1,反过来,跟它拼起来是 2 的倍数的那个数,对 2 取模就是 2-1 咯。
但是由于刚刚才开始遍历,所以什么收获都没有,只是单单把 a_1 的数据记录进两个数组中。

接下来 i=2,遍历到的 a_i4,现在这个 42 取模变成 0 啦,反过来跟它拼起来的(之后称为“反模数”),会是取模为 2-0=2 的吗?不可能,因为对 2 取模,这个数一定会变得比 2 小才对呀,所以还要再对 2 取一次模,变成 0
调查一下,cnt_0 以及 sum_0 里,什么数据都没有存储,没东西哟,那还是只能记录当前情况,不能使 ans 减少。

然后遍历到 i=3 啦,a_i3,它的模数是 1,反模数也是 1 咯。这次的 1 数据库里存了东西吗?存啦,存了之前 i=1 时记录下的数据呀!
那么就可以产生一次贡献了,这个贡献其实上是 (cnt_1 \times a_3 + sum_1) \div 2。可是,为什么要这么干?
因为这一次是特殊情况,cnt_1 里边只有 1,也就是说当前可以和它相加成功的只有一个数,但如果有多个,这个 a_3 就得累加多次了,累加 cnt_1 次嘛。不过这个 sum_1 存的是之前那些数的和,可以直接加进去。
至于那个 \div 2,其实上等同于 \times \frac{1}{2} 呀,不过是更好在代码里体现罢了。如果你不清楚这里为什么要 \times \frac{1}{2},请返回去再看一看哟,前面讲过滴!

最后 i=4 的情况,应该不需要我再说一遍吧,上面讲得已经够清楚啦。如果还是有点儿迷惑,可以自己摇一摇 i=4 的情况来辅助理解。

接下来升级 kg,就直接让 k 变成 k+1g 变成 g \times 2 即可。还可以发现 k 其实是没有用的,所以可以直接用 g 来做循环变量,从 2 开始,依次 \times 2

对了,那这个 g 枚举到什么时候就打断循环呢?如果设 mxa_i 中的最大值,那么这个 g 只要不超过 mx \times 2,就可以继续,超过了就跳出循环。
为什么?因为 mx 是最大的呀,它自己和自己相加一定是和里边最大的了吧。如果 g 都超过 mx 自己和自己相加,也就是 mx \times 2 了,那还能有哪个两个数之和会是 g 的倍数呢?这就没有枚举的必要了呀,所以是这个限制。

说到现在,你一定知道怎么做了,代码编起来很简单,就别看我的了吧!不过,我还是会把代码贴上哟。

代码

注意代码里的 k 其实指的就是思路里所说的 g,其他变量名都统一。随你怎么看吧!

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5 , M = 2e7+5;
long long n,a[N],cnt[M],sum[M],mx,ans;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];mx=max(mx,a[i]);
        ans+=a[i]*(n-1);
        long long tmp=a[i];
        while(tmp%2==0)tmp/=2;
        ans+=tmp;
    }
    for(int k=2;k<=mx*2;k*=2){
        memset(cnt,0,sizeof(cnt));
        memset(sum,0,sizeof(sum));
        for(int i=1;i<=n;i++){
            int zm=a[i]%k,fm=(k-a[i]%k)%k;
            if(cnt[fm])ans-=(sum[fm]+cnt[fm]*a[i])/k;
            cnt[zm]++,sum[zm]+=a[i];
        }
    }
    cout<<ans;
    return 0;
}

如果看懂了就给个赞吧,谢谢!