CF1423J - Bubble Cup hypothesis 题解
本来是想刷水题过日子来着,就随便开了个 2400,结果发现是个神仙题/kk
首先注意到,当 \(2^{0\sim i}\) 的系数都填上 \(7\) 时,这时候的结果等于 \(7\sum\limits_{j=0}^i2^j=7\left(2^{i+1}-1\right)\),最小的严格覆盖它的 \(2\) 的整次幂是 \(2^{i+4}\),也就是最多会超出 \(2^i\) 三位。于是我们可以从低位往高位 DP,任意时刻需要满足前 \(i\) 位都符合了,因为以后再也不可能影响到它们了,然后需要记录后面三位是多少。转移的时候就直接枚举状态,减一下,看当前系数是否在 \([0,8)\) 内,然后加上去。于是我们得到了一个 \(\mathrm O\!\left(T\cdot 60\times 8^2\right)\) 的算法,算一下是 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\) 的时候已经 \(889\mathrm{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\) 已经降低到 \(404\mathrm{ms}\),继续卡。我们考虑将 for(j)
这层也展开。这下好了,发现新大陆了。发现对于每次转移,能够转移的条件是关于 \(j=0\) 时的 \(shd\) 的一个不等式,而它是个 \(0/1\),不像 \(j>0\) 的时候还要加上 j<<1
。然后还有,全部展开的话,会发现对于某些 \(j,k\) 根本无论如何也不可能转移,这样可以直接手动删除。于是这样展开后,转移分为三类:仅 \(shd=0\) 时,仅 \(shd=1\) 时,所有时。总共的转移量只有 \(36\),比原来的 \(8^2=64\) 要少了一半。顺便再把 \(dp_{i,0\sim 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\) 需要跑 \(280\mathrm{ms}\),算一下 \(n=5\times 10^5\) 的时候是 \(1.4\mathrm s\),快成功了。进一步,根据一堆寻址,如果按照地址连续寻址会更快这个理论,我们把中间一堆所有时的转移按照第一维排个序,快了大概 \(0.1\mathrm s\)。然后注意到 \(dp_{i,0\sim 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;
}
这个寄存器的效果是真的惊人,直接给我最大点跑了 \(608\mathrm{ms}\)。至此这道题被我野蛮的 AC 了。所以这道题是一个练习卡常的好题(×
显然上面那个不是正解,而是我自己瞎 yy 的,所以接下来讲正解。
考虑将一组系数 \(x_{0\sim n}\) 满足条件的式子写出来:
注意到 \(x_i\in[0,8)\),想到八进制,考虑将这么多项三个分一组:
这时候注意到,这三项等价于八进制数,也就是值与 \(x\) 一一对应,于是我们只要计算关于正整数 \(a,b,c\) 的方程 \(a+2b+4c=m\) 的解的组数即可。这个就非常简单了: