反演基础

前言

在 OI 中,所谓容斥思想,很多时候就是面对题目中的强性质不好直接表示时,先表示出一个易求的弱性质,再去考虑用强性质表示出弱性质,通过反演来得到用弱性质表示的强性质。

对于不同式子的反演方法各不相同,类似的变化也比较多样,下文将结合例题来探讨各种不同反演的应用。

二项式反演

基本思想

\(f_n\) 表示恰好使用 \(n\) 个不同元素形成的特定结构的方案数,\(g_n\) 表示从 \(n\) 个不同元素中选出若干个元素形成特定结构的总方案数。显然这里的 \(g_n\) 性质是弱于 \(f_n\) 的,因为 \(g_n\) 没有限制选出元素形成特定结构的元素个数。

若已知 \(f_n\)\(g_n\),考虑枚举 \(g_n\) 中元素的个数 \(i\),如果从 \(n\) 个元素中选 \(i\) 个,每种情况的方案数就是 \(f_i\),不难得到:

\[g_n=\sum_{i=0}^n{n\choose i}f_i \]

若已知 \(g_n\)\(f_n\),我们对上式使用容斥,得到:

\[f_n=\sum_{i=0}^n{n\choose i}(-1)^{n-i}g_i \]

上面容斥式子的本质就是二项式反演。

二项式反演

二项式反演的基本形式即

\[g_n=\sum_{i=0}^n{n\choose i}f_i\iff f_n=\sum_{i=0}^n{n\choose i}(-1)^{n-i}g_i \]

这个式子还有一个等价的对称形式

\[g_n=\sum_{i=0}^n(-1)^i{n \choose i}f_i\iff f_n=\sum_{i=0}^n{n\choose i}(-1)^ig_i \]

证明

我们考虑基本形式的证明,其它形式的数值证明是类似的。

对于 \(f_n\) 的式子,我们先把其中的 \(g_i\) 展开,有

\[\begin{aligned} f_n&=\sum_{i=0}^n{n\choose i}(-1)^{n-i}g_i\\ &=\sum_{i=0}^n{n\choose i}(-1)^{n-i}\sum_{j=0}^i{i\choose j}f_j \end{aligned} \]

交换求和次序,先枚举 \(j\),再考虑枚举每个 \(i\ge j\),有

\[\sum_{j=0}^nf_j\sum_{i=j}^n{n\choose i}{i\choose j}(-1)^{n-i} \]

运用三项式版恒等式,再把与 \(i\) 无关的项提到前面,有

\[\begin{aligned} &\sum_{j=0}^nf_j\sum_{i=j}^n{n\choose j}{n-j\choose i-j}(-1)^{n-i}\\ =&\sum_{j=0}^n{n\choose j}f_j\sum_{i=j}^n{n-j\choose i-j}(-1)^{n-i}\\ \end{aligned} \]

\(k=i-j\),枚举 \(k\) 代替枚举 \(i\),剩下的式子就是 \((1-1)^{n-j}\) 的二项式定理展开,故有

\[\begin{aligned} \sum_{i=0}^n{n\choose i}(-1)^{n-i}g_i&=\sum_{j=0}^n{n\choose j}f_j\sum_{k=0}^{n-j}{n-j\choose k}(-1)^{n-j-k}\\ &=\sum_{j=0}^n{n\choose j}f_j(1-1)^{n-j}\\ &=\sum_{j=0}^n{n\choose j}f_j[n-j=0]=f_n \end{aligned} \]

推广

从线性代数的角度来看,如果把 \(f_0,f_1,\cdots,f_n\)\(g_0,g_1,\cdots,g_n\) 写成列向量,则二项式反演可以写为

\[\begin{gather*} \underbrace{ \begin{pmatrix} \binom{0}{0}&0&\cdots&0\\ \binom{1}{0}&\binom{1}{1}&\cdots&0\\ \vdots&\vdots&\ddots&\vdots\\ \binom{n}{0}&\binom{n}{1}&\cdots&\binom{n}{n} \end{pmatrix} }_{\boldsymbol A} \begin{pmatrix} f_0\\ f_1\\ \vdots\\ f_n \end{pmatrix}=\begin{pmatrix} g_0\\ g_1\\ \vdots\\ g_n \end{pmatrix} \iff \underbrace{ \begin{pmatrix} \binom{0}{0}&0&\cdots&0\\ -\binom{1}{0}&\binom{1}{1}&\cdots&0\\ \vdots&\vdots&\ddots&\vdots\\ (-1)^{n}\binom{n}{0}&(-1)^{n-1}\binom{n}{1}&\cdots&\binom{n}{n} \end{pmatrix} }_{\boldsymbol B} \begin{pmatrix} g_0\\ g_1\\ \vdots\\ g_n \end{pmatrix}=\begin{pmatrix} f_0\\ f_1\\ \vdots\\ f_n \end{pmatrix} \end{gather*} \]

所以上文的证明等价于证明 \(\boldsymbol A\cdot\boldsymbol B=\boldsymbol I\)

\(\boldsymbol A\)\(\boldsymbol B\) 转置之后得到的矩阵仍然是互逆的,所以我们还可以得到二项式反演的矩阵转置形式:

\[g_m=\sum_{i=m}^n{i\choose m}f_i\iff f_m=\sum_{i=m}^n(-1)^{i-m}{i\choose m}g_i \]

从组合意义来讲,这里的 \(g_m\) 是“至少”包含 \(m\) 个元素的方案数,与上文“至多”包含 \(m\) 个元素的方案的 \(g_m\) 不相类似。

在实际运用里,要灵活选用反演式子来解决问题。

应用

BZOJ 2839 集合计数

\(N\) 个元素的集合选出若干个子集使交集元素个数为 \(K\),求方案数 \(\bmod10^9+7\)

我们很难直接钦定怎么选能直接选出题目要求的若干子集。考虑更弱的性质,不妨设 \(f_k\) 表示选出若干子集,交集至少\(k\) 个元素的方案数。我们从 \(n\) 个元素中选定 \(k\) 个元素,方案数 \({n\choose k}\);因为我们不关心余下的元素是否在交集中,剩下的 \(n-k\) 个元素可以任意选/不选,与前面 \(k\) 个元素构成的子集方案数 \(2^{n-k}\);这些子集中至少选 \(1\) 个组成交集至少为 \(k\) 的若干子集的方案数就是 \(2^{2^{n-k}}-1\)。所以 \(f_k\) 就有

\[f_k={n\choose k}(2^{2^{n-k}}-1) \]

再设 \(g_k\) 表示选出若干个子集,交集恰好\(k\) 个元素的方案数,\(g_k\) 就是题目要求的答案。接下来我们可以使用 \(g_i\) 来表示出 \(f_k\):枚举 \(i\)\(k\)\(n\),实际要选定 \(k\) 个元素的方案数就是 \({i\choose k}\)。所以有 \(f_k\)\(g_i\) 的关系式

\[f_k=\sum_{i=k}^n{i\choose k}g_i \]

现在考虑二项式反演得出 \(g_k\) 关于 \(f_i\) 的式子。发现上式就是前文二项式反演的推广中那个式子,得到

\[\begin{aligned} g_k&=\sum_{i=k}^n(-1)^{i-k}{i\choose k}f_i\\ &=\sum_{i=k}^n(-1)^{i-k}{i\choose k}{n\choose i}(2^{2^{n-i}}-1) \end{aligned} \]

直接计算即可,复杂度 \(O(n\log n)\)

代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 1e6 + 10, mo = 1e9 + 7;
int n, k; ll ans;
ll fac[maxn], ifac[maxn], pow2[maxn];

ll qpow(ll x, ll y) {
	ll res = 1;
	while(y) {
		if(y & 1) (res *= x) %= mo;
		(x *= x) %= mo, y >>= 1;
	} return res;
}
void pre_calc() {
	fac[0] = ifac[0] = pow2[0] = 1;
	for(int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mo, pow2[i] = pow2[i - 1] * 2 % (mo - 1);
	ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = ifac[i + 1] * (i + 1) % mo;
	return;
}
ll C(int x, int y) {return fac[x] * ifac[y] % mo * ifac[x - y] % mo;}

int main() {
	ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0); 
	
	cin >> n >> k; pre_calc();
	for(int i = k; i <= n; i++) ((ans += (((i - k) & 1) ? -1 : 1) * C(i, k) * C(n, i) % mo * (qpow(2, pow2[n - i]) % mo - 1) % mo) += mo) %= mo; 
	cout << ans;
	
	return 0;
} 

Luogu P4859 已经没有什么好害怕的了

\(n\)\(A\) 物品和 \(n\)\(B\) 物品,每个物品有一个价值。将 \(A,B\) 物品两两配对,求 \(A\) 价值大于 \(B\) 价值的组数比其余组数多恰好 \(k\) 组的方案数 \(\bmod10^9+9\)

可以先计算出 \(A\) 价值大于 \(B\) 价值的组数 \(K={n+k\over2}\),如果不是整数则不可能有符合条件的方案。

类似的,\(A\)\(B\) 大的组数恰好为 \(K\) 的限制太强,先考虑易求的弱性质。
\(A,B\) 的价值分别分别排序,可以双指针简单求出每个 \(A_i\) 有多少个 \(B\) 更小的匹配数 \(d_i\) 。考虑一个 dp:设 \(f_{i,j}\) 表示已经配对前 \(i\) 个,钦定了匹配 \(j\) 组满足 \(A>B\),其余组不匹配的方案数。由于 \(A\) 有序,前面匹配上大于 \(B\) 的一定会使后面的匹配数减小。可以得到转移方程:

\[f_{i,j}=f_{i-1,j}+(d_i-(j-1))f_{i-1,j-1} \]

边界是 \(f_{0,0}=1\)
接着考虑一个 \(g_i\) 表示全部匹配上且至少有 \(i\)\(A>B\) 的方案数。如果我们可以用 \(f\) 表示出 \(g\) 就可以二项式反演求出恰好 \(K\) 组的方案数。即是在 \(f_{n,i}\) 已经钦定匹配上 \(i\) 组的基础上,剩下的 \(n-i\) 组任意匹配。就有

\[g_i=(n-j)!f_{n,i} \]

答案 \(ans_i\),即恰好匹配上 \(i\) 组的方案数可以表示出 \(g_k\)

\[g_k=\sum_{i=k}^n{i\choose k}ans_i \]

二项式反演,得到 \(ans_K\)

\[ans_K=\sum_{i=K}^n(-1)^{i-K}{i\choose K}g_i \]

直接计算即可。复杂度 \(O(n^2)\),瓶颈在 dp。

代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 2e3 + 10, mo = 1e9 + 9;
int n, k, a[maxn], b[maxn], d[maxn]; 
ll fac[maxn], ifac[maxn], f[maxn][maxn], g[maxn], ans;

ll qpow(ll x, ll y) {
	ll res = 1;
	while(y) {
		if(y & 1) (res *= x) %= mo;
		(x *= x) %= mo, y >>= 1;
	} return res;
}
void pre_calc() {
	ifac[0] = fac[0] = 1; for(int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mo;
	ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = ifac[i + 1] * (i + 1) % mo;
	return;
}
ll C(int x, int y) {return fac[x] * ifac[y] % mo * ifac[x - y] % mo;}

int main() {
	ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
	
	cin >> n >> k; pre_calc(); if((n + k) & 1) {cout << 0; return 0;} k = (n + k) / 2; 
	for(int i = 1; i <= n; i++) cin >> a[i]; sort(a + 1, a + n + 1);
	for(int i = 1; i <= n; i++) cin >> b[i]; sort(b + 1, b + n + 1);
	
	for(int i = 1, j = 1; i <= n; i++) {
		d[i] = d[i - 1];
		while(a[i] > b[j] && j <= n) j++, d[i]++;
	}//双指针求出d
	f[0][0] = 1;
	for(int i = 1; i <= n; i++) {
		f[i][0] = f[i - 1][0];
		for(int j = 1; j <= i; j++) f[i][j] = (f[i - 1][j] + (d[i] - (j - 1)) * f[i - 1][j - 1]) % mo;
	}//dp求f 	
	for(int i = 1; i <= n; i++) g[i] = fac[n - i] * f[n][i] % mo;//推式子求g 
	for(int i = k; i <= n; i++) ((ans += (((i - k) & 1) ? -1 : 1) * C(i, k) * g[i] % mo) += mo) %= mo;//反演求ans
	cout << ans; 
	
	return 0;
}

Luogu P6295 有标号 DAG 计数

\(\le n\) 个点的有标号弱连通有向无环图(有向边替换为无向边后图连通)个数 \(\bmod998244353\)

此题需要多项式与生成函数基础。

设有标号弱连通 DAG 方案数的 EGF 为 \(F(x)\),有标号 DAG 方案数的 EGF 为 \(G(x)\)。根据对 EGF 的 exp 组合意义是有标号组合对象组成的集合个数,由于弱连通 DAG 性质强于一般 DAG ,我们如果把弱连通 DAG 看做基本组合对象,其经过组合可以得到所有的一般 DAG。也就是说我们有

\[G(x)=\exp(F(x))\iff F(x)=\ln(G(x)) \]

所以如果我们可以求出 \(G(x)\) 的 EGF,再做一次多项式 \(\ln\) 就可以得到答案的 EGF。

接下来我们考虑表示出 \(G(x)\)。设 \(G(x)\) 系数为 \(g_i\),我们要尝试容斥出一个式子能够刻画 \(g_i\)
一个 DAG 计数的套路是我们可以钦定 DAG 中入度为 \(0\) 的点的个数,因为 DAG 中至少存在一个入度为 \(0\) 的点。不妨设 \(h_{i,j}\) 表示 \(i\) 个点 DAG 钦定有 \(j\) 个点入度为 \(0\),其余点不作限制的方案数,\(c_{i,j}\) 表示 \(i\) 个点 DAG 钦定恰好有 \(j\) 个入度为 \(0\) 的方案数。根据二项式反演我们可以表示出 \(h_{i,j}\)\(c_{i,j}\) 的关系有

\[h_{i,j}=\sum_{k=j}^i{k\choose j}c_{i,k}\iff c_{i,j}=\sum_{k=j}^i(-1)^{k-j}{k\choose j}h_{i,k} \]

考虑 \(h_{i,j}\) 中除开钦定的 \(j\) 个入度为 \(0\) 的点,剩下不做限制的 \(i-j\) 个点实际上构成了一个一般的 DAG,即 \(g_{i-j}\)。这两部分其中的 \(j\) 个入度为 \(0\) 的点可以向除开 \(j\) 个点本身的所有点随意连边,这是钦定入度为 \(0\) 带来的性质,所以在总共 \(j(n-j)\) 条边中可以任意选/不选,方案数就是 \(2^{j(n-j)}\)。所以有 \(h_{i,j}\)\(g_{i-j}\) 的关系式

\[h_{i,j}={i\choose j}2^{j(n-j)}g_{i-j} \]

\(\sum c_{i,j}\) 实际上包括了 \(i\) 个点可能构成的所有 DAG 的方案,也就是说 \(g_i\) 可以直接用 \(c_{i,j}\) 表示,有

\[g_i=\sum_{j=1}^ic_{i,j} \]

现在我们可以做一个代换,对上式用 \(h\) 表示 \(c\),再用 \(g\) 表示 \(h\),得到 \(g\)\(g\) 的关系即

\[\begin{aligned} g_n&=\sum_{i=1}^nc_{n,i}\\ &=\sum_{i=1}^n\sum_{j=i}^n(-1)^{j-i}{j\choose i}h_{n,j}\\ &=\sum_{i=1}^n\sum_{j=i}^n(-1)^{j-i}{j\choose i}{n\choose j}2^{j(n-j)}g_{n-j}\\ \end{aligned} \]

先想办法化简,考虑交换求和次序,凑二项式定理

\[\begin{aligned} g_n&=\sum_{j=1}^n{n\choose j}2^{j(n-j)}g_{n-j}\sum_{i=1}^j{j\choose i}(-1)^{j-i}\\ &=\sum_{j=1}^n{n\choose j}2^{j(n-j)}g_{n-j}(\sum_{i=0}^j{j\choose i}(-1)^{j-i}-{j\choose 0}(-1)^j)\\ &=\sum_{j=1}^n{n\choose j}2^{j(n-j)}g_{n-j}((1-1)^j-(-1)^j)\\ &=\sum_{j=1}^n{n\choose j}2^{j(n-j)}g_{n-j}(-1)^{j+1}\\ \end{aligned} \]

接下来我们的核心问题集中在处理 \(2^{j(n-j)}\) 这个离奇的式子上,实际上我们也可以先把 \({n\choose j}\) 拆开找找灵感

\[g_n=\sum_{j=1}^n{n!\over j!(n-j)!}2^{j(n-j)}g_{n-j}(-1)^{j+1} \]

如果您的注意力比较不错,就会发现拆开后两边 \(g\) 的下标都出现在阶乘里。如果我们调整一下位置

\[{g_n\over n!}=\sum_{j=1}^n{(-1)^{j+1}\over j!}2^{j(n-j)}{g_{n-j}\over(n-j)!} \]

出现了 \(G(x)\) 系数的 EGF 形式!而且两边还是是同构的!
现在我们要把 \(2^{j(n-j)}\) 拆成易处理的,且同样含有 \(n,(n-j)\) 项的东西。
实际上有一个不太常见的凑式子 trick:\(j(n-j)={n\choose 2}-{j\choose 2}-{n-j\choose 2}\)。数值上易证。
所以就又可以继纟卖化简口拉:

\[\begin{aligned} {g_n\over n!}&=\sum_{j=1}^n{(-1)^{j+1}\over j!}{2^{n\choose2}\over2^{j\choose2}2^{{n-j\choose2}}}{g_{n-j}\over(n-j)!}\\ {g_n\over2^{n\choose2} n!}&=\sum_{j=1}^n{(-1)^{j+1}\over2^{j\choose2} j!}{g_{n-j}\over2^{n-j\choose2}(n-j)!} \end{aligned} \]

设 $$P(x)=\sum_{i\ge0}{g_i\over2^{i\choose2} i!}x^i$$

\[Q(x)=\sum_{i\ge1}{(-1)^{i+1}\over2^{i\choose2}i!}x^i \]

把上式两边同时看做 OGF 的系数,有

\[\begin{aligned} \sum_{n\ge 0}{g_n\over2^{n\choose2} n!}x^n&=\sum_{n\ge 0}\sum_{j=1}^n{(-1)^{j+1}\over2^{j\choose2} j!}{g_{n-j}\over2^{n-j\choose2}(n-j)!}x^n\\ P(x)&=\sum_{j=1}^n{(-1)^{j+1}\over2^{j\choose2} j!}x^j\sum_{n\ge0}{g_{n-j}\over2^{n-j\choose2}(n-j)!}x^{n-j} \end{aligned} \]

可以发现右式和 \(P(x)Q(x)\) 很像。根据卷积的定义,如果 \(A(x),B(x)\) 都是下标从 \(0\) 开始的生成函数,我们有

\[A(x)B(x)=\sum_{n\ge0}\sum_{i=0}^na_ib_{n-i}x^n \]

然而 \(Q(x)\) 下标是从 \(1\) 开始的,这意味着我们要调整第二个求和使其没有 \(q_0\) 这一项,即

\[P(x)Q(x)=\sum_{n\ge0}\sum_{i=0}^{n-1}p_iq_{n-i}x^n \]

发现将上界调整为 \(n-1\) 就可以解决这个问题。
接着我们再用 \(j=i+1\) 来作为指标使上式看起来更接近原式,有

\[P(x)Q(x)=\sum_{n\ge0}\sum_{j=1}^{n}p_{j-1}q_{n-j+1}x^n \]

这时候我们突然发现—— \(n=0\) 的这一项消失了!但是原式是有 \(g_0\) 这一项的。我们要拿 \(P(x)Q(x)\) 替换原式就要补上 \(g_0\)

那么 \(g_0\) 应该是多少呢?我们回忆一下之前的一个式子

\[g_n=\sum_{j=1}^n{n!\over j!(n-j)!}2^{j(n-j)}g_{n-j}(-1)^{j+1} \]

容易得到 \(g_1=1\),因为一个点的 DAG 当然只有一种情况。
于是上式带入 \(n=1\),就得到了 \(g_0=g_1=1\)

于是前面的 \(P(x)\) 就被表示出来了!有

\[P(x)=P(x)Q(x)+1\iff P(x)={1\over1-Q(x)} \]

其中

\[P(x)=\sum_{i\ge0}{g_i\over2^{i\choose2} i!}x^i,\ Q(x)=\sum_{i\ge1}{(-1)^{i+1}\over2^{i\choose2}i!}x^i \]

\(Q(x)\) 可以直接 \(O(n\log n)\) 计算得到。

\(1-Q(x)\) 做一遍多项式求逆就得到了 \(P(x)\)

\(P(x)\) 处理一下就得到了 \(G(x)\) 的 EGF。

\(G(x)\) 的 EGF 做一遍多项式 \(\ln\) 就得到了 \(F(x)\) 的 EGF。

\(F(x)\) 的 EGF 转成 OGF 就得到答案啦啦啦!

代码实现
#include<bits/stdc++.h>
typedef long long ll;

using namespace std;

const int maxn=2e5 + 10, mo=998244353, inv2 = 499122177;
int n, fac[maxn], ifac[maxn];

//以下是多项式板子 
inline int add(int x,int y){
	return (x+y>=mo)?x+y-mo:x+y;
}

inline int sub(int x,int y){
	return (x-y<0)?x-y+mo:x-y;
}

inline int mul(int x,int y){
	return 1ll*x*y%mo;
}

int qpow(int x,ll y){
	int rs=1;
	while(y){
		if(y&1) rs=1ll*rs*x%mo;
		x=1ll*x*x%mo;
		y>>=1;
	}
	return rs;
}

int rev[1<<20];	

void init_rev(int len){
	for(int i=0;i<len;++i){
		rev[i]=rev[i>>1]>>1;
		if(i&1) rev[i]|=len>>1;
	}
}

namespace Polynomial{
	struct poly{
		int a[1<<20];
		
		poly(){};
		
		inline int& operator[](int x){
			return a[x];
		}
		
		inline int operator[](int x)const{
			return a[x];
		}
		
		inline void operator=(const poly &t){
			memcpy(a,t.a,sizeof(a));
			return;
		}
		
		inline void slice(int len,int k){
			for(int i=len;i<k;++i) a[i]=0;
		}
		
		void NTT(int len,int rv){
			for(int i=0;i<len;++i) if(i<rev[i]) swap(a[i],a[rev[i]]);
			for(int h=2;h<=len;h<<=1){
				int wn=qpow(3,(mo-1)/h);
				for(int i=0;i<len;i+=h){
					int w=1;
					for(int k=i;k<i+h/2;++k){
						int u=a[k],v=mul(w,a[k+h/2]);
						a[k]=add(u,v);
						a[k+h/2]=sub(u,v);
						w=mul(w,wn);
					}
				}
			}
			if(rv==-1){
				reverse(a+1,a+len);
				int inv=qpow(len,mo-2);
				for(int i=0;i<len;++i) a[i]=mul(a[i],inv);
			}
		}
		
		poly inv(int len)const{
			poly g,g0,d;
			g[0]=qpow(a[0],mo-2);
			for(int lim=2;(lim>>1)<len;lim<<=1){
				g0=g,d=*this;
				d.slice(lim,len);
				int k=lim<<1;
				init_rev(k);
				g0.NTT(k,1),d.NTT(k,1);
				for(int i=0;i<k;++i) g0[i]=mul(g0[i],sub(2,mul(g0[i],d[i])));
				g0.NTT(k,-1);
				g0.slice(lim,k);
				g=g0;
			}
			return g;
		}
		
		poly der(int len)const{
			poly g=*this;
			for(int i=0;i<len;++i) g[i]=mul(g[i+1],i+1);
			return g;
		}
		
		poly integ(int len)const{
			poly g=*this;
			for(int i=len-1;i;--i) g[i]=mul(g[i-1],qpow(i,mo-2));
			g[0]=0;
			return g;
		}
		
		poly ln(int len)const{
			poly iv=this->inv(len),rs;
			poly d=this->der(len);
			int k=len<<1;
			init_rev(k);
			d.NTT(k,1),iv.NTT(k,1);
			for(int i=0;i<k;++i) rs[i]=mul(d[i],iv[i]);
			rs.NTT(k,-1);
			rs=rs.integ(k);
			rs.slice(len,k);
			return rs;
		}
	};
}
using namespace Polynomial;
poly P,Q,F,G;
//以上是多项式板子 

int len = 1;

void pre_calc() {
	fac[0] = ifac[0] = 1; for(int i = 1; i <= n; i++) fac[i] = 1ll * fac[i - 1] * i % mo;
	ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mo;
	while(len<=n) len<<=1;
	return;
}

int main() {
	ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
	
	cin >> n; n++; pre_calc();
	Q[0] = 1; for(int i = 1; i < n; i++) Q[i] = 1ll * qpow(inv2, 1ll * i * (i - 1) / 2) * ifac[i] % mo, Q[i] = (i & 1) ? mo - Q[i] : Q[i];
	P = Q.inv(len);
	for(int i = 0; i < n; i++) G[i] = 1ll * P[i] * qpow(2, 1ll * i * (i - 1) / 2) % mo;
	F = G.ln(len);  
	for(int i = 1; i < n; i++) cout << 1ll * F[i] * fac[i] % mo << endl;
	
	return 0;
} 

后记

拼尽全力终于在花了整整一天时间解决了此题
生成函数卷积那个地方看了接近两个小时才大概懂

蒟蒻宝宝的第三道黑题,第一道黑题题解
应该还是挺有质量的罢

posted @ 2025-03-18 20:41  Ydoc770  阅读(65)  评论(0)    收藏  举报