[Xmas Contest 2024] Finite Field Training 题解

· · 算法·理论

题目链接

前置知识:Nimber。

本题解中,所有的 Nimber 相关都用粗体来表示,而所有普通的整数正常表示。

定义数乘:

a\textbf{\textit{b}}=\sum_{i=1}^a\textbf{\textit{b}}=\begin{cases}\textbf{\textit{b}}&a\equiv1\pmod 2\\\textbf{0}&a\equiv 0\pmod 2\end{cases}

关于题目的小彩蛋:题目名称缩写是 FFT。

题面

题目背景

????「在 2024 年的日本,似乎 Nim 积的实现已经众所周知。即使在题面里没有直接出现,Nim 积也经常作为哈希的解法等出现,但计数问题却大多在 \mathbb F_{998\,244\,353} 上完成。像这段史料一样,选择让组合对象的权值在 \mathbb F_{2^{64}} 上的题非常少见。」

题目描述

对于非负整数 n,m,设 B(n,m)n 个点 m 条边的二分图个数。给定非负整数 N\textbf{\textit{A}}\in \mathbb F_{2^{64}},对于每个 n=0,1,\cdots,N,试求 \sum\limits_{m}B(n,m)\textbf{\textit{A}}^m

## 题解 ### 思路 先考虑在模 $998\,244\,353$ 意义下怎么做。这是经典的,设 $F(z)$ 为答案的 EGF,而 $G(z)$ 是每个二分图染色的权值之和对应的 EGF(即,每个二分图会被算 $2$ 的连通块个数次方次),则显然有 $F(z)^2=G(z)$。(可以构造双射) 但是这个做法不能照搬到 $\mathbb F_{2^{64}}$ 上。一个浅显的原因是**除以** $2$ 做不了所以没有 EGF。更深刻的原因是即使你定义两个**序列**的 EGF 卷积为 $$ \textbf{\textit{c}}_k=\sum_{i+j=k}\dbinom{k}{i}\textbf{\textit{a}}_i\textbf{\textit{b}}_j $$ 也会发现任何**序列** $\{\textbf{\textit{a}}_n\}$ 与自身的卷积为 $\textbf{1},\textbf{0},\textbf{0},\cdots$,或者说单位元。因此你无法开根号。 那怎么办?肯定还是要用染色来考虑,因为二分图只有这一个容易考虑的了。但是二分图的染色方案数一定是偶数啊?考虑钦定 $1$ 号点的颜色为黑色,这时**贡献**非 $\textbf{0}$ 的二分图一定是连通二分图。换言之,我们可以容易求出连通二分图对应的**答案**: $$ \textbf{\textit{f}}_n=\sum_{i=1}^n\dbinom{n-1}{i-1}\textbf{(\textit{A}+1)}^{i(n-i)} $$ 其中有经典的 $\textbf{\textit{x}}^{i(n-i)}=\dfrac{\textbf{\textit{x}}^{n(n-1)/2}}{\textbf{\textit{x}}^{i(i-1)/2}\textbf{\textit{x}}^{(n-i)(n-i-1)/2}}$(特判 $\textbf{\textit{A}}=\textbf{1}$),故 $\{\textbf{\textit{f}}_n\}$ 为 **EGF 卷积**形式,做一次**卷积**即可。 然后做一次 **exp** 即可。时间复杂度 $O(n\log ^2 np(\mathbb F_{V}))$,其中 $p(V)$ 表示在 $V$ 内做 **Nim 积**的复杂度。 ### Nimber EGF 怎么做? 先考虑卷积。 $$ \textbf{\textit{c}}_k=\sum_{i+j=k}\dbinom{k}{i}\textbf{\textit{a}}_i\textbf{\textit{b}}_j $$ 根据 Lucas 定理,你发现这个实际上就是要求了 $i$ 和 $j$ 的按位与为 $0$,求卷积。这就是子集卷积,可以在 $O(n\log ^2n)$ 的复杂度内完成。 至于 exp,直接套用牛顿迭代的公式即可。注意我们上面说过一个 **Nimber 序列** 的 **EGF 卷积逆**是它本身。EGF 的意义下,一个**多项式**求导就是**序列的值**平移。 ### 实现细节 你发现你需要很快地求 Nim 积……吗? 一个很常见的 Nim 积求法是选择 $\mathbb F_{2^{64}}$ 的一个**本原元** $\textbf{\textit{g}}$。(或者说,其在 $\mathbb F_2$ 上的**最小多项式** $\textbf{\textit{p}}(\textbf{\textit{g}})=\textbf{0}$ 的次数为 $64$)则把 Nimber 同构到 $\mathbb F_2[X]/p(X)$ 上去,算出来乘法再同构回来。 这个求法是针对每次给定两个数求的。本题中,其实可以一开始就打到这个同构,最后输出的时候再同构回来! 那么怎么在 $\mathbb F_2[X]/p(X)$ 中快速运算呢?需要支持: - 求两个 $<2^{64}$ 的数的不进位乘法。 - 求一个 $<2^{127}$ 的数对某个 $p$ 的不进位取模。 对于前者,可以发现指令集里有 `_mm_clmulepi64_si128` 就是用来算这个的!而后者,考虑取一个长得好一点的 $p(X)$(注意它需要在 $\mathbb F_2$ 上不可约),这里取了 $\textbf{\textit{p}}(\textbf{\textit{x}})=\textbf{\textit{x}}^{64}+\textbf{\textit{x}}^4+\textbf{\textit{x}}^3+\textbf{\textit{x}}+\textbf{1}$,这样取模只需要几次位运算就解决了。 具体的代码实现: ```cpp inline u64 mult_gf264(u64 x,u64 y) { __m128i a=_mm_set_epi64x(0,x),b=_mm_set_epi64x(0,y); __m128i c=_mm_clmulepi64_si128(a,b,0x00); x=c[0];y=c[1]; u64 q=y^(y<<1),t=q>>61;q^=t^(t<<1); return x^q^(q<<3); } ``` 等等我们怎么找这个同构?换言之我们要找到 $p$ 的根。求根的算法在论文里也讲了——首先 $\textbf{\textit{P}}(\textbf{\textit{x}})=(\textbf{\textit{x}}^{2^{64}}-\textbf{\textit{x}})$ 恰为所有 $\textbf{\textit{x}}-\textbf{\textit{a}},\textbf{\textit{a}}\in \mathbb F_{2^{64}}$ 的积——这是 Fermat 小定理。注意到取 $\textbf{\textit{Q}}(\textbf{\textit{x}})=\sum\limits_{i=0}^{63}\textbf{\textit{x}}^{2^i}$,则有 $\textbf{\textit{Q}}(\textbf{\textit{x}})(\textbf{\textit{Q}}(\textbf{\textit{x}})+\textbf{1})=\textbf{\textit{P}}(\textbf{\textit{x}})$。根据原论文的结论,随机取 $l(x)$ 求 $\gcd(\textbf{\textit{Q}}(\textbf{\textit{l}}(\textbf{\textit{x}})),\textbf{\textit{p}}(\textbf{\textit{x}}))$ 有不少于 $\dfrac12$ 的概率求出一个非平凡因式?我不知道为什么,但是它确实表现很好。 详解:[EI blog](https://www.cnblogs.com/Elegia/p/polynomial-factorization-over-finite-field.html) 贴一下解方程工具的代码: ```cpp #include <bits/stdc++.h> using namespace std; using u64 = uint64_t; using u128 = unsigned __int128; const int B = 65536; u64 g_pow[B * 2], g_lg[B]; u64 mult_q_init(u64 x, int lvl = 64) { if (lvl == 1) return x; int v = lvl >> 1; if (x < (1ull << v)) return mult_q_init(x, v) << v; u64 a = x >> v, b = x & ((1ull << v) - 1); return (mult_q_init(a ^ b, v) << v) | mult_q_init(mult_q_init(a, v), v); } u64 mult_q(u64 x, int lvl = 64) { if (x == 0) return 0; if (lvl == 16) return g_pow[g_lg[x] + 40029]; int v = lvl >> 1; if (x < (1ull << v)) return mult_q(x, v) << v; u64 a = x >> v, b = x & ((1ull << v) - 1); return (mult_q(a ^ b, v) << v) | mult_q(mult_q(a, v), v); } u64 mult_blk_init(u64 x, u64 y, int u = 16) { if (x == 0 || y == 0) return 0; if (x == 1 || y == 1) return x ^ y ^ 1; int v = u / 2; u64 a = x >> v, b = x & ((1ull << v) - 1), c = y >> v, d = y & ((1ull << v) - 1); if (a == 0) return mult_blk_init(b, c, v) << v | mult_blk_init(b, d, v); if (c == 0) return mult_blk_init(a, d, v) << v | mult_blk_init(b, d, v); u64 ac = mult_blk_init(a, c, v), bd = mult_blk_init(b, d , v), apb_cpd = mult_blk_init(a ^ b, c ^ d, v); u64 hi = apb_cpd ^ bd, lo = mult_q_init(ac, v) ^ bd; return (hi << v) | lo; } u64 mult_blk(u64 x, u64 y, int u = 64) { if (x == 0 || y == 0) return 0; if (x < B && y < B) return g_pow[g_lg[x] + g_lg[y]]; if (x == 1 || y == 1) return x ^ y ^ 1; int v = u / 2; u64 a = x >> v, b = x & ((1ull << v) - 1), c = y >> v, d = y & ((1ull << v) - 1); if (a == 0) return mult_blk(b, c, v) << v | mult_blk(b, d, v); if (c == 0) return mult_blk(a, d, v) << v | mult_blk(b, d, v); u64 ac = mult_blk(a, c, v), bd = mult_blk(b, d , v), apb_cpd = mult_blk(a ^ b, c ^ d, v); u64 hi = apb_cpd ^ bd, lo = mult_q(ac, v) ^ bd; return (hi << v) | lo; } void init(u64 g) { g_pow[0] = 1; for (int i = 1; i < B * 2; i++) g_pow[i] = mult_blk_init(g_pow[i - 1], g); for (int i = 0; i < B - 1; i++) if (!g_lg[g_pow[i]]) g_lg[g_pow[i]] = i; else { cout << g << " " << i << endl; assert(0); } } u64 qkpw(u64 a,u64 b) { u64 r=1; while(b) { if(b&1)r=mult_blk(r,a); a=mult_blk(a,a); b>>=1; } return r; } u64 inv(u64 a) { return qkpw(a,u64(-2)); } void unify(int N,u64 *g) { if(g[N]==1) return ; u64 x=inv(g[N]); for(int i=0;i<=N;++i) g[i]=mult_blk(g[i],x); } void mod(int M,u64 *g,int N,u64 *h) { unify(N,h); for(int i=M;i>=N;--i) { if(g[i]) { u64 cur=g[i]; for(int j=0;j<=N;++j) g[j+i-N]^=mult_blk(cur,h[j]); } } } int find_degree(int N,u64 *g) { int cur=N;while(cur>=0&&g[cur]==0)--cur; return g[cur]?cur:(-1); } void mult(int M,u64 *g,int N,u64 *h,u64 *ans) { for(int i=0;i<=M+N;++i) ans[i]=0; for(int i=0;i<=M;++i) { for(int j=0;j<=N;++j) { ans[i+j]^=mult_blk(g[i],h[j]); } } } const int Bq=5; mt19937_64 myrand(chrono::steady_clock::now().time_since_epoch().count()); u64 rand64() { return myrand(); } int GCD(int N,u64 *g,int M,u64 *h) { unify(N,g);unify(M,h); while(1) { if(N>=0&&M>=0) { if(N>=M) { mod(N,g,M,h); N=find_degree(M-1,g); unify(N,g); } else { mod(M,h,N,g); M=find_degree(N-1,h); unify(M,h); } } else { if(N==-1) { N=M;for(int i=0;i<=M;++i) g[i]=h[i]; } else { M=N;for(int i=0;i<=N;++i) h[i]=g[i]; } return M; } } } void solve(int N,u64 *f) { if(N==0) return ; unify(N,f); if(N==1) { cout<<f[0]<<'\n'; return ; } u64 l[191],ret[191],qaq[191]; int d; int cnt=0; while(1) { for(int i=0;i<=Bq;++i) l[i]=rand64(); if(l[Bq]==0) l[Bq]=1; for(int i=0;i<N;++i) ret[i]=0; for(int __=0;__<64;++__) { for(int i=0;i<N;++i) ret[i]^=l[i]; mult(N-1,l,N-1,l,qaq); mod(2*N-2,qaq,N,f); for(int i=0;i<N;++i) l[i]=qaq[i]; } for(int i=0;i<=N;++i) qaq[i]=f[i]; d=GCD(find_degree(N-1,ret),ret,N,qaq); if(d!=0&&d!=N) break; if((++cnt)>100) return ; } u64 a[191],b[191]; for(int i=0;i<=d;++i) a[i]=qaq[i]; for(int i=N;i>=d;--i) { b[i-d]=f[i]; if(f[i]) { u64 cur=f[i]; for(int j=0;j<=d;++j) f[j+i-d]^=mult_blk(cur,a[j]); } } solve(d,a);solve(N-d,b); } int n; u64 f[1145]; int main() { init(2025); cin>>n; for(int i=0;i<=n;++i) cin>>f[i]; solve(n,f); return 0; } ``` 求出来可以取的一个根是 $\textbf{4\,928\,496\,685\,556\,603\,065}$。 还有一些卡常细节,比如为了避免 cache miss 数组要开大一点,具体看代码吧。 ### 代码 ```cpp #include <bits/stdc++.h> #include <immintrin.h> #include <emmintrin.h> using namespace std; #define popc(i) popcount(unsigned(i)) using u64 = uint64_t; using u128 = unsigned __int128; const int B = 65536; u64 g_pow[B * 2], g_lg[B]; u64 mult_q_init(u64 x, int lvl = 64) { if (lvl == 1) return x; int v = lvl >> 1; if (x < (1ull << v)) return mult_q_init(x, v) << v; u64 a = x >> v, b = x & ((1ull << v) - 1); return (mult_q_init(a ^ b, v) << v) | mult_q_init(mult_q_init(a, v), v); } u64 mult_q(u64 x, int lvl = 64) { if (x == 0) return 0; if (lvl == 16) return g_pow[g_lg[x] + 40029]; int v = lvl >> 1; if (x < (1ull << v)) return mult_q(x, v) << v; u64 a = x >> v, b = x & ((1ull << v) - 1); return (mult_q(a ^ b, v) << v) | mult_q(mult_q(a, v), v); } u64 mult_blk_init(u64 x, u64 y, int u = 16) { if (x == 0 || y == 0) return 0; if (x == 1 || y == 1) return x ^ y ^ 1; int v = u / 2; u64 a = x >> v, b = x & ((1ull << v) - 1), c = y >> v, d = y & ((1ull << v) - 1); if (a == 0) return mult_blk_init(b, c, v) << v | mult_blk_init(b, d, v); if (c == 0) return mult_blk_init(a, d, v) << v | mult_blk_init(b, d, v); u64 ac = mult_blk_init(a, c, v), bd = mult_blk_init(b, d , v), apb_cpd = mult_blk_init(a ^ b, c ^ d, v); u64 hi = apb_cpd ^ bd, lo = mult_q_init(ac, v) ^ bd; return (hi << v) | lo; } u64 mult_blk(u64 x, u64 y, int u = 64) { if (x == 0 || y == 0) return 0; if (x < B && y < B) return g_pow[g_lg[x] + g_lg[y]]; if (x == 1 || y == 1) return x ^ y ^ 1; int v = u / 2; u64 a = x >> v, b = x & ((1ull << v) - 1), c = y >> v, d = y & ((1ull << v) - 1); if (a == 0) return mult_blk(b, c, v) << v | mult_blk(b, d, v); if (c == 0) return mult_blk(a, d, v) << v | mult_blk(b, d, v); u64 ac = mult_blk(a, c, v), bd = mult_blk(b, d , v), apb_cpd = mult_blk(a ^ b, c ^ d, v); u64 hi = apb_cpd ^ bd, lo = mult_q(ac, v) ^ bd; return (hi << v) | lo; } void init(u64 g) { g_pow[0] = 1; for (int i = 1; i < B * 2; i++) g_pow[i] = mult_blk_init(g_pow[i - 1], g); for (int i = 0; i < B - 1; i++) if (!g_lg[g_pow[i]]) g_lg[g_pow[i]] = i; else { cout << g << " " << i << endl; assert(0); } } u64 qkpw(u64 a,u64 b) { u64 r=1; while(b) { if(b&1)r=mult_blk(r,a); a=mult_blk(a,a); b>>=1; } return r; } u64 inv(u64 a) { return qkpw(a,u64(-2)); } u64 pw[1145],bas[1145],val[1145]; void init2(u64 g) { pw[0]=1;for(int i=1;i<128;++i)pw[i]=mult_blk(pw[i-1],g); for(int i=0;i<64;++i) { u64 cur=pw[i],vl=1ull<<i; for(int j=63;j>=0;--j) { if((cur>>j)&1) { if(bas[j]) { cur^=bas[j];vl^=val[j]; } else { bas[j]=cur;val[j]=vl; break; } } } } for(int j=0;j<=63;++j) { for(int k=0;k<j;++k) if((bas[j]>>k)&1) { bas[j]^=bas[k]; val[j]^=val[k]; } } } u64 nimber_to_gf264(u64 x) { u64 ret=0; for(int i=0;i<64;++i) if((x>>i)&1) ret^=val[i]; return ret; } inline u64 mult_gf264(u64 x,u64 y) { __m128i a=_mm_set_epi64x(0,x),b=_mm_set_epi64x(0,y); __m128i c=_mm_clmulepi64_si128(a,b,0x00); x=c[0];y=c[1]; u64 q=y^(y<<1),t=q>>61;q^=t^(t<<1); return x^q^(q<<3); } u64 gf264_to_nimber(u64 x) { u64 ret=0; for(int i=0;i<64;++i) if((x>>i)&1) ret^=pw[i]; return ret; } const int Bt=1048576; u64 p[21][Bt+256],q[21][Bt+256],r[21][Bt+256]; void conv(int k,u64 *a,u64 *b,u64 *ans) { int n=1<<k; for(int i=0;i<=k;++i) { memset(p[i],0,sizeof(u64)<<k); memset(q[i],0,sizeof(u64)<<k); memset(r[i],0,sizeof(u64)<<k); } for(int i=0;i<n;++i) { p[popc(i)][i]=a[i]; q[popc(i)][i]=b[i]; } for(int l=0;l<k;++l) for(int j=0;j<n;++j) if((j>>l)&1) { for(int i=0;i<k;++i) { p[i][j]^=p[i][j^(1<<l)]; q[i][j]^=q[i][j^(1<<l)]; } } for(int i=0;i<=k;++i) { for(int j=0,l=i;l>=0;++j,--l) { for(int id=0;id<n;++id) r[i][id]^=mult_gf264(p[j][id],q[l][id]); } } for(int l=0;l<k;++l) for(int j=0;j<n;++j) if((j>>l)&1) { for(int i=0;i<=k;++i) { r[i][j]^=r[i][j^(1<<l)]; } } for(int i=0;i<n;++i) ans[i]=r[popc(i)][i]; } void conv_small_large(int k,u64 *b,u64 *ans) { int n=1<<k; for(int i=0;i<=k;++i) { memset(q[i],0,sizeof(u64)<<k); memset(r[i],0,sizeof(u64)<<k); } for(int i=0;i<n;++i) { q[popc(i)][i]=b[i]; } for(int l=0;l<k;++l) for(int j=0;j<n;++j) if((j>>l)&1) { for(int i=0;i<k;++i) { q[i][j]^=q[i][j^(1<<l)]; } } for(int i=0;i<=k;++i) { for(int j=0,l=i;l>=0;++j,--l) { for(int id=0;id<n/2;++id) r[i][id]^=mult_gf264(p[j][id],q[l][id]); for(int id=n/2;id<n;++id) r[i][id]^=mult_gf264(p[j][id-n/2],q[l][id]); } } for(int l=0;l<k;++l) for(int j=0;j<n;++j) if((j>>l)&1) { for(int i=0;i<=k;++i) { r[i][j]^=r[i][j^(1<<l)]; } } for(int i=0;i<n;++i) ans[i]=r[popc(i)][i]; } u64 qaq[Bt+256]; void exp(int k,u64 *f,u64 *ans) { ans[0]=1; for(int i=1;i<=k;++i) { ans[1<<(i-1)]=0; conv(i-1,ans,ans+1,qaq+1); qaq[0]=1; for(int j=0;j<(1<<i);++j) qaq[j]^=f[j]; conv_small_large(i,qaq,ans); } } int paranoia,k,n; u64 a,iva; u64 pia[Bt+256],piab[Bt+256]; u64 x[Bt+256],y[Bt+256],z[Bt+256]; u64 f[Bt+256]; u64 ans[Bt+256]; u64 pwinva(u64 t) { return mult_gf264(pia[t&1048575],piab[t>>20]); } int main() { init(2025); init2(4928496685556603065); #ifndef DEBUG ios_base::sync_with_stdio(false);cin.tie(0); #endif cin>>paranoia>>a; a^=1;iva=inv(a); k=1; while((1<<k)<=paranoia) ++k; n=1<<k; a=nimber_to_gf264(a);iva=nimber_to_gf264(iva); if(a==0) { for(int i=1;i<n;++i) f[i]=1; } else { pia[0]=1;for(int i=1;i<=Bt;++i) pia[i]=mult_gf264(pia[i-1],iva); piab[0]=1;for(int i=1;i<=Bt;++i) piab[i]=mult_gf264(piab[i-1],pia[Bt]); for(int i=0;i<n;++i) { x[i]=pwinva(i*(i+1ull)/2); y[i]=pwinva(i*(i-1ull)/2); } conv(k,x,y,z); u64 mult=1,qaaq=1; for(int i=1;i<n;++i) { f[i]=mult_gf264(z[i-1],qaaq); mult=mult_gf264(mult,a); qaaq=mult_gf264(qaaq,mult); } } exp(k,f,ans); for(int i=0;i<=paranoia;++i) { cout<<gf264_to_nimber(ans[i])<<" \n"[i==paranoia]; } return 0; } ``` ## 致谢 感谢 @[zhuzhu2891](https://www.luogu.com.cn/user/515385) 和 @[Galois_Field_1048576](https://www.luogu.com.cn/user/360265) 对我解决本题提供的帮助。 感谢 UOJ 群群友告诉我 AtCoder 的 exit code 9 不是 RE 是 TLE。 参考文献: 《浅谈 Nimber 和多项式算法》宁波市镇海中学 罗煜翔