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);
	}
}

posted @ 2021-03-13 16:24  ssdfzhyf  阅读(94)  评论(0编辑  收藏  举报