容斥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)

山东省队集训

给定一张 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)

就可以做了。

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

后记

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/146754.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

本田发布全新CB1000 Hornet,是杜卡迪街霸劈了腿还是Z1000红杏出墙?

米兰车展上&#xff0c;本田带来了全新的大黄蜂CB1000 Hornet&#xff0c;外观方面抛弃了之前的本田推出的Neo Sports Caf风格&#xff0c;新款的外观看起来要更加战斗一点。不过新的这个前脸改的&#xff0c;我只能说是杜卡迪街霸劈了腿还是Z1000红杏出墙&#xff1f;外观方面…

【Python】Numpy(学习笔记)

一、Numpy概述 1、Numpy Numpy&#xff08;Numerical Python&#xff09;是一个开源的Python科学计算库&#xff0c;用于快速处理任意维度的数组。 Numpy使用ndarray对象来处理多维数组&#xff0c;该对象是一个快速而灵活的大数据容器&#xff0c; Numpy num - numerical 数…

Kohana框架的安装及部署

Kohana框架的安装及部署 tipsKohana安装以及部署1、重要文件作用说明1.1 /index.php1.2 /application/bootstrap.php 2、项目结构3、路由配置3.1、隐藏项目入口的路由3.2、配置默认路由3.3、配置自定义的路由(Controller目录下的控制器)3.4、配置自定义的路由(Controller/direc…

JS操作canvas

<canvas>元素本身并不可见&#xff0c;它只是创建了一个绘图表面并向客户端js暴露了强大的绘图API。 1 <canvas> 与图形 为优化图片质量&#xff0c;不要在HTML中使用width和height属性设置画布的屏幕大小。而要使用CSS的样式属性width和height来设置画布在屏幕…

父组件用ref获取子组件数据

子组件 Son/index.vue 子组件的数据和方法一定要记得用defineExpose暴露&#xff0c;不然父组件用ref是获取不到的&#xff01;&#xff01;&#xff01; <script setup> import { ref } from "vue"; const sonNum ref(1); const changeSon () > {sonNum.…

DAY54 392.判断子序列 + 115.不同的子序列

392.判断子序列 题目要求&#xff1a;给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;例如&#xff0c;"ace"是…

探秘Vue组件间通信:详解各种方式助你实现目标轻松搞定!

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! ​ 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一…

threejs(13)-着色器设置点材质

着色器材质内置变量 three.js着色器的内置变量&#xff0c;分别是 gl_PointSize&#xff1a;在点渲染模式中&#xff0c;控制方形点区域渲染像素大小&#xff08;注意这里是像素大小&#xff0c;而不是three.js单位&#xff0c;因此在移动相机是&#xff0c;所看到该点在屏幕…

基于单片机的电源切换控制器设计(论文+源码)

1.系统设计 在基于单片机的电源切换控制器设计中&#xff0c;系统功能设计如下&#xff1a; &#xff08;1&#xff09;实现电源的电压检测&#xff1b; &#xff08;2&#xff09;如果电压太高&#xff0c;通过蜂鸣器进行报警提示&#xff0c;继电器进行切换&#xff0c;使…

Idea 编译SpringBoot项目Kotlin报错/Idea重新编译

原因应该是一次性修改了大量的文件, SpringBoot项目启动Kotlin报错, Build Project也是同样的结果, 报错如下 Error:Kotlin: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.1.13. Build-&…

python语言的由来与发展历程

Python语言的由来可以追溯到1989年&#xff0c;由Guido van Rossum&#xff08;吉多范罗苏姆&#xff09;创造。在他的业余时间里&#xff0c;Guido van Rossum为了打发时间&#xff0c;决定创造一种新的编程语言。他受到了ABC语言的启发&#xff0c;ABC语言是一种过程式编程语…

Hadoop-HDFS架构与设计

HDFS架构与设计 一、背景和起源二、HDFS概述1.设计原则1.1 硬件错误1.2 流水访问1.3 海量数据1.4 简单一致性模型1.5 移动计算而不是移动数据1.6 平台兼容性 2.HDFS适用场景3.HDFS不适用场景 三、HDFS架构图1.架构图2.Namenode3.Datanode 四、HDFS数据存储1.数据块存储2.副本机…

亚马逊云AI大语言模型应用下的创新Amazon Transcribe的使用

Transcribe简介 语音识别技术&#xff0c;也被称为自动语音识别&#xff08;Automatic Speech Recognition&#xff0c;简称ASR&#xff09;&#xff0c;其目标是将人类的语音中的词汇内容转换为计算机可读的输入&#xff0c;例如按键、二进制编码或者字符序列。语音识别技术已…

2023-11-14 LeetCode每日一题(阈值距离内邻居最少的城市)

2023-11-14每日一题 一、题目编号 1334. 阈值距离内邻居最少的城市二、题目链接 点击跳转到题目位置 三、题目描述 有 n 个城市&#xff0c;按从 0 到 n-1 编号。给你一个边数组 edges&#xff0c;其中 edges[i] [fromi, toi, weighti] 代表 fromi 和 toi 两个城市之间的…

阿里达摩院开源DAMO-YOLO

1.简介 DAMO-YOLO是一个兼顾速度与精度的目标检测框架&#xff0c;其效果超越了目前的一众YOLO系列方法&#xff0c;在实现SOTA的同时&#xff0c;保持了很高的推理速度。DAMO-YOLO是在YOLO框架基础上引入了一系列新技术&#xff0c;对整个检测框架进行了大幅的修改。具体包括…

c语言从入门到实战——基于指针的数组与指针数组

基于指针的数组与指针数组 前言1. 数组名的理解2. 使用指针访问数组3. 一维数组传参的本质4. 冒泡排序5. 二级指针6. 指针数组7. 指针数组模拟二维数组 前言 指针的数组是指数组中的元素都是指针类型&#xff0c;它们指向某种数据类型的变量。 1. 数组名的理解 我们在使用指针…

Excel-快速将公式运用到一整列

先在该列的第一个单元格里写好公式&#xff0c;然后单击该单元格 在图中标示的地方输入我们需要填充的单元格区域 同时按住Ctrl和Enter键&#xff0c;这时需要填充的单元格区域就都被选中了 然后单击一下图中公式的后面&#xff0c;再次按下Ctrl和Enter键&#xff0c;这样就完…

第3章:搜索与图论【AcWing】

文章目录 图的概念图的概念图的分类有向图和无向图 连通性连通块重边和自环稠密图和稀疏图参考资料 图的存储方式邻接表代码 邻接矩阵 DFS全排列问题题目描述思路回溯标记剪枝代码时间复杂度 [N 皇后问题](https://www.luogu.com.cn/problem/P1219)题目描述全排列思路 O ( n ! …

Unity--互动组件(Toggle)

1.组件的可交互 2.组件的过渡状态 3.组件的导航 4.Toggle的属性和参数设置 Toggle 切换控制是一个复选框&#xff0c;允许用户打开或关闭的一个选项&#xff1b; ”Toggle的属性和参数&#xff1a;“” Is on&#xff1a;&#xff08;开启&#xff09; 拨动开关是否从一开…

二叉树基础

前言 我们好久没有更新数据结构的博文了&#xff0c;今天来更新一期树&#xff01;前几期我们已经介绍了顺序表、链表&#xff0c;栈和队列等基本的线性数据结构并对其分别做了实现&#xff0c;本期我们再来介绍一个灰常重要的非线性基本结构 ---- 树型结构。 本期内容介绍 树…