2020 ICPC Shanghai C - Sum of Log
题目链接:
https://vjudge.net/contest/416227#problem/C
这个题要求\(\sum_{i=0}^{X}\sum_{j=[i==0]}^{Y}[i\&j==0]\lfloor\log_{2}(i+j)+1\rfloor\)
\(0\le X,Y\le1e9\)
然后有\(T\)组数据,\(T\le 1e5\),限时\(1s\)
从对\([i\&j==0]\lfloor\log_{2}(i+j)+1\rfloor\)求和这一要求可以看出,我们只考虑\(i\&j==0\)的情况,它意味着\(i,j\)的二进制位不可同为\(1\),等价于\(i+j\)不会发生任何进位。\(\lfloor\log_{2}(i+j)+1\rfloor\)是\(i+j\)的二进制位数,由于\(i+j\)不会发生任何进位,\(\lfloor\log_{2}(i+j)+1\rfloor\)就是\(i,j\)的位数的最大值。
当\(i\)取\([2^a,2^{a+1}-1]\),\(j\)取\([2^a,2^{a+1}-1]\)时,这个计数问题很容易解决,即为\(2\cdot 3^a(a+1)\)
解释:
第\(a\)位(从小到大数,且个位为第\(0\)位)上,要么\(i\)的第\(a\)位是\(1\),\(j\)的第\(a\)位是\(0\),要么\(i\)的第\(a\)位是\(0\),\(j\)的第\(a\)位是\(1\)。然后第\(0\)到第\(a-1\)位,每一位有\(3\)种可能:
\(i\)对应\(0\),\(j\)对应\(0\)
\(i\)对应\(0\),\(j\)对应\(1\)
\(i\)对应\(1\),\(j\)对应\(0\)
\(i+j\)的位数即\(i,j\)的位数的最大值\(a+1\),故答案即为\(2\cdot 3^a(a+1)\)
在最理想的情况下,求和是很简单的,我们可以稍微把问题变得复杂一些:
当\(i\)取\([N,N+2^{a}-1]\),\(j\)取\([M,M+2^{b}-1]\)
而且\(N\&(2^{a}-1)==0,M\&(2^{b}-1)==0,N\ge 2^a,M\ge 2^b\)
即\(N\)的最小的\(a\)位都是\(0\),\(M\)的最小的\(b\)位都是\(0\),\(N\)的位数大于\(a\),\(M\)的位数大于\(b\)
此时\(i\)的位数等于\(N\)的位数,\(j\)的位数等于\(M\)的位数,那么满足\(i\&j==0\)的\(\lfloor\log_{2}(i+j)+1\rfloor\)就是\(N\)的位数和\(M\)的位数的最大值,记为\(k\),这显然是一个定值
\(\sum_{i=N}^{N+2^a-1}\sum_{j=M}^{M+2^b-1}[i\&j==0]\lfloor\log_{2}(i+j)+1\rfloor=k\sum_{i=N}^{N+2^a-1}\sum_{j=M}^{M+2^b-1}[i\&j==0]\)
由于这个求和是可以交换\(i,j\)的,我们不妨设\(a\ge b\)
这样,在最小的\(b\)位中,\(i,j\)的取值是自由的,每一位对应\(3\)种对求和有意义的取值组合:
\(i\)对应\(0\),\(j\)对应\(0\)
\(i\)对应\(0\),\(j\)对应\(1\)
\(i\)对应\(1\),\(j\)对应\(0\)
有\(3^b\)种可能
假如\(a>b\),那么从第\(b\)到第\(a-1\)位中,\(j\)的取值是固定的,与\(M\)保持一致,而\(i\)的取值是自由的
对于每一位来说,如果\(j\)取\(0\),那么\(i\)取\(0,1\)皆可
如果\(j\)取\(1\),那么\(i\)只能取\(1\)
设\(M\)的第\(b\)到第\(a-1\)位中,有\(sum_0\)个\(0\),那么就有\(2^{sum_0}\)种取值
若\(a=b\),则令\(sum_0=0\)即可
从第\(a\)位到最高位(30位),由于\(i,j\)的取值分别和\(N,M\)保持一致,故只有\(1\)种取值
综上,\(\sum_{i=N}^{N+2^a-1}\sum_{j=M}^{M+2^b-1}[i\&j==0]\lfloor\log_{2}(i+j)+1\rfloor=k3^b2^{sum_0}\)
其中\(N\&(2^{a}-1)==0,M\&(2^{b}-1)==0,N\ge 2^a,M\ge 2^b,a\ge b\)
\(k\)是\(N,M\)位数的最大值,\(sum_0\)是\(M\)的第\(b\)位到第\(a-1\)位中\(0\)的个数
在笔者看来,数位\(DP\)最核心的思想就是分段求和
给定\(X,Y\),我们需要给\(X\),\(Y\)分别分段,然后两两组合计算并求和
比如题目给的\(19\,26\)这一样例
\([0,19]\)即可分成\([0,0],[1,1],[2,3],[4,7],[8,15],[16,19]\)
\([0,26]\)即可分成\([0,0],[1,1],[2,3],[4,7],[8,15],[16,23],[24,25],[26,26]\)
然后根据上面的方法两两组合求和,这里特别注意\([0,0]\)和\([0,0]\)不可以组合,这是题目的规定
这种分法大约需要把区间分为\(60\)段
假设\(X\)有\(cnt\)位,那么可以先分出\([0,0],[1,1],[2,3],...[2^{cnt-2},2^{cnt-1}-1]\)这些区间
这些最多是\(cnt\)份
之后可以继续分出若干区间\([2^{cnt-1},2^{cnt-1}+2^{a_1}-1],[2^{cnt-1}+2^{a_1},2^{cnt-1}+2^{a_1}+2^{a_2}-1]...\)
由于\(cnt-1>a_1>a_2>...\ge 0\),所以最多有\(cnt-1\)份
总共不超过\(2cnt-1\)份,考虑\(X\le 1e9\),故最多分成\(61\)份,每次计算需要不超过\(4000\)次求和
这样总共就需要进行约\(4000*100000=4e8\)次求和,这样就要求每次计算时复杂度是\(O(1)\)的
我们需要预处理出每一个区间\([N,N+2^a-1]\)的\(N\)的哪些位是\(0\),然后用前缀和求出区间中\(0\)的数量,这样可以快速得出\(sum_0\)的大小
此外还得预处理出\(3\)的若干次幂和\(2\)的若干次幂,不可以现算
我的代码用G++11提交T了一次,然后用G++17提交就A了,可见出题人卡得一手常数。当然,我相信在一定的优化之后,用所有的方法编译都是可以通过的。
代码如下
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
//typedef unsigned long long ll;
typedef long long ll;
const ll mod=1000000007;
int posx[40],posy[40];
int bits(int num)//返回有几位
{
int cnt=0;
do
{
cnt++;
num>>=1;
}while(num);
return cnt;
}
ll power[4][35];
struct qujian
{
int st,free,len;//表示区间[st,st+2^free-1],其中st有len位
private:int pre_sum_act[35];
public:
int *pre_sum;//前缀和数组
void mp(int st_,int free_,int len_)
{
st=st_;free=free_;len=len_;
pre_sum=pre_sum_act+1;//防止求前缀和时访问下标-1
}
void pre()//求前缀和
{
pre_sum[-1]=0;
for(int i=0;i<31;i++)
{
pre_sum[i]=pre_sum[i-1]+!(st&(1<<i));
}
}
int num_0(int l,int r)//求[l,r]这个区间中,st中有几个0
{
return pre_sum[r]-pre_sum[l-1];
}
ll operator *(qujian &b)//两个区间中有几对i,j使得i&j==0
{
//注意一定使用指针或者引用访问,免得程序执行时进行拷贝,提高复杂度
ll ans=1;
qujian *x,*y;
x=this;
y=&b;
if(x->free<y->free)swap(x,y);
ans*=power[3][y->free];//题解中所说的3^b
ans*=power[2][y->num_0(y->free,x->free-1)];//题解中所说的2^sum_0
return ans;
}
}qjx[100],qjy[110];
int lim(int num,qujian qj[])
{
int weishu=bits(num);
int cnt=0;
qj[++cnt].mp(0,0,1);//[0,0]
for(int i=0;i<weishu-1;i++)
{
qj[++cnt].mp(1<<i,i,i+1);
}
/*注:这种写法分出来的区间可能会多一点,比如[1000,1111]本来可以看成一个区间,
这么写会拆成[1000,1011],[1100,1101],[1110,1110],[1111,1111]四个区间,总数不会超过62个
好处是比较好写(好写很重要),可以用简单的循环处理
*/
for(int st=1<<(weishu-1),i=weishu-2;i>=0;i--)
{
if((num&(1<<i))==0)continue;
qj[++cnt].mp(st,i,weishu);
st|=(1<<i);
}
if(num)qj[++cnt].mp(num,0,weishu);
return cnt;
}
int main()
{
//预处理幂次
power[2][0]=1;
power[3][0]=1;
for(int i=1;i<32;i++)power[2][i]=power[2][i-1]*2%mod;
for(int i=1;i<32;i++)power[3][i]=power[3][i-1]*3%mod;
int T;
scanf("%d",&T);
while(T--)
{
int X,Y;
scanf("%d%d",&X,&Y);
//区间划分
int qj_num_x=lim(X,qjx);
int qj_num_y=lim(Y,qjy);
for(int i=1;i<=qj_num_x;i++)
{
qjx[i].pre();//前缀和预处理
}
for(int j=1;j<=qj_num_y;j++)
{
qjy[j].pre();
}
ll ans=0;
//枚举不同的区间
for(int i=1;i<=qj_num_x;i++)
{
for(int j=1+(i==1);j<=qj_num_y;j++)
{
if(qjx[i].st&qjy[j].st)continue;
ll k=max(qjx[i].len,qjy[j].len);
ll ans1=qjx[i]*qjy[j];
ans+=k*ans1;
}
}
//最后不要忘了取模
ans=ans%mod;
printf("%lld\n",ans);
}
}