本来是想刷水题过日子来着,就随便开了个 2400,结果发现是个神仙题/kk
首先注意到,当 (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}) 满足条件的式子写出来:
注意到 (x_iin[0,8)),想到八进制,考虑将这么多项三个分一组:
这时候注意到,这三项等价于八进制数,也就是值与 (x) 一一对应,于是我们只要计算关于正整数 (a,b,c) 的方程 (a+2b+4c=m) 的解的组数即可。这个就非常简单了: