容斥dp,二项式反演

前言

由于水平有限,这篇文章比较难懂,并且也有很多不够透彻的地方,如果您有任何的看法,非常感谢您私信指导。

容斥dp

用dp的方法来描述容斥,大概的想法是,把容斥系数分到每一步里去乘。

通常当你有容斥做法,且适配的子集条件较为一般,且数据范围不足通过时考虑使用容斥dp。

确实不太好描述,看题吧。

来源神秘

求长度为 n ≤ 2000 n\leq 2000 n2000,值域为 m ≤ 2000 m\leq 2000 m2000的序列个数,满足前一个数不是后一个数的非本身的倍数。

f i , j f_{i,j} fi,j表示前 i i i个数,第 i i i个数为 j j j的合法序列方案数,则:
f i , j = ∑ m k = 1 [ j = k 或 j ∤ k ] f i − 1 , k f_{i,j}=\underset{k=1}{\overset m\sum}[j=k 或 j\nmid k]f_{i-1,k} fi,j=k=1m[j=kjk]fi1,k
复杂度 O ( n m 2 ) O(nm^2) O(nm2)

如果我们转化成:
f i , j = ∑ m k = 1 f i − 1 , k − ∑ m k = 1 [ j ≠ k 且 j ∣ k ] f i − 1 , k f_{i,j}=\underset{k=1}{\overset m\sum}f_{i-1,k}-\underset{k=1}{\overset m\sum}[j\not=k 且 j\mid k]f_{i-1,k} fi,j=k=1mfi1,kk=1m[j=kjk]fi1,k

就可以做到 O ( n m log ⁡ m ) O(nm\log m) O(nmlogm)

n , m ≤ 1 0 5 n,m\leq 10^5 n,m105
考虑容斥,如果设 S i S_i Si表示第 i , i + 1 i,i+1 i,i+1个数满足限制的所有方案构成的集合,那么答案为 ∣ ⋂ S i ∣ |\bigcap S_i| Si,那么其补集为一些数不满足限制,也就是说是倍数关系。如果设 f ( i ) f(i) f(i)表示 i i i选出 i i i个位置不满足条件的方案数,设 g ( i ) g(i) g(i)表示恰好 n n n中有 i i i个位置不满足条件的方案数,那么相当于二项式反演的式子(虽然可以直接认为是容斥,但是虽然但是):
f ( x ) = ∑ n i = x ( i x ) g ( i ) f(x)=\underset{i=x}{\overset n\sum}\begin{pmatrix}i\\x\end{pmatrix}g(i) f(x)=i=xn(ix)g(i)
g ( x ) = ∑ n i = x ( i x ) ( − 1 ) i − x f ( i ) g(x)=\underset{i=x}{\overset n\sum}\begin{pmatrix}i\\x\end{pmatrix}(-1)^{i-x}f(i) g(x)=i=xn(ix)(1)ixf(i)
a n s = g ( 0 ) = ∑ n i = 0 ( − 1 ) i f ( i ) ans=g(0)=\underset{i=0}{\overset n\sum}(-1)^if(i) ans=g(0)=i=0n(1)if(i)

我们可以认为一个不合法的相邻位置会贡献一个 − 1 -1 1的容斥系数,具体来说会给它这种方案乘上一个 − 1 -1 1的系数,对于一种方案来说,它实际上对答案的贡献(加到答案上的值是) ( − 1 ) k (-1)^k (1)k,其中 k k k表示这种方案中有多少邻居不合法。

然后我们考虑现在的所有位置,存在着两种情况,一种叫做“非法”,一种叫做“不管”,这些东西形成了一些连续段,最终拼成了整个序列。

那么可以将数列拆成 1 1 1个不知道+ x ≥ 0 x\geq 0 x0个非法 的若干连续段,相当于编出来一个转移。

那么就可以根据这个东西把序列划分为若干个阶段,然后设出状态。即 f i f_i fi表示以 i i i为结尾的所有容斥系数之和。容易发现 f i f_i fi等于长度为 i i i的合法序列的数量。

那么就会有 f i = ∑ j ≥ 0 f i − j ⋅ w j ⋅ h ( j ) f_i=\underset{j\geq 0}{\sum}f_{i-j}\cdot w_{j}\cdot h(j) fi=j0fijwjh(j),相当于枚举最后一个非法段的长度。
那么 h ( j ) = ( − 1 ) j − 1 h(j)=(-1)^{j-1} h(j)=(1)j1,因为长度为 j j j的非法段其实指定了 j − 1 j-1 j1个非法位置,以及开头有一个不管的位置。

w j w_j wj表示长度为 j j j的非法段共有多少种,容易发现这种段的长度为 log ⁡ m \log m logm

这样可以做到 O ( ( n + m ) log ⁡ m ) O((n+m)\log m) O((n+m)logm)

n ≤ 1 0 18 , m ≤ 1 0 6 n\leq 10^{18},m\leq 10^6 n1018,m106
转移与前面 log ⁡ m \log m logm项有关,可以矩阵加速。
O ( log ⁡ 3 m log ⁡ n + m    poly log ⁡ m ) O(\log^3m\log n+m \;\text{poly}\log m) O(log3mlogn+mpolylogm)

n ≤ 1 0 18 , m ≤ 1 0 9 n\leq 10^{18},m\leq 10^9 n1018,m109:配合各类筛法以及根号分治。

AT_dp_y/CF599C

不能走黑不好做。考虑容斥,设 S i S_i Si表示第 i i i个黑色格子不走的方案数,则 a n s = ∣ ⋂ S i ∣ ans=|\bigcap S_i| ans=Si,其补集转化为走一些黑色格子。

f ( x ) f(x) f(x)表示选出 x x x个黑走的方案数,则 a n s = ∑ n i = 0 ( − 1 ) i f ( i ) ans=\underset{i=0}{\overset n\sum}(-1)^if(i) ans=i=0n(1)if(i)

我们先把黑色格子以 ( x , y ) (x,y) (x,y)升序排列,然后设 f i f_i fi表示必然走第 i i i个黑格子的所有方案的系数和,在这里会发现这实际上相当于以第 i i i个黑格子为终点,其他黑色格子不走的方案数。

那么转移就是 f i = ∑ j < i − ( x i − x j + y i − y j x i − x j ) f j f_i=\underset{j<i}{\sum}-\begin{pmatrix}x_i-x_j+y_i-y_j\\x_i-x_j\end{pmatrix}f_j fi=j<i(xixj+yiyjxixj)fj,表示枚举之前走了哪个黑色格子。
这里的 − 1 -1 1是因为当前走了一个黑色格子,所以要乘上 − 1 -1 1的系数。
组合数是表示从上一个黑色格子走到这里的方案数。

这样会发现一个问题是,这样没有办法表示是从左上角出发,到右下角结束的。因此我们强制在左上角和右下角添加一个黑色格子。并且令初值 f 1 = − 1 f_1=-1 f1=1,表示走了左上角黑色格子的方案容斥系数和。

观察转移的形式,我们会发现 f i f_i fi实际上表示的是走了左上角那个黑色格子,以及第 i i i个黑色格子的方案数,这样就可以做了。

时间复杂度 O ( n 2 ) O(n^2) O(n2)

CF1728G

标算做法是由于每次只添加一盏灯,因此会照亮一个连续段,只需要剩下的灯照亮前后缀即可,预处理前后缀就能算出来。

但是有多项式做法。

考虑容斥是怎么做的,相当于选出一些点强制不能照亮,剩下点随意,这样方案数是好求的。
那么对于一种照亮的情况,我们可以把它划分为 首尾不能照亮+中间随意的连续段,那么设 f i f_i fi表示第 i i i个点为结尾的容斥系数和,转移相当于枚举最后一个左右为不能照亮,中间是任意的连续段。(以 i i i为结尾,说明 i i i不能照亮)

那么 f i = ∑ j < i − w j , i ⋅ f j f_i=\underset{j<i}\sum- w_{j,i}\cdot f_j fi=j<iwj,ifj

w w w O ( n m 2 ) O(nm^2) O(nm2)好算的。

这样dp出来相当于强制第一个点和第 i i i个点不能照亮的方案数,所以我们在正负无穷的位置添加两个点,就可以dp出答案。

这样每次询问 w w w只会有 m 2 m^2 m2个位置(在这次询问中)改变。

复杂度为 O ( ( n + q ) m 2 ) O((n+q)m^2) O((n+q)m2)

填数(模拟赛题)

给定 n , m , k n,m,k n,m,k k k k 个区间 ( l i , r i ) (l_i,r_i) (li,ri),求有多少个长为 n n n 的正整数数组 a a a 满足:

  1. 1 ≤ a i ≤ m 1 \leq a_i \leq m 1aim
  2. 对于每个给定的区间 ( l i , r i ) (l_i,r_i) (li,ri),存在至少一对 l i ≤ p 0 < p 1 ≤ r i l_i \leq p_0 < p_1 \leq r_i lip0<p1ri,满足 a p 0 = a p 1 a_{p_0} = a_{p_1} ap0=ap1

答案对 1 0 9 + 7 10^9 + 7 109+7 取模。 1 ≤ n , m ≤ 1 0 6 1 \leq n,m \leq 10^6 1n,m106 1 ≤ k ≤ 2000 1 \leq k \leq 2000 1k2000

区间问题一个经典套路是不存在相交区间或者不存在包含区间。
本题中我们发现子集严格强于超集,因此不存在包含区间。我们把包含区间去重之后编一个后效性消除,把区间按照右端点升序排列。

然后我们把 = = =容斥为 ≠ \not= =,相当于强制一些区间当中没有重复的数字,然后用dp的方法把容斥系数乘起来。

f i f_i fi表示考虑前 i i i个区间,第 i i i个区间被选为非法的所有情况的容斥系数和,则:

f i = ∑ j < i − f j ⋅ { l i > r j : m l e n i ‾ ⋅ m l i − r j e l s e : ( m − ( r j − l i + 1 ) ) l e n i − ( r j − l i + 1 ) ‾ f_i=\underset{j<i}\sum -f_j\cdot \left\{\begin{matrix} l_i>r_j:m^{\underline{len_i}}\cdot m^{l_i-r_j}\hspace{2.25cm}\\ else:(m-(r_j-l_i+1))^{\underline{len_i-(r_j-l_i+1)}} \end{matrix}\right. fi=j<ifj{li>rj:mlenimlirjelse:(m(rjli+1))leni(rjli+1)

强制开头结尾有一个区间即可。

P4099

首先这个题有不容斥的做法,如果设 f u f_u fu表示以 u u u为子树的答案会发现没法转移,因为不知道子树内部拓扑序的情况。
如果我们想要把一个儿子加入,必须要知道这个儿子在拓扑序列中的排名,因此必须要记录进状态里,即 f u , i f_{u,i} fu,i表示以 u u u为子树, u u u在拓扑序列中的顺序为 i i i的方案数。然后编一个转移就可以了。

但是这个题也可以容斥(虽然容斥做法好像更难一点…)

具体来说,一条边其实提供了一个限制,即 u → v u\rightarrow v uv说明 u u u的拓扑序在 v v v之前,如果只有内向边是好做的,那我们选择对外向边容斥。
S i S_i Si表示满足所有内向边的限制和第 i i i条外向边限制的所有序列构成的集合,则答案为 ∣ ⋂ S i ∣ |\bigcap S_i| Si,那么补集相当于取到一些外向边,然后把它们反转为内向边,剩下一些外向边断开的方案数。

那么如果我们指定一个反转,可以认为那条外向边带有 − 1 -1 1的容斥系数。

非常容易编出来一个dp,即 f u f_u fu表示 u u u子树的答案,转移考虑把 u u u的一个儿子合并到 u u u子树内部,如果是内向边,就乘上 ( s i z u + s i z v − 1 s i z v ) \begin{pmatrix}siz_u+siz_v-1\\siz_v\end{pmatrix} (sizu+sizv1sizv)合并,表示 u u u为最后一个节点,前面 s i z u + s i z v − 1 siz_u+siz_v-1 sizu+sizv1个位置选出 s i z v siz_v sizv个位置放置 v v v子树内部的拓扑序列情况。
如果是外向边,那么就考虑两种情况。
一种叫做反转,这时候带有 − ( s i z u + s i z v − 1 s i z v ) -\begin{pmatrix}siz_u+siz_v-1\\siz_v\end{pmatrix} (sizu+sizv1sizv)的系数。
另一种叫做断开,这时候带有 ( s i z u + s i z v s i z v ) \begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} (sizu+sizvsizv)的系数,因为与 u u u没有关系了, u u u不一定是最后一个,因此可以在任意位置插入。

void dfs(int u,int fa) {
	f[u]=1;
	siz[u]=1;
	for(auto&i:a[u]) {
		int w=i.first,v=i.second;
		if(v==fa) continue;
		dfs(v,u);
		if(w) f[u]=f[u]*f[v]%M*C[siz[u]+siz[v]-1][siz[v]]%M;
		else f[u]=f[u]*f[v]%M*C[siz[u]+siz[v]-1][siz[v]-1]%M;
		siz[u]+=siz[v];
	}
}

但是会发现这样是错的。

具体错误是,假如说要合并一个子树,并且一种情况经过了断边/反转之后 u u u的连通块大小实际上为 x x x v v v的连通块大小为 y y y,但是我们在合并内向边的时候强制 u u u在最后一个,即从 s i z u + s i z v − 1 siz_u+siz_v-1 sizu+sizv1中选,实际上是不对的,我们只需要强制在 x + y x+y x+y个位置中, u u u在最后一个即可。那我们可以修改一下状态,即设 f u , x f_{u,x} fu,x表示目前所在的内向连通块大小为 x x x的方案数,现在再来编一下转移:

  • 内向边: f u , x + y ← + f u , x f v , y ( s i z u + s i z v x + y ) ( x + y − 1 y ) f_{u,x+y}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\x+y\end{pmatrix}\begin{pmatrix}x+y-1\\y\end{pmatrix} fu,x+y+fu,xfv,y(sizu+sizvx+y)(x+y1y),表示先选出拓扑序列的一些位置放连通块,然后再连通块内部安排放置顺序,在放连通块的位置内部安排顺序时,必须强制 u u u在最后一个位置。
  • 外向边取反: f u , x + y ← − f u , x f v , y ( s i z u + s i z v x + y ) ( x + y − 1 y ) f_{u,x+y}\leftarrow -f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\x+y\end{pmatrix}\begin{pmatrix}x+y-1\\y\end{pmatrix} fu,x+yfu,xfv,y(sizu+sizvx+y)(x+y1y)
  • 外向边断开: f u , x ← + f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x+fu,xfv,y(sizu+sizvsizv)
void dfs(int u,int fa) {
	f[u][1]=1;
	siz[u]=1;
	for(auto&i:a[u]) {
		int w=i.first,v=i.second;
		if(v==fa) continue;
		
		dfs(v,u);
		
		memset(g,0,sizeof g);
		
		for(int i=1;i<=siz[u];i++)
			for(int j=1;j<=siz[v]&&i+j<=n;j++)
				if(w)
					(g[i+j]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][i+j]%M*C[i+j-1][j]%M)%=M;
				else
					(g[i]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M,
					(g[i+j]+=(M-1)*f[u][i]%M*f[v][j]%M*C[siz[u]+siz[v]][i+j]%M*C[i+j-1][j]%M)%=M;
		
		memcpy(f[u],g,sizeof g);
		siz[u]+=siz[v];
	}
}

但是这样会发现连样例都过不了。原因是系数乘错了。乘错的原因是,在 f u , x f_{u,x} fu,x中,已经乘上了 ( s i z u x ) \begin{pmatrix}siz_u\\x\end{pmatrix} (sizux)作为大小为 x x x的连通块放在 s i z u siz_u sizu个位置的方案,然后在转移到 f u , x + y f_{u,x+y} fu,x+y时又选了一次,这样就选多了。事实上只需要在 s i z u + s i z v siz_u+siz_v sizu+sizv个位置中选出 s i z v siz_v sizv个位置放新子树的拓扑序信息即可。但是这样就没法限制 v v v u u u前了,具有后效性,而增加一维状态记录 u u u在拓扑序中的位置,来消除后效性在时间上是不可能的,因此我们选择推迟后效性。

转移:

  • 内向边: f u , x + y ← + f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x+y}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x+y+fu,xfv,y(sizu+sizvsizv),表示先选出拓扑序列的一些位置放连通块,然后再连通块内部安排放置顺序,在连通块内部安排顺序时,未强制 u u u在最后一个位置。
  • 外向边取反: f u , x + y ← − f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x+y}\leftarrow -f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x+yfu,xfv,y(sizu+sizvsizv)
  • 外向边断开: f u , x ← + f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x+fu,xfv,y(sizu+sizvsizv)

注意是先合并再修改 f f f,具体看代码。

当我们把 u u u的所有子树合并完成之后,考虑一个实际上大小为 x x x的连通块,其中由于我们并没有限制 u u u必须要在其他节点之前,因此答案会多算 x x x倍,因此再除掉就可以了:
f u , x ← f u , x ⋅ 1 x f_{u,x}\leftarrow f_{u,x}\cdot \frac 1x fu,xfu,xx1

#include<iostream>
#include<cstring>
#include<vector>
#include<map>
#include<numeric>
using namespace std;
const int N=1e3;
const long long M=1e9+7;
long long C[N+5][N+5];
long long f[N+5][N+5];
long long g[N+5];
long long siz[N+5];
long long inv[N+5];
vector<vector<pair<int,int>>>a;
int n;
void dfs(int u,int fa) {
	f[u][1]=1;
	siz[u]=1;
	for(auto&i:a[u]) {
		int w=i.first,v=i.second;
		if(v==fa) continue;
		
		dfs(v,u);
		
		memset(g,0,sizeof g);
		
		for(int i=1;i<=siz[u];i++)
			for(int j=1;j<=siz[v]&&i+j<=n;j++)
				if(w)
					(g[i+j]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M;
				else {
					(g[i]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M;
					(g[i+j]+=(M-1)*f[u][i]%M*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M;
				}
		
		memcpy(f[u],g,sizeof g);
		siz[u]+=siz[v];
	}
	for(int i=1;i<=siz[u];i++)
		(f[u][i]*=inv[i])%=M;
}
int main() {
	for(int i=0;i<=N;i++) C[i][i]=C[i][0]=1;
	inv[0]=inv[1]=1;
	for(int i=2;i<=N;i++) (inv[i]=(M-1)*(M/i)%M*inv[M%i])%=M;
	for(int i=1;i<=N;i++)
		for(int j=1;j<=N;j++)
			(C[i][j]=C[i-1][j-1]+C[i-1][j])%=M;
	int T;
	cin>>T;
	while(T--) {
		memset(f,0,sizeof f);
		memset(siz,0,sizeof siz);
		a.resize(0);
		cin>>n;
		a.resize(n+5);
		for(int i=1,u,v;i<n;i++) {
			char c;
			cin>>u>>c>>v;
			u++;
			v++;
			if(c=='<') a[u].push_back({0,v}),a[v].push_back({1,u});
			else a[u].push_back({1,v}),a[v].push_back({0,u});
		}
		dfs(1,0);
		cout<<accumulate(f[1]+1,f[1]+n+1,0ll)%M<<endl;
	}
}
/*
1
4
1 < 0
2 < 0
3 < 0

1 
3
0 < 1
0 < 2

1
4 
0 < 1 
0 < 2 
0 < 3

*/

ARC101_C

树上任意一边都被覆盖,容斥为选出一些边不被覆盖,每选出这样一条边,就在这种方案上乘一个 − 1 -1 1的系数。

考虑一个dp,再合并完所有子树之后,如果我们要断开它与父亲的那条边,那就要求出子树内部随意匹配的方案数,显然与子树连通块的大小有关,因此要记录一维表示子树连通块的大小。

f u , i f_{u,i} fu,i表示以 u u u为根的子树目前有大小为 i i i的不知道边构成的连通块的方案数,转移就是考虑把儿子 v v v合并进来:

f u , x + y ← + f u , x f v , y ( x > 0 , y ≥ 0 ) f_{u,x+y}\leftarrow+f_{u,x}f_{v,y}(x>0,y\geq 0) fu,x+y+fu,xfv,y(x>0,y0)

先合并再修改。

如果 u u u和父亲的连边是断开的,那么 u u u父亲边的状态是“非法”,不是“未指定”,因此不知道连通块的大小为 0 0 0,则 f u , 0 f_{u,0} fu,0表示断边的容斥方案数,我们dp求出 f u , x > 0 f_{u,x>0} fu,x>0后考虑求出 f u , 0 f_{u,0} fu,0

那么 f u , 0 ← − f u , i w i f_{u,0}\leftarrow-f_{u,i}w_i fu,0fu,iwi,表示我们指定它向父亲的连边是断开的,因此带有 − 1 -1 1的系数。
w i w_i wi表示大小为 i i i的连通块任意匹配方案数,显然 w 奇数 = 0 w_{奇数}=0 w奇数=0,现在考虑偶数是如何递推的。

我们想要从 w i − 2 w_{i-2} wi2递推到 w i w_i wi,那么就需要增加一对匹配,可以选择新增一对匹配,这样方案数是 1 1 1,或者选择新增两个点,然后断开原来的 i − 2 2 \frac {i-2}2 2i2组中的一组匹配,新的两个点和旧的两个点形成两组匹配,这样配对会有 2 2 2种方案,这样方案数是 2 ( i − 2 2 ) = i − 2 2\left(\frac{i-2}2\right)=i-2 2(2i2)=i2

因此 w i = ( 1 + i − 2 ) w i − 2 = ( i − 1 ) w i − 2 w_i=(1+i-2)w_{i-2}=(i-1)w_{i-2} wi=(1+i2)wi2=(i1)wi2

或者得到 w i = ∏ j / 2 j = 1 ( 2 j − 1 ) w_i=\underset{j=1}{\overset {j/2}\prod}(2j-1) wi=j=1j/2(2j1),表示每次选出最小的点进行匹配。
初值为 f u , 1 = 1 f_{u,1}=1 fu,1=1,答案为 − f 1 , 0 -f_{1,0} f1,0,因为转移的时候假设根节点连向父亲的边断开了,所以乘了一个 − 1 -1 1的系数,要再乘回来。

#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
const int N=5e3;
const long long M=1e9+7;
vector<int> a[N+5];
long long w[N+5];//i对
long long f[N+5][N+5];
int siz[N+5];
int n;
long long g[N+5];
void dfs(int u,int fa) {
	siz[u]=1;
	for(auto&v:a[u])
		if(v^fa) {
			dfs(v,u);
			memset(g,0,sizeof g);
			for(int x=1; x<=siz[u]; x++)
				for(int y=0; y<=siz[v]; y++)
					if(x+y<=N)
						(g[x+y]+=f[u][x]*f[v][y])%=M;
			memcpy(f[u],g,sizeof f[u]);
			siz[u]+=siz[v];
		}
	for(int i=0; i<=n; i++)
		(f[u][0]+=(M-1)*f[u][i]%M*w[i]%M)%=M;
}
int main() {
	cin>>n;
	for(int i=1,u,v; i<n; i++) {
		cin>>u>>v;
		a[v].push_back(u);
		a[u].push_back(v);
	}
	w[0]=1;
	for(int i=2; i<=n; i+=2) w[i]=w[i-2]*(i-1)%M;

	for(int i=1; i<=n; i++) f[i][1]=1;
	dfs(1,0);

//	for(int i=1;i<=n;i++,cout<<endl)
//		for(int j=0;j<=n;j++,cout<<' ')
//			cout<<f[i][j];
	cout<<(M-1)*f[1][0]%M;
}

话说不知道为啥初值设为 − 1 -1 1也能过,有人知道请您联系我。

有标号连通图计数

n n n个节点的无向连通图有多少个,节点有标号,编号为 [ 1 , n ] [1,n] [1,n]
n < = 5000 n<=5000 n<=5000

这个题是朴素容斥,即考虑如何用容斥原理来修正答案。

f i f_i fi表示大小为 i i i的连通图数量,一个想法是首先连出一棵树,但是不太好搞,考虑补集转换,总边数为 i ( i − 1 ) 2 \frac{i(i-1)}2 2i(i1),总可能性为 h ( i ) = 2 i ( i − 1 ) 2 h(i)=2^{\frac {i(i-1)}2} h(i)=22i(i1)种,减去不连通的情况即可,那我们枚举 1 1 1号点所在的连通块大小 j j j,然后从剩下的 i − 1 i-1 i1个点中选出 j − 1 j-1 j1个点,情况数量为: f j ( i − 1 j − 1 ) f_j \begin{pmatrix}i-1\\ j-1\end{pmatrix} fj(i1j1),剩下 i − j i-j ij个点随意连边,转移方程为:
f i = h ( i ) − ∑ j < i f j ( i − 1 j − 1 ) h ( i − j ) f_i=h(i)-\underset{j<i}\sum f_j \begin{pmatrix}i-1\\ j-1\end{pmatrix}h(i-j) fi=h(i)j<ifj(i1j1)h(ij)

有标号DAG计数

求出 n n n个点有标号DAG有多少个, n < = 5000 n<=5000 n<=5000

f i f_i fi表示 i i i个点的有标号DAG数量。

考虑如何递推,DAG里面比较好说的要么是0入度点,要么是0出度点。我们可以指定 j j j号点是零入度点,然后把其他点连边的可能性算出来就行了。但是注意到这样限制不好的,因为可能在指定 x x x为零入度点的时候, j j j也是零入度点,这样就算重了。

可以想到容斥,选出 k k k个零入度点的贡献就是 f ( k ) = ( i k ) 2 k ( i − k ) f i − k f(k)=\begin{pmatrix}i\\k\end{pmatrix}2^{k(i-k)}f_{i-k} f(k)=(ik)2k(ik)fik

但是这样还是不对的,具体来说,设 g ( x ) g(x) g(x)表示大小为 i i i的DAG中,恰好有 x x x个零入度点的方案数,由于任意组合都会贡献,因此 f ( x ) = ∑ i j = x ( j x ) g ( j ) f(x)=\underset{j=x}{\overset i\sum}\begin{pmatrix}j\\x\end{pmatrix}g(j) f(x)=j=xi(jx)g(j),则 g ( x ) = ∑ i j = x ( j x ) ( − 1 ) j − x f ( j ) g(x)=\underset{j=x}{\overset i\sum}\begin{pmatrix}j\\x\end{pmatrix}(-1)^{j-x}f(j) g(x)=j=xi(jx)(1)jxf(j)

然后令 f i = ∑ i x = 0 g ( x ) f_i=\underset{x=0}{\overset i\sum}g(x) fi=x=0ig(x)?化简完会得到 f i = f i f_i=f_i fi=fi

注意到 g ( 0 ) g(0) g(0)表示选出 0 0 0个零入度点的方案数, g ( 0 ) g(0) g(0) f i f_i fi f i f_i fi的转移,所以把 g 0 g_0 g0这一项删掉。
这也是有实际意义的,我们一开始枚举 j j j作为 0 0 0入度的点的方案数,因此我们后面改成枚举集合时,集合大小至少为 1 1 1

f i = ∑ i x = 1 g ( x ) = ∑ i x = 1 ∑ i j = x ( j x ) ( − 1 ) j − x ( i j ) 2 j ( i − j ) f i − j = ∑ i j = 1 ( i j ) 2 j ( i − j ) f i − j ∑ i x = 1 ( j x ) ( − 1 ) j − x f_i=\underset{x=1}{\overset i\sum}g(x)=\underset{x=1}{\overset i\sum}\underset{j=x}{\overset i\sum}\begin{pmatrix}j\\x\end{pmatrix}(-1)^{j-x}\begin{pmatrix}i\\j\end{pmatrix}2^{j(i-j)}f_{i-j}=\underset{j=1}{\overset i\sum}\begin{pmatrix}i\\j\end{pmatrix}2^{j(i-j)}f_{i-j}\underset{x=1}{\overset i\sum}\begin{pmatrix}j\\x\end{pmatrix}(-1)^{j-x} fi=x=1ig(x)=x=1ij=xi(jx)(1)jx(ij)2j(ij)fij=j=1i(ij)2j(ij)fijx=1i(jx)(1)jx
= ∑ i j = 1 ( i j ) 2 j ( i − j ) f i − j ( − ( − 1 ) j + ∑ i x = 0 ( j x ) ( − 1 ) j − x ) = ∑ i j = 1 ( i j ) 2 j ( i − j ) f i − j ( ( − 1 ) j + 1 + [ j = 0 ] ) = ∑ i j = 1 ( i j ) 2 j ( i − j ) f i − j ( − 1 ) j + 1 =\underset{j=1}{\overset i\sum}\begin{pmatrix}i\\j\end{pmatrix}2^{j(i-j)}f_{i-j}\left(-(-1)^{j}+\underset{x=0}{\overset i\sum}\begin{pmatrix}j\\x\end{pmatrix}(-1)^{j-x}\right)=\underset{j=1}{\overset i\sum}\begin{pmatrix}i\\j\end{pmatrix}2^{j(i-j)}f_{i-j}\left((-1)^{j+1}+[j=0]\right)=\underset{j=1}{\overset i\sum}\begin{pmatrix}i\\j\end{pmatrix}2^{j(i-j)}f_{i-j}(-1)^{j+1} =j=1i(ij)2j(ij)fij((1)j+x=0i(jx)(1)jx)=j=1i(ij)2j(ij)fij((1)j+1+[j=0])=j=1i(ij)2j(ij)fij(1)j+1

这样就把容斥系数推出来了,当然容斥系数也可以猜出来。

这样是 O ( n 2 ) O(n^2) O(n2)的。

有标号弱连通DAG计数

同上一题,区别就是要求弱连通。

容斥,枚举 1 1 1所在的弱连通块的大小。则答案为 f n − ∑ n − 1 i = 1 f i f n − i f_n-\underset{i=1}{\overset {n-1}\sum}f_i f_{n-i} fni=1n1fifni

山东省队集训

给定一张 n ≤ 15 n\leq 15 n15个点 m ≤ 200 m\leq 200 m200条边的无向图,对于所有 k k k ,请求出保留恰好 k k k条边使得整张图连通的方案数。

容斥,设 f i , S f_{i,S} fi,S表示保留了 i i i条边,使得 S S S连通的方案数。不连通就枚举 1 1 1所在的连通块。
f i , S = ( c n t S i ) − ∑ 1 ∈ T ⊊ S ∑ i j = 0 f j , T ( c n t S − T i − j ) f_{i,S}=\begin{pmatrix}cnt_S\\i\end{pmatrix}-\underset{1\in T\subsetneq S}{\sum}\underset{j=0}{\overset i\sum}f_{j,T}\begin{pmatrix}cnt_{S-T}\\i-j\end{pmatrix} fi,S=(cntSi)1TSj=0ifj,T(cntSTij)

c n t S cnt_S cntS表示两端都在 S S S内的边的数量,时间复杂度 O ( 3 n m 2 ) O(3^nm^2) O(3nm2),难以通过。

观察到转移系数只与 c n t S − T cnt_{S-T} cntST有关,把 c n t cnt cnt相同的 T T T一起转移,复杂度 O ( 3 n m + 2 n m 3 ) O(3^nm+2^nm^3) O(3nm+2nm3),可以通过:
f i , S = ( c n t S i ) − ∑ i k = 0 ∑ i j = 0 ( k i − j ) × g j ( S , k ) f_{i,S}=\begin{pmatrix}cnt_S\\i\end{pmatrix}-\underset{k=0}{\overset i\sum}\underset{j=0}{\overset i\sum}\begin{pmatrix}k\\i-j\end{pmatrix}\times g_j(S,k) fi,S=(cntSi)k=0ij=0i(kij)×gj(S,k)

其中 g i ( S , k ) = ∑ 1 ∈ T ⊊ S [ c n t S − T = k ] f i , T g_i(S,k)=\underset{1\in T\subsetneq S}\sum[cnt_{S-T}=k]f_{i,T} gi(S,k)=1TS[cntST=k]fi,T

我们枚举一个 S , T , i S,T,i S,T,i,把它贡献到对应的 g g g上即可。

或者把 f i f_i fi看成多项式的形式,那么就是 ( 1 + x ) ? (1+x)^? (1+x)?的形式,那我们令 ( 1 + x ) = y (1+x)=y (1+x)=y,转移就变成了多项式平移,可以 O ( m ) O(m) O(m)完成。

或者还有一种我没看懂的做法:
(来自lyp的PPT)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
没有看懂,如果您看懂了的话,请您联系我。

LOJ575

首先想到的是离散dp。
考虑如果我们要往序列后面添加一个数字,那么我们只需要知道原来的序列末尾在序列中的排名,就可以知道我们能添加多少个比它小/大的数字了。

f i , j f_{i,j} fi,j表示长度为 i i i的序列值域为 [ 1 , i ] [1,i] [1,i],末尾为 j j j的方案数,如果是小于号,那么 f i , j ( n − j + 1 ) + → f i + 1 , k > j f_{i,j}(n-j+1)+\rightarrow f_{i+1,k>j} fi,j(nj+1)+fi+1,k>j,表示我们向 [ 1 , i ] [1,i] [1,i]中插入一个比 j j j大的数字 k k k,把值域变为 [ 1 , i + 1 ] [1,i+1] [1,i+1],大于号同理。

但是这样 O ( n 2 ) O(n^2) O(n2)到顶了。

考虑容斥,我们对大于号容斥,那么一些大于号会变成小于号,另一些会变成不知道。
那么符号序列就可以拼成一个不知道+若干个非法。

f i f_i fi表示dp到了第 i i i个数字的系数和,转移就是枚举最后一个上升序列是 ( j , i ] (j,i] (j,i],同时要求 a j = ‘ > ’ a_j=‘>’ aj=>,表示 a j a_j aj被我们容斥为了不知道:
f i = ∑ j < i [ a j = ‘ > ’ ] ( − 1 ) c n t i − 1 − c n t j f j ( i j ) f_i=\underset{j<i}\sum [a_j=‘>’](-1)^{cnt_{i-1}-cnt_j}f_j\begin{pmatrix}i\\j\end{pmatrix} fi=j<i[aj=>](1)cnti1cntjfj(ij)

时间复杂度 O ( n 2 ) O(n^2) O(n2)

做分治NTT即可优化到 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n)

ABC236Ex

如果到图上考虑,相等连边,那么合法方案数就是图全部不连通的方案数。

S i S_i Si表示第 i i i条边不存在的方案集,则要求的就是 ∣ ⋂ S i ∣ |\bigcap S_i| Si,补集就是指定一些边存在,另一些边不知道。

那么可以对边集容斥,就获得了 O ( 2 n ( n − 1 ) 2 ) O\left(2^{\frac{n(n-1)}2}\right) O(22n(n1))的做法。

相当于我们把点集 [ n ] [n] [n]划分为若干集合 { S 1 , S 2 , S 3 , . . . , S k } \{S_1,S_2,S_3,...,S_k\} {S1,S2,S3,...,Sk},确保内部连通,外部不知道。

f S f_S fS表示考虑 S S S内部的容斥系数之和。转移就是枚举最后一个被选出的连通块。
f S = ∑ u ∈ T ⊆ S w T ⋅ h ′ ( T ) ⋅ f S − T f_S=\underset{u\in T\subseteq S}\sum w_{T}\cdot h'(T)\cdot f_{S-T} fS=uTSwTh(T)fST

那我们只需要知道 h ′ ( T ) = h ( ∣ T ∣ ) h'(T)=h(|T|) h(T)=h(T)即可,那就是使得 ∣ T ∣ |T| T个点的图连通的所有方案的容斥系数之和。

可以通过一个打表小容斥求出:
n n n个点的所有方案的容斥系数和为,枚举实际上选了几条边,得到:
H ( n ) = ∑ n ( n − 1 ) 2 i = 0 ( n ( n − 1 ) 2 i ) ( − 1 ) i = [ n = 1 ] H(n)=\underset{i=0}{\overset{\frac{n(n-1)}2}\sum}\begin{pmatrix}\frac{n(n-1)}2\\i\end{pmatrix}(-1)^i=[n=1] H(n)=i=02n(n1)(2n(n1)i)(1)i=[n=1]

那么它等于连通+不连通的系数和,如果不连通,那么枚举1所在的连通块大小:
H ( n ) = h ( n ) + ∑ n − 1 i = 1 ( n − 1 i − 1 ) h ( i ) H ( n − i ) = h ( n ) + ∑ n − 1 i = 1 ( n − 1 i − 1 ) h ( i ) [ n − 1 = i ] H(n)=h(n)+\underset{i=1}{\overset {n-1}\sum}\begin{pmatrix}n-1\\i-1\end{pmatrix}h(i)H(n-i)=h(n)+\underset{i=1}{\overset {n-1}\sum}\begin{pmatrix}n-1\\i-1\end{pmatrix}h(i)[n-1=i] H(n)=h(n)+i=1n1(n1i1)h(i)H(ni)=h(n)+i=1n1(n1i1)h(i)[n1=i]

即: [ n = 1 ] = h ( n ) + ( n − 1 ) h ( n − 1 ) [n=1]=h(n)+(n-1)h(n-1) [n=1]=h(n)+(n1)h(n1)

则: h ( n ) = [ n = 1 ] − ( n − 1 ) h ( n − 1 ) h(n)=[n=1]-(n-1)h(n-1) h(n)=[n=1](n1)h(n1)

就可以做了。

但是话说这个集合划分容斥和斯特林反演什么的有啥关系,不知道,也没有查到。如果有人知道,请您私信告诉我。

后记

作者水平不行,如果您对文中问题有任何见解,非常感谢您的私信指导!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值