容斥原理学习笔记

定义

设全集 \(U\) 中有 \(n\) 种不同的属性,而第 \(i\) 种属性称为 \(P_i\),拥有属性 \(P_i\) 的元素构成集合 \(S_i\),那么

$ \left | \bigcup_{i=1}^{n} S_i\right | =\sum \left | S_i \right |-\sum_{i<j} \left | S_i\cap S_j \right |+\sum_{i<j<k} \left | S_i\cap S_j \cap S_k\right |+...+(-1)^{n-1}\left | S_1 \cap... \cap S_n \right |
$

\(\left | \bigcup_{i=1}^{n} S_i \right | =\sum_{m=1}^{n} (-1)^{m-1} \sum_{a_i<a_i+1} \left | \bigcap_{i=1}^{m} S_{a_i} \right | \)

硬币购物

共有 \(4\) 种硬币。面值分别为 \(c_1,c_2,c_3,c_4\)

某人去商店买东西,去了 \(n\) 次,对于每次购买,他带了 \(d_i\)\(i\) 种硬币,想购买 \(s\) 的价值的东西。请问每次有多少种付款方法。

\(1 \leq c_i, d_i, s \leq 10^5\)\(1 \leq n \leq 1000\)

思路

转换一下,本题实际上就是求 \(\sum_{i=1}^{4} x_ic_i=s,0\leq x_i \leq d_i\) 的非负整数解的个数。

抽象出容斥原理的模型:

1.全集 U:不定方程 \(\sum_{i=1}^{4} x_ic_i=s\) 的非负整数解的个数。

2.元素:变量 \(x_i\)

3.属性:\(x_i \leq b_i\)

那么本题的答案就是 $\left | \bigcap_{i=1}^{n} S_i \right |=\left | \bigcup \right | -\left | \bigcup_{i=1}^{n} \overline{S_i} \right | $。

其中全集 \(\bigcup\) 可以用完全背包来做,后面的这部分就可以用到容斥。

此时的属性为 \(x_i \geq d_i+1\),可以将当前选出的属性集合中的 \(i\) 先减去 \((d_i+1)c_i\),即:

\(\sum_{i=1}^{4}x_ic_i=s-\sum_{i=1}^{k}c_{a_i}(d_{a_i}+1)\)

那么就又转化为了一个完全背包问题,这部分内容可以预处理,总的时间复杂度就为 \(O(4S+2^4n)\),其中 \(S\)\(s\) 的最大值,\(2^4\) 为每一次容斥枚举的复杂度。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10;
#define LL long long
LL f[N];int c[5],n,s,d[5];
void init(){f[0]=1;for(int i=1;i<=4;i++) for(int j=c[i];j<N;j++) f[j]+=f[j-c[i]];}
int main()
{
	for(int i=1;i<=4;i++) scanf("%d",&c[i]);scanf("%d",&n);init();
	while(n--)
	{
		for(int i=1;i<=4;i++) scanf("%d",&d[i]);scanf("%d",&s);LL ans=f[s];
		for(int i=1;i<16;i++)
		{
			LL t=s,cnt=0;for(int j=0;j<4;j++) if(i>>j&1) cnt++,t-=(d[j+1]+1)*c[j+1];
			if(cnt%2==0&&t>=0) ans+=f[t];
			else if(t>=0) ans-=f[t];
		}
		printf("%lld\n",ans);
	}
	return 0;
}

矩阵填数

给定一个 \(h \times w\) 的矩阵,矩阵的行编号从上到下依次为 \(1 \sim h\),列编号从左到右依次 \(1 \sim w\)

在这个矩阵中你需要在每个格子中填入 \(1 \sim m\) 中的某个数。

给这个矩阵填数的时候有一些限制,给定 \(n\) 个该矩阵的子矩阵,以及该子矩阵的最大值 \(v\),要求你所填的方案满足该子矩阵的最大值为 \(v\)

答案对 \(10 ^ 9 + 7\) 取模。

\(1 \le h, w, m \le 10 ^ 4\)\(1 \le n \le 10\)\(1 \le v \le m\)

思路:

设第 \(i\) 个矩阵填入的数中的最大值为 \(x_i\),那么本题中的属性 \(S_i\) 即为 \(x_i=v_i\)。全集为 \(x_i \leq v_i\)

考虑容斥,还是求出答案的补集,则属性 \(\overline{S_i}\)\(x_i<v_i\)。对于一个大小为 \(siz\) 的矩阵,总的方案数就为 \(x_i^{siz}\)

需要注意的是,矩阵之间可能存在交集,所以需要将矩阵再切割为若干的小矩阵,对于每一块新的小矩阵,要在所有的 \(v_i\) 中取最小值。

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=10010,mod=1e9+7;
int minv[110][110],pos[N][4],v[N],X[N],Y[N],totx,toty,h,w,m,n,ans,siz[N];
int mul(int a,int b){int res=1;while(b) ((b&1)&&(res=1ll*res*a%mod,0)),a=1ll*a*a%mod,b>>=1;return res;}
void chkmin(int &a,int b){if(b<a) a=b;}
void solve()
{
	scanf("%d%d%d%d",&h,&w,&m,&n);totx=toty=ans=0;
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d%d%d%d",&pos[i][0],&pos[i][1],&pos[i][2],&pos[i][3],&v[i]);
		X[++totx]=pos[i][0],X[++totx]=++pos[i][2];Y[++toty]=pos[i][1],Y[++toty]=++pos[i][3];
	}
	X[++totx]=1,X[++totx]=w+1;Y[++toty]=1,Y[++toty]=h+1;
	sort(X+1,X+totx+1);totx=unique(X+1,X+totx+1)-X-1;
	sort(Y+1,Y+toty+1);toty=unique(Y+1,Y+toty+1)-Y-1;
	for(int i=0;i<(1<<n);i++)
	{
		for(int j=1;j<totx;j++) for(int k=1;k<toty;k++) minv[j][k]=m;
		for(int j=1;j<=n;j++)
		    for(int x=lower_bound(X+1,X+totx+1,pos[j][0])-X;X[x]!=pos[j][2];x++)
		    for(int y=lower_bound(Y+1,Y+toty+1,pos[j][1])-Y;Y[y]!=pos[j][3];y++)
		        chkmin(minv[x][y],v[j]-((i>>j-1)&1));
		int tmp=1;
		for(int j=1;j<totx;j++) for(int k=1;k<toty;k++) tmp=1ll*tmp*mul(minv[j][k],(X[j+1]-X[j])*(Y[k+1]-Y[k]))%mod;
		ans=(ans+(siz[i]&1?-1ll:1ll)*tmp)%mod;
	}
	printf("%d\n",(ans+mod)%mod);
}
int main()
{
	for(int i=1;i<1024;i++) siz[i]=siz[i>>1]+(i&1);
	int T;scanf("%d",&T);while(T--) solve();
	return 0;
}

Emiya 家今天的饭

给出一个 \(n \times m\) 的矩阵,\(a_{i,j}\) 表示第 \(i\) ,第 \(j\) 列的格子中有 \(a_{i,j}\) 个互不相同的元素。

现在要从矩阵中取出若干个元素,满足:

1.至少取出一个元素。

2.每行至多取出一个元素。

3.每列选出的元素总量不能超过总数的一半。

求满足上述条件的方案数对 \(998,244,353\) 取模的结果。

\(1 \leq n \leq 100\)\(1 \leq m \leq 2000\)\(0 \leq a_{i,j} \lt 998,244,353\)

思路:

注意到至多只有一列元素不满足列的限制,考虑容斥,即枚举不合法的列。而对于拿走合法的列中的元素,我们并不关心它在具体的哪一列。

设当前枚举的不合法列为 \(col\),设 \(f_{i,j,k}\) 表示前 \(i\) 行在 \(col\) 这一列中选择了 \(j\) 个元素,在其他列中选择了 \(k\),令 \(s_i\) 为第 \(i\) 行元素的总和,那么可以得到转移方程:

\(f_{i,j,k}=f_{i-1,j,k}+a_{i,col}\times f_{i-1,j-1,k}+(s_i-a_{i,col}) \times f_{i-1,j,k-1}\)

总的时间复杂度为 \(O(mn^3)\),考虑优化。

注意到最终统计答案时只关心 \(j\)\(k\) 的相对大小关系,而并不需要知道具体的值,那么就可以考虑将状态优化为 \(f_{i,j}\),表示前 \(i\) 行中第 \(col\) 列比其他列多选了 \(j\) 个元素,得到转移方程:

\(f_{i,j}=f_{i-1,j}+f_{i-1,j-1} \times a_{i,col}+f_{i-1,j+1} \times (s_i-a_{i,col})\)

此时的复杂度就成功降低到了 \(O(mn^2)\)

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110,M=2010,mod=998244353;
int a[N][M],s[N],f[N][N*2],n,m,ans=1;
int mul(int a,int b){int res=1;while(b) ((b&1)&&(res=1ll*res*a%mod,0)),a=1ll*a*a%mod,b>>=1;return res;}
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
void sub(int &a,int b){a-=b;if(a<0) a+=mod;}
int main()
{
	freopen("in.txt","r",stdin);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++) scanf("%d",&a[i][j]),add(s[i],a[i][j]);
		ans=1ll*ans*(s[i]+1)%mod;
	}
	sub(ans,1);
	for(int col=1;col<=m;col++)
	{
		for(int i=1;i<=n;i++) for(int j=0;j<=2*n;j++) f[i][j]=0;f[0][n]=1;
		for(int i=1;i<=n;i++)
		    for(int j=n-i;j<=n+i;j++)
		        add(f[i][j],f[i-1][j]),add(f[i][j],1ll*f[i-1][j-1]*a[i][col]%mod),add(f[i][j],1ll*f[i-1][j+1]*(s[i]-a[i][col]+mod)%mod);
		for(int j=n+1;j<=2*n;j++) sub(ans,f[n][j]);
	}
	printf("%d\n",ans);
	return 0;
}

染色问题

萌萌家有一个棋盘,这个棋盘是一个 \(n \times m\) 的矩形,分成 \(n\)\(m\) 列共 \(n \times m\) 个小方格。

现在萌萌和南南有 \(C\) 种不同颜色的颜料,他们希望把棋盘用这些颜料染色,并满足以下规定:

  1. 棋盘的每一个小方格既可以染色(染成 \(C\) 种颜色中的一种),也可以不染色。
  2. 棋盘的每一行至少有一个小方格被染色。
  3. 棋盘的每一列至少有一个小方格被染色。
  4. 每种颜色都在棋盘上出现至少一次。

求不同染色方案总数对 \(1,000,000,007\) 取模的值。

\(1 \le n,m,c \le 400\)

思路:

考虑容斥,设出现过的颜色数量为 \(x\),令全集为 \(x \leq c\)。那么所求的答案为 \(x=c\),由于每种颜色相互等价,可以直接枚举出现过的颜色数量的最大值 \(i\),即满足 \(x \leq i\),方案数再乘上一个组合数即可。

\(f[i]\) 表示至多用 \(i\) 种颜色的染料给整个棋盘染色的方案数。由于需要满足每一行每一列至少有一个格子被染色,这一部分也可以考虑容斥。

\(j\) 表示至多在 \(j\) 列棋盘中染色,那么每一行的答案就是 \((i+1)^j-1\)。那么也只需要枚举 \(j\),再乘上组合数就可以得到 \(f_i\)

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=410,mod=1e9+7;
int n,m,k,fac[N],infac[N],f[N];
int mul(int a,int b){int res=1;while(b) (b&1)&&(res=1ll*res*a%mod,0),a=1ll*a*a%mod,b>>=1;return res;}
int C(int n,int m){return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;}
int main()
{
	scanf("%d%d%d",&n,&m,&k);fac[0]=infac[0]=1;int ans=0;
	for(int i=1;i<=max(max(n,m),k);i++) fac[i]=1ll*fac[i-1]*i%mod,infac[i]=mul(fac[i],mod-2);
	for(int i=1;i<=k;i++)
		for(int j=m,cur=1;j>=1;j--,cur=mod-cur)
			f[i]=(f[i]+1ll*C(m,j)*mul(mul(i+1,j)-1,n)%mod*cur)%mod;
	for(int i=k,cur=1;i>=1;i--,cur=mod-cur) ans=(ans+1ll*C(k,i)*f[i]%mod*cur)%mod;
	printf("%d\n",ans);
	return 0;
}

有两棵均有 \(n\) 个节点的树。

第一棵树的生成方式是:

  1. 节点 \(1\) 作为树的根。
  2. 对于 \(i \in [2, n]\),从 \([1, i - 1]\) 中选取一个节点作为 \(i\) 的父亲。

第二棵树的生成方式是:

  1. 节点 \(n\) 作为树的根。
  2. 对于 \(i \in [1, n - 1]\),从 \([i + 1, n]\) 中选取一个节点作为 \(i\) 的父亲。

对于任意 \(i \in [1, n]\),若第一棵树中的节点 \(i\) 为叶子,那么第二棵树中的节点 \(i\) 为非叶子;若第一棵树中的节点 \(i\) 为非叶子,那么第二棵树中的节点 \(i\) 为叶子。一个节点被称为叶子当且仅当没有节点的父亲是它。

统计生成两棵树的方案数是多少。具体地,你需要对于所有 \(n \in [2, N]\) 都计算方案数。两种方案不同当且仅当存在一棵树中的一个节点 \(i\),两种方案中它的父亲不同。输出答案对 \(M\) 取模后的结果。

\(2 \le N \le 500\)\(10 \le M \le 2^{30}\)

思路:

\(f(S)\) 为第一棵树中非叶子节点集合为 \(S\) 时的方案数;

\(g(T)\) 为第二棵树中非叶子节点集合为 \(T\) 时的方案数。

那么最终的答案就是:

\(Ans=\sum_{S \cap T=\emptyset,S \cup T=\left \{ 1,2,3...n \right \} } f(S)g(T)\)

直接计算 \(f(S)\) 比较复杂,考虑容斥。设 \(f'(S)\) 表示第一棵树中非叶子节点集合为 \(S\)子集时的方案数,那么就可以得到:

\(f(S)=\sum_{S' \in S} f'(S')(-1)^{|S|-|S'|}\)

\(g(T)\) 同理,那么就可以对式子进行转换:

\(Ans=\sum_{S \cap T=\emptyset,S \cup T=\left \{ 1,2,3...n \right \} } f(S)g(T)\)

\(=\sum_{S \cap T=\emptyset,S \cup T=\left \{ 1,2,3...n \right \} } \sum_{S' \subseteq S,T' \subseteq T} f'(S')g'(T')(-1)^{|S|-|S'|+|T|-|T'|}\)

\(=\sum_{S' \subseteq S,T' \subseteq T} f'(S')g'(T')(-1)^{n-|S'|-|T'|} 2^{n-|S'|-|T'|}\)

\(=\sum_{S' \subseteq S,T' \subseteq T} f'(S')g'(T')(-2)^{n-|S'|-|T'|}\)

其中 \(2^{n-|S'|-|T'|}\) 表示,剩下的 \(n-|S'|-|T'|\) 个点可以任意存在于 \(S\)\(T\) 集合中。

\(f_{i,j,k}\),表示考虑到第 \(i\) 个点时,\([1,i]\) 中有 \(j\) 个节点在 \(S'\) 集合中,\([i+1,n]\) 中有 \(k\) 个节点在 \(T'\) 集合中。

那么对于当前的点,它在第一棵数中可以接在 \(j\) 个节点下面,在第二棵树中可以接在 \(k\) 个节点下面,总的方案数就是 \(j \times k\)

枚举第 \(i+1\) 号点在哪个集合当中,可以得到状态转移方程:

\(f_{i+1,j+1,k} \gets f_{i,j,k} \times i \times j\)

\(f_{i+1,j,k-1} \gets f_{i,j,k} \times i \times j\)

\(f_{i+1,j,k} \gets f_{i,j,k} \times (-2) \times i \times j\)

初始时,\(f_{1,1,i}=1 (1\leq i < n)\)

由于 \(n\) 号节点在第一棵树中可以被接在 \(S'\) 集合中的任意一个点下面,所以最终的答案就是:\(\sum_{i=1}^{n-1} f_{n,i,1} \times i\)

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510;
int f[N][N][N],n,mod,ans;
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
void sub(int &a,int b){a-=b;if(a<0) a+=mod;}
int main()
{
//	freopen("tree_ex1.in","r",stdin);
	scanf("%d%d",&n,&mod);for(int k=1;k<n;k++) f[1][1][k]=1;
	for(int i=1;i<n;i++)
	{
		ans=0;
		for(int j=1;j<=i;j++)
		{
			for(int k=1;k<=n-i;k++)  
		    {
		    	add(f[i+1][j+1][k],1ll*f[i][j][k]*j%mod*k%mod);
		    	add(f[i+1][j][k-1],1ll*f[i][j][k]*j%mod*k%mod);
		    	add(f[i+1][j][k],1ll*(mod-2)*f[i][j][k]%mod*j%mod*k%mod);
			}
			add(ans,1ll*f[i][j][1]*j%mod);
		}
		printf("%d\n",ans);
	}
	return 0;
}
posted @ 2023-03-08 18:24  曙诚  阅读(41)  评论(0)    收藏  举报