CF1423J

本来是想刷水题过日子来着,就随便开了个 2400,结果发现是个神仙题/kk


Portal

首先注意到,当 (2^{0sim i}) 的系数都填上 (7) 时,这时候的结果等于 (7sumlimits_{j=0}^i2^j=7left(2^{i+1}-1 ight)),最小的严格覆盖它的 (2) 的整次幂是 (2^{i+4}),也就是最多会超出 (2^i) 三位。于是我们可以从低位往高位 DP,任意时刻需要满足前 (i) 位都符合了,因为以后再也不可能影响到它们了,然后需要记录后面三位是多少。转移的时候就直接枚举状态,减一下,看当前系数是否在 ([0,8)) 内,然后加上去。于是我们得到了一个 (mathrm O!left(Tcdot 60 imes 8^2 ight)) 的算法,算一下是 1e9,但是只有加法,写个快模常数会非常小,于是考虑莽一波。

for(int i=1;i<=60;i++)for(int j=0;j<8;j++){
	int shd=(j<<1)+((x>>i-1)&1);
	dp[i][j]=0;
	for(int k=0;k<8;k++)
		0<=shd-k&&shd-k<8&&add(dp[i][j],dp[i-1][k]);
}

显然会 T,(n=10^5) 的时候已经 (889mathrm{ms}) 了。接下来考虑施展卡常大法/cy

首先手动 O3,fread / fwrite 快读快写走起。然后发现 #define int long long 会严重降低效率,于是只把读入设成 ll 其他都用 int。然后将 for(k) 这层循环展开,注意到 dp[i][j] 多次使用,于是取个引用避免多次计算地址。

for(int i=1;i<=60;i++)for(int j=0;j<8;j++){
	const int shd=(j<<1)+((x>>i-1)&1);int &d=dp[i][j]=0;
	0<=shd&&shd<8&&(d+=dp[i-1][0])>=mod&&(d-=mod),
	1<=shd&&shd<9&&(d+=dp[i-1][1])>=mod&&(d-=mod),
	2<=shd&&shd<10&&(d+=dp[i-1][2])>=mod&&(d-=mod),
	3<=shd&&shd<11&&(d+=dp[i-1][3])>=mod&&(d-=mod),
	4<=shd&&shd<12&&(d+=dp[i-1][4])>=mod&&(d-=mod),
	5<=shd&&shd<13&&(d+=dp[i-1][5])>=mod&&(d-=mod),
	6<=shd&&shd<14&&(d+=dp[i-1][6])>=mod&&(d-=mod),
	7<=shd&&shd<15&&(d+=dp[i-1][7])>=mod&&(d-=mod);
}

目前 (n=10^5) 已经降低到 (404mathrm{ms}),继续卡。我们考虑将 for(j) 这层也展开。这下好了,发现新大陆了。发现对于每次转移,能够转移的条件是关于 (j=0) 时的 (shd) 的一个不等式,而它是个 (0/1),不像 (j>0) 的时候还要加上 j<<1。然后还有,全部展开的话,会发现对于某些 (j,k) 根本无论如何也不可能转移,这样可以直接手动删除。于是这样展开后,转移分为三类:仅 (shd=0) 时,仅 (shd=1) 时,所有时。总共的转移量只有 (36),比原来的 (8^2=64) 要少了一半。顺便再把 (dp_{i,0sim 7}) 一起取个引用,发挥通常情况下循环展开的作用。

for(int i=1;i<=60;i++){
	const bool shd=(x>>i-1)&1;
	int &d0=dp[i][0],&d1=dp[i][1],&d2=dp[i][2],&d3=dp[i][3],&d4=dp[i][4],&d5=dp[i][5],&d6=dp[i][6],&d7=dp[i][7];
	#define add(x,y) (d##x+=dp[i-1][y])>=mod&&(d##x-=mod)
	shd||(add(4,1),add(5,3),add(6,5),add(7,7)),
	add(3,0),add(4,2),add(5,4),add(6,6),
	add(3,1),add(4,3),add(5,5),add(6,7),
	add(2,0),add(3,2),add(4,4),add(5,6),
	add(2,1),add(3,3),add(4,5),add(5,7),
	add(1,0),add(2,2),add(3,4),add(4,6),
	add(1,1),add(2,3),add(3,5),add(4,7),
	add(0,0),add(1,2),add(2,4),add(3,6),
	shd&&(add(0,1),add(1,3),add(2,5),add(3,7));
}

此时 (n=10^5) 需要跑 (280mathrm{ms}),算一下 (n=5 imes 10^5) 的时候是 (1.4mathrm s),快成功了。进一步,根据一堆寻址,如果按照地址连续寻址会更快这个理论,我们把中间一堆所有时的转移按照第一维排个序,快了大概 (0.1mathrm s)。然后注意到 (dp_{i,0sim 7}) 多次使用,虽然已经取引用了,但还有更快的访问方法:存到寄存器里(只有 (8) 个,寄存器装得下)。注意 register int &d0=dp[i][0]; 是卵用没有的,因为它到底还是取了引用,那么就不可能把这个变量从原来处于的地方给重新装到寄存器里。于是必须重新定义变量,在一开始定义的时候就装到寄存器里,算完之后再赋到 dp 数组里。

for(int i=1;i<=60;i++){
	bool shd=(x>>i-1)&1;
	register int d0=0,d1=0,d2=0,d3=0,d4=0,d5=0,d6=0,d7=0;
	#define add(x,y) (d##x+=dp[i-1][y])>=mod&&(d##x-=mod)
	add(0,0),add(1,0),add(1,1),add(1,2),add(2,0),add(2,1),add(2,2),add(2,3),add(2,4),add(3,0),add(3,1),add(3,2),add(3,3),add(3,4),add(3,5),add(3,6),add(4,2),add(4,3),add(4,4),add(4,5),add(4,6),add(4,7),add(5,4),add(5,5),add(5,6),add(5,7),add(6,6),add(6,7),
	shd||(add(4,1),add(5,3),add(6,5),add(7,7)),shd&&(add(0,1),add(1,3),add(2,5),add(3,7));
	dp[i][0]=d0,dp[i][1]=d1,dp[i][2]=d2,dp[i][3]=d3,dp[i][4]=d4,dp[i][5]=d5,dp[i][6]=d6,dp[i][7]=d7;
}

这个寄存器的效果是真的惊人,直接给我最大点跑了 (608mathrm{ms})。至此这道题被我野蛮的 AC 了。所以这道题是一个练习卡常的好题(×


显然上面那个不是正解,而是我自己瞎 yy 的,所以接下来讲正解。

考虑将一组系数 (x_{0sim n}) 满足条件的式子写出来:

[sumlimits_{i=0}^n2^ix_i=m ]

注意到 (x_iin[0,8)),想到八进制,考虑将这么多项三个分一组:

[sumlimits_{i=0}^{frac n3}8^i(x_{3i}+2x_{3i+1}+4x_{3i+2})=m\sumlimits_{i=0}^{frac n3}8^ix_{3i}+2sum_{i=0}^{frac n3}8^ix_{3i+1}+4sum_{i=0}^{frac n3}8^ix_{3i+2}=m ]

这时候注意到,这三项等价于八进制数,也就是值与 (x) 一一对应,于是我们只要计算关于正整数 (a,b,c) 的方程 (a+2b+4c=m) 的解的组数即可。这个就非常简单了:

[egin{aligned}ans&=sum_{i=0}^{leftlfloorfrac m4 ight floor}sum_{j=0}^{leftlfloorfrac{m-4i}2 ight floor}1\&=sum_{i=0}^{leftlfloorfrac m4 ight floor}left(leftlfloordfrac{m-4i}2 ight floor+1 ight)\&=sum_{i=0}^{leftlfloorfrac m4 ight floor}left(leftlfloordfrac m2 ight floor+1-2i ight)\&=left(leftlfloordfrac m4 ight floor+1 ight)left(leftlfloordfrac m2 ight floor+1 ight)-leftlfloordfrac m4 ight floorleft(leftlfloordfrac m4 ight floor+1 ight)end{aligned} ]

珍爱生命,远离抄袭!
原文地址:https://www.cnblogs.com/ycx-akioi/p/solution-cf1423j.html