P6097 题解

· · 题解

前置文章。其包含了这里的一些概念。

原始算法

在这题里我们希望:

c^0=W(a^0,b^0),c^1=W(a^0,b^1)+W(a^1,b^0)

好消息是总项数只有 3,直接做也是 O(3^n) 的,坏消息是难以用前置文章中的通用思路构造出小于 3 的项数。

这个形式形如一个真正的卷积,我们搓出一个形式元 x,有:

W(a^0+a^1x,b^0+b^1x)=W(a^0,b^0)+(W(a^0,b^1)+W(a^1,b^0))x+W(a^1,b^1)x^2

也就是 c^0+c^1x+W(a^1,b^1)x^2

那就好办了。 $$d^0=W(a^0,a^1),d^1=W(a^0+a^1x,b^0+b^1x)$$ $$c^0=d^0,c'^1=d^1-d^0$$ 这里的 $c'^1$ 是多项式,真正答案在于最低有效位,可以推出 $i$ 的最低有效位的指数是 $\operatorname{popcount}(i)$。 分析复杂度,在 “FMT” 和 “IFMT” 过程之中,所有操作是加法和乘 $x$,均为线性;其余操作是执行 $2^n$ 次的卷积。总复杂度 $O(2^nn^2)$。 (PS:如果你让 $x=\epsilon$,足够小的话,最后的结果形如 $Ax^k+\sum_{i\ge k+1}Bx^{i}$,剩余部分完全能够被区分出来。只不过这要求 $x=O(nV)$,也就是需要上高精度,导致和一般的子集卷积复杂度相同。) (PS:这部分和一般不使用 $x$ 的算法,在张量分析下可以使用普通张量秩和边界秩分别解释,即 $\sum a_i\circ b_i\circ c_i=X$ 还是 $\sum a_i\circ b_i\circ c_i=Xx^k+Yo(x^k)$(如果不知道这表示什么东西的话可以忽略),其天然对应占位多项式。) 此时的代码: ```cpp #include<bits/stdc++.h> using namespace std; const unsigned P=1e9+9,N=1<<20|3; unsigned a[N][21],b[N][21]; int n; inline void ADD(unsigned&A,unsigned B){A=(A+B>=P?A+B-P:A+B);} void _FMT(unsigned a[][21]) { for(int i=0;i<n;++i) for(int j=0;j<(1<<n);++j)if(!(j>>i&1)) { unsigned *A=a[j|(1<<i)],*B=a[j]; memmove(A+1,A,80);A[0]=B[0]; for(int i=1;i<=n;++i)ADD(A[i],B[i]); } } void _IFMT(unsigned a[][21]) { for(int i=0;i<n;++i) for(int j=0;j<(1<<n);++j)if(!(j>>i&1)) { unsigned *A=a[j|(1<<i)],*B=a[j]; for(int i=0;i<=n;++i)ADD(A[i],P-B[i]); } } unsigned long long tmp[21]; int main() { cin.tie(0)->sync_with_stdio(0); cin>>n; for(int i=0;i<(1<<n);++i)cin>>a[i][0]; for(int i=0;i<(1<<n);++i)cin>>b[i][0]; _FMT(a),_FMT(b); for(int i=0;i<(1<<n);++i) { memset(tmp,0,sizeof tmp); for(int j=0;j<=n;++j) for(int k=0;j+k<=n;++k) (tmp[j+k]+=1ull*a[i][j]*b[i][k]))%=P; for(int j=0;j<=n;++j) a[i][j]=tmp[j]%P; } _IFMT(a); for(int i=0;i<(1<<n);++i)cout<<a[i][__builtin_popcount(i)]<<" ";cout<<endl; } ``` ### 改进算法:位置对齐 如果初始输入直接放入 $x^{\operatorname{popcount}(i)}$ 的系数,“FMT”中的 `memmove` 就可以省去。更改部分的代码: ```cpp void _FMT(unsigned a[][21]) { for(int i=0;i<n;++i) for(int j=0;j<(1<<n);++j)if(!(j>>i&1)) { unsigned *A=a[j|(1<<i)],*B=a[j]; for(int i=0;i<=n;++i)ADD(A[i],B[i]); } } //do other thing int main() { cin.tie(0)->sync_with_stdio(0); cin>>n; for(int i=0;i<(1<<n);++i)cin>>a[i][__builtin_popcount(i)]; for(int i=0;i<(1<<n);++i)cin>>b[i][__builtin_popcount(i)]; //do other thing } ``` ### 进一步:线性空间 如果我们强制线性空间,那么多项式 $f(x)$ 是无法存储的。 但是我们可以存储 $f(B)$,其中 $B$ 是一个初始固定的值。接下来可以尝试使用拉格朗日插值求系数。 但是不需要求系数这么麻烦。我们知道对于 $S$,记 $c=\operatorname{popcount}(S)$,则 $f_S(x)=ans_Sx^c+\sum_{j>c}z_{S,j}x^j$。 我们可以对 $g_S(x)=\frac{f_S(x)}{x^c}$ 求 $g_S(0)$,即使 $\frac{f_S(0)}{0^c}$ 看起来似乎有问题,但如果中间看成多项式操作就完全能够解释。(这里是因为足够好的连续函数除法会导致一些点上的间断,但是结果仍然可以通过极限弥补成“连续”的,即 $g_S(0)=\lim_{B\to 0}g_S(B)$) 于是我们可以直接预处理拉格朗日插值的系数 $c_j$,然后对于所有 $S$ 有 $g_S(0)=\sum_j c_jg_S(j)$,选取 $j\ge O(\log n)$,这样 $j$ 超过了多项式最大度数(似乎是 $\log n+O(1)$?),最后枚举 $1\le B\le j$,对于所有 $S$ 求 $g_S(B)$ 并做出对应贡献。 时间复杂度不变,空间从 $O(2^nn)$ 转到 $O(2^n)$。 ```cpp void _FMT(unsigned long long a[]) { for(int i=0;i<n;++i) for(int j=0;j<(1<<n);++j)if(!(j>>i&1)) ADD(a[j|(1<<i)],a[j]); } void _IFMT(unsigned long long a[]) { for(int i=0;i<n;++i) for(int j=0;j<(1<<n);++j)if(!(j>>i&1)) ADD(a[j|(1<<i)],P-a[j]); } const int M=50; long long K[M+3]; long long pw[M+3],ipw[M+3]; long long inv[M+3]; int main() { cin.tie(0)->sync_with_stdio(0); inv[1]=1; for(int i=2;i<=M;++i)inv[i]=(P-1ll*P/i)*inv[P%i]%P; cin>>n; for(int i=0;i<(1<<n);++i)cin>>a[i]; for(int i=0;i<(1<<n);++i)cin>>b[i]; for(int i=1;i<=M;++i) { long long A=1,B=1; for(int j=1;j<=M;++j)if(i!=j) A=A*(P-j)%P; for(int j=1;j<=M;++j)if(i!=j) if(i<j) B=B*(P-inv[j-i])%P; else B=B*(inv[i-j])%P; K[i]=A*B%P; } for(int _=1;_<=M;++_) { pw[0]=ipw[0]=1; for(int j=1;j<=M;++j)pw[j]=pw[j-1]*_%P,ipw[j]=ipw[j-1]*inv[_]%P; for(int i=0;i<(1<<n);++i) A[i]=a[i]*pw[__builtin_popcount(i)]%P, B[i]=b[i]*pw[__builtin_popcount(i)]%P; _FMT(A),_FMT(B); for(int i=0;i<(1<<n);++i)A[i]=1ll*A[i]*B[i]%P; _IFMT(A); for(int i=0;i<(1<<n);++i)(ans[i]+=K[_]*(A[i]*ipw[__builtin_popcount(i)]%P))%=P; } for(int i=0;i<(1<<n);++i)cout<<ans[i]<<" ";cout<<endl; } ```