快速数论变换 NTT
基础模板
const int N = 4e6;
ll qpow(ll a, ll b, int p)
{
ll res = 1;
while (b){
if (b & 1) res = res * a % p;
b >>= 1;
a = a * a % p;
}
return res;
}
const int p = 998244353;
const int g = 3;
const int gi = qpow(g, p - 2, p); // 原根的倒数
int n, m;
ll A[N], B[N];
void NTT(ll A[], int n, int op){
if(n == 1) return;
ll A1[n>>1], A2[n>>1];
for(int i = 0; i < n/2; i ++){
A1[i] = A[i*2];
A2[i] = A[i*2+1];
}
NTT(A1, n/2, op); NTT(A2, n/2, op);
ll g1 = qpow(op == 1 ? g : gi, (p - 1) / n, p);
ll gk = 1;
for(int i = 0; i < n/2; i ++){
A[i] = (A1[i] + A2[i] * gk % p) % p;
A[i + n/2] = (A1[i] - A2[i] * gk % p + p) % p;
gk = gk * g1 % p;
}
}
void solve()
{
cin >> n >> m;
for(int i = 0; i < n; i ++) cin >> A[i];
for(int i = 0; i < m; i ++) cin >> B[i];
n --; m --; // 最高次项为n-1,m-1
int nn = 1;
while(nn <= n + m) nn <<= 1;
NTT(A, nn, 1); NTT(B, nn, 1);
for(int i = 0; i < nn; i ++){
A[i] = A[i] * B[i] % p;
}
NTT(A, nn, -1);
for(int i = 0; i <= n + m; i ++){
cout << A[i] * qpow(nn, p - 2, p) % p << " ";
}
}
实战模版
const int mod = 998244353;
const int g = 3;
void NTT(vector<int>& a, bool inverse){
int n = a.size();
for(int i = 1, j = 0; i < n; i ++){
int bit = n >> 1;
for(; j & bit; bit >>= 1) j ^= bit;
j ^= bit;
if(i < j) swap(a[i], a[j]);
}
for(int len = 2; len <= n; len <<= 1){
int wlen = qpow(g, (mod - 1) / len, mod);
if(inverse) wlen = qpow(wlen, mod - 2, mod);
for(int i = 0; i < n; i += len){
int w = 1;
for(int j = 0; j < len / 2; j ++){
int u = a[i + j];
int v = (ll)a[i + j + len / 2] * w % mod;
a[i + j] = u + v;
if(a[i + j] >= mod) a[i + j] -= mod;
a[i + j + len / 2] = u - v;
if(a[i + j + len / 2] < 0) a[i + j + len / 2] += mod;
w = (ll)w * wlen % mod;
}
}
}
if(inverse){
int inv_n = qpow(n, mod - 2, mod);
for(int i = 0; i < n; i ++) a[i] = (ll)a[i] * inv_n % mod;
}
}
vector<int> Poly_mul(const vector<int>& a, const vector<int>& b){
if(a.empty()) return b;
if(b.empty()) return a;
if((ll)a.size() * b.size() <= 5000){
vector<int> c((int)a.size() + (int)b.size() - 1, 0);
for(int i = 0; i < (int)a.size(); i ++){
for(int j = 0; j < (int)b.size(); j ++){
c[i + j] = (c[i + j] + (ll)a[i] * b[j]) % mod;
}
}
return c;
}
int need = (int)a.size() + (int)b.size() - 1;
int n = 1;
while(n < need) n <<= 1;
vector<int> x(a.begin(), a.end()), y(b.begin(), b.end());
x.resize(n);
y.resize(n);
NTT(x, 0);
NTT(y, 0);
for(int i = 0; i < n; i ++) x[i] = (ll)x[i] * y[i] % mod;
NTT(x, 1);
x.resize(need);
return x;
}
// 接口:对于两个vector表示的多项式,调用 c = Poly_mul(a, b);
2026牛客寒假训练营6 J
NTT 设计
由于是要统计每个偏移量的贡献,因此我们要让多项式中未知数的幂次等于该偏移量。
对于每一种字符 \(c\),设计两个多项式 \(a,b\)。
- 对于多项式 \(a\),让等于字符 \(c\) 的位置系数为 1,其他为 0;
- 对于多项式 \(b\),让小于字符 \(c\) 的位置系数为 1,其他为 0;
这样,二者相乘时,就可以使得 \(s_{i} > t_{j}\) 的位置对 \((i, j)\) 产生 1 个贡献。
而为了让位置对 \((i, j)\) 的贡献作用在其相应的偏移量上,我们可以考虑将多项式 \(a\) 中每个原来的未知数幂次全部取负,这样两项相乘后得到的未知数幂次可以体现两个位置之间的偏移量。
真正实现时,为了套用模板,不能实际让未知数的幂次取负值。这个处理也很简单:对于负幂次设计一个合适的偏移量,使其变为非负数就行了。
总复杂度 \(O(26 * n\log n)\)(因为还需要枚举字符),具体细节见 code。
2025郑州邀请赛K
和上一题类似,又是凯撒加密。只不过本题不是关于位置偏移,而是模 \(m\) 意义下的值域偏移。
考虑计算偏移量为 \(k\) 时的总贡献:
发现是卷积形式,考虑 NTT。
本题用多项式求解的核心在于:将同一偏移量的所有分贡献累计到某个固定 \(x^{i}\) 的系数中。为了达到这种效果,我们仍需沿用上一道题的思路,将两个因子多项式的其中一个的所有系数反转(这里反转 \(freq\))。此时,偏移量为 \(0\) 的所有分贡献均累计到了 \(x^{m-1}\) 的系数中。
进一步考虑如何表示偏移量为 \(1,2,...,m-1\) 的贡献。这里有一个小 trick:将 \(value\) 数组倍长,倍长后每个长度为 \(m\) 的滑动窗口便对应了某一种偏移量,其中偏移量为 \(k\) 的所有分贡献会被累计到 \(x^{m-1+k}\) 所对应的系数中。于是,将上述两个改造后的多项式相乘,就能得到所有偏移量的结果,答案为这些结果的最大值。总复杂度 \(O(m\log m)\)。
2026沈阳邀请赛 H
也是与偏移量相关的 NTT,需要自己观察并推导出相关式子。
考虑如何求 \(g(k) = \sum_{i=1}^{n}f(S_{i}, S_{k})\):
枚举固定后缀 \(S_{k}\) 的每一位与哪些 \(S_{i}\) 产生贡献,可以得到:
\(g(k)\) 本身不好求,但是会发现其差分 \(g_{k} - g_{k+1}\) 是好求的:
前一项可以简单预处理得到;后一项等价于求原串中偏移量恰好为 \(k\) 的位置对的贡献之和。
NTT 后即可得到对于每一项 \(k \in [1, n)\),\(g(k) - g(k + 1)\);暴力求出 \(g(n)\) 后,即可得到每一项 \(g(k)\) 的值。
注意本题没有取模要求,且答案会超过 998244353,因此只能用 FFT(换用更大模数的 NTT 会 TLE...)
ABC462G
结合容斥原理
edu118F
结合容斥原理







浙公网安备 33010602011771号