基础容斥

清华集训 主旋律

给定一个 \(N\) 个点的有向图,问这个图有多少个强连通子图。保证 \(N\le 15\)

(也可以尝试去做完全图的情况:\(N\) 个点的强连通图个数。)

解法

(图里可能有 \(N^2\) 条边)

简单问题:给定一个有向图,问有多少个子图是 DAG。

DP: \(f[S]\) 表示在点集 \(S\) 之间删除一些边,使得剩下的图是个DAG的方案数。枚举哪些点的入度为 0,然后递归。

\[f[S] = \sum_{T\subseteq S, T\neq \varnothing} 2^{\#edges(T,S-T)} \cdot f[S-T]. \]

但是这个递归式会算重:枚举的时候只保证了 \(T\) 里的点入读为 0,但是 \(S-T\) 可能也有入度为0的点。使用容斥:

\[f[S] = \sum_{T\subseteq S,T\neq \varnothing} 2^{\#edges(T,S-T)} \cdot f[S-T]\cdot (-1)^{|T|-1}. \]

使用这个DP计算,计算量是 \(3^N\)

原题

一个图如果不强连通的话,那它就可以写成大于 1 个强连通分量连成的一个DAG。

强连通图的个数 = 所有图的个数 - 非强连通的个数。

尝试计算非强连通图的个数,枚举一个 \(T\),让 \(T\) 连成一个强连通图,\(T\) 不接受来自 \(S-T\) 的边。

\[f[S] = ALL(S) - \sum_{T\subseteq S, T\neq \varnothing} f[T]\cdot 2^{ways(T,S-T)}\cdot ALL(S-T). \]

ALL(S) 表示所有可能的图的个数。

\(g[T]\) 表示:

  • 把集合 \(T\) 划分成若干块,每一块里连成一个强连通块。如果有奇数个块:产生 1 的贡献;如果有偶数个块:产生 -1 的贡献。
  • \(g[T]\) 是所有的贡献之和。

\[f[S] = ALL(S) - \sum_{T\subseteq S, T\neq \varnothing} g[T]\cdot 2^{ways(T,S-T)}\cdot ALL(S-T). \]

怎么算 \(g\)?给定一个集合 \(S\),固定 \(x\in S\),枚举 x 所在的强连通块:

\[g[S] =f[S] -\sum_{T\subseteq S,x\in T} f[T]\cdot g[S-T] \]

点击查看代码

#include<bits/stdc++.h>
using namespace std;
int n,m;
int in[1<<15],out[1<<15],cnt[1<<15],sum[1<<15],w[1<<15];
long long b[215],f[1<<15],g[1<<15];
void dfs(int S,int T){
    if(S&(T-1))dfs(S,S&(T-1));
    w[T]=w[T-(T&-T)]+cnt[in[T&-T]&S];
}
const long long md=1e9+7;
int main(){
    scanf("%d%d",&n,&m);
    b[0]=1;
    for(int i=1;i<=m;i++){
        int x,y;scanf("%d%d",&x,&y),x--,y--;
        in[1<<y]|=1<<x,out[1<<x]|=1<<y;
        b[i]=b[i-1]*2%md;
    }
    for(int i=1;i<(1<<n);i++){
        int x=i-(i&-i);cnt[i]=cnt[x]+1,sum[i]=sum[x]+cnt[in[i&-i]&i]+cnt[out[i&-i]&i],f[i]=b[sum[i]];
        dfs(i,i);
        for(int j=x;j;j=x&(j-1))g[i]=(g[i]-f[i^j]*g[j]%md)%md;
        for(int j=i;j;j=i&(j-1))f[i]=(f[i]-b[sum[i]-w[j]]*g[j])%md;
        g[i]=(g[i]+f[i])%md;
    }
    printf("%lld\n",(f[(1<<n)-1]+md)%md);
    return 0;
}


ZJOI2016 小星星

给定一个 \(n\) 个点 \(m\) 条边的无向图和一个 \(n\) 个点的树。问把这个树“嵌入”到这个图里的方案数。保证 \(n\le 17\)

做法 1:在树上DP,写 \(f[x][S][y]\) 表示处理了 x 为根的子树,图里的S这些点已经被用了,\(x\) 被映射到了图里 y 这个点上。

\[f[x][S][y]\cdot f[ch[x]][S'][y'] \cdot \mathbf{1}[(y,y')\in G] \to f'[x][S\cup S'][y]. \]

直接做是 \(O(3^n\cdot n^3)\)可以用 FMT(快速莫比乌斯变换) 优化到 \(O(2^n\cdot n^{3})\).

做法 2:容斥。如果不考虑“每个图里的点都要用”,写一个DP:\(f[x][y]\) 表示 x 为根处理完,x 映射到 y 上的方案数。

这样做的话可能图里有些点没有被用到。枚举哪些点没有被用到,容斥。 \(O(2^n\cdot n^3)\)

点击查看代码


#include<bits/stdc++.h>
using namespace std;
int n,m;
bool mp[25][25];
int ver[45],ne[45],head[45],tot;
inline void link(int x,int y){
	ver[++tot]=y;
	ne[tot]=head[x];
	head[x]=tot;
}
long long dp[25][25],ans;
vector<int> vec;
void dfs(int x,int fi){
	for(auto e:vec)dp[x][e]=1;
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		if(u==fi)continue;
		dfs(u,x);
		for(auto e:vec){
			long long tmp=0;
			for(auto t:vec){
				if(!mp[e][t])continue;
				tmp+=dp[u][t];
			}dp[x][e]*=tmp;
		}
	}
}
int cnt[1<<20];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int x,y;scanf("%d%d",&x,&y);
		mp[x][y]=mp[y][x]=1;
	}
	for(int i=1;i<n;i++){
		int x,y;scanf("%d%d",&x,&y);
		link(x,y);link(y,x);
	}
	for(int s=0;s<(1<<n);s++){
		vec.clear();cnt[s]=cnt[s>>1]+(s&1);
		for(int i=0;i<n;i++)if((s>>i)&1)vec.push_back(i+1);
		dfs(1,1);
		if((n-cnt[s])&1)for(auto x:vec)ans-=dp[1][x];
		else for(auto x:vec)ans+=dp[1][x];
	}
	printf("%lld",ans);
	return 0;
}
	

SHOI 黑暗前的幻想乡

给定一个 \(n\) 个点的图。每条边被染上了 \(n-1\) 种颜色里的一种。问有多少个生成树包含了 \(n-1\) 个不同颜色的边。\(n\le 17\)。 (author:陈立杰)

如果没有颜色的限制:Matrix Tree 定理,\(O(n^3)\)

有颜色限制:枚举哪些颜色没有被用到,做容斥。\(O(2^nn^3)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
const long long md=1e9+7;
struct mat{
	long long a[17][17];
	mat(){memset(a,0,sizeof(a));}
	inline long long* operator [](int t){
		return a[t];
	}
	inline long long pwr(long long x,long long y){
		long long res=1;
		while(y){
			if(y&1)res=res*x%md;
			x=x*x%md;y>>=1;
		}return res;
	}
	inline long long det(){
		long long res=1;
		for(int i=1;i<n;i++){
			int loc=i;
			for(int j=i;j<n;j++)if(a[j][i])loc=j;
			if(loc!=i)swap(a[i],a[loc]),res=-res;res=res*a[i][i]%md;
			long long tmp=pwr(a[i][i],md-2);
			for(int j=i;j<n;j++)a[i][j]=a[i][j]*tmp%md;
			for(int j=i+1;j<n;j++){
				long long tmp=a[j][i];
				for(int t=i;t<n;t++)a[j][t]=(a[j][t]-a[i][t]*tmp)%md;
			}
		}for(int i=1;i<n;i++)res=res*a[i][i]%md;return res;
	}
};
struct node{
	int x,y;
	node(int _x,int _y){
		x=_x;y=_y;
	}
};
vector<node> vec[17];
inline long long solve(int s){
	mat res;
	for(int i=0;i<n;i++){
		if((s>>i)&1)for(auto x:vec[i]){
			res[x.x][x.x]++;res[x.y][x.y]++;
			res[x.x][x.y]--;res[x.y][x.x]--;
		}
	}return res.det();
}
int cnt[1<<17];
int main(){
	scanf("%d",&n);
	for(int i=0;i<n-1;i++){
		int m;scanf("%d",&m);
		for(int j=0;j<m;j++){
			int x,y;scanf("%d%d",&x,&y);
			vec[i].push_back(node(x-1,y-1));
		}
	}
	long long ans=0;
	for(int s=0;s<(1<<(n-1));s++){
		cnt[s]=cnt[s>>1]+(s&1);
		if((n-1-cnt[s])&1)ans=(ans-solve(s))%md;
		else ans=(ans+solve(s))%md;
	}
	printf("%lld",(ans+md)%md);

    return 0;
}

随机 K 大值

现在有 \(N\) 个独立的随机变量,第 \(i\) 个变量 \(X_i\) 服从 \([L_i, R_i]\) 上的均匀分布。问 \(X_1,\dots, X_N\) 中的第 \(K\) 大的数的期望。\(N\le 50, L_i,R_i\le 100\)

考虑简单版本:\(L_i = 0, R_i = 100\) (从 [0, 100] 里均匀独立地随机 N 个数,问其中第 K 大的期望是多少。)

通常的容斥:有 \(N\) 个限制条件,问有多少种方案 (1) 满足至少一个限制条件 / (2) 不满足任何限制条件。

如果我们问 (3) 有多少种方案满足恰好 K 个限制条件? (3‘)有多少方案满足至少 K 个条件。

\(f[S]\) 表示满足并且只满足集合 S 里的限制条件的方案数。用 \(g[S]\) 表示至少满足了集合 S 里的限制条件的方案数。

\[g[S] = \sum_{S\subseteq T} f[T]. \]

由反演公式:

\[f[T] = \sum_{T\subseteq S} g[S]\cdot (-1)^{|S-T|}. \]

那么问题 1 的答案是:

\[\sum_{S\neq \varnothing} f[S] = ALL - f[\varnothing] = ALL - \sum_{S} g[S]\cdot (-1)^{|S|} = \sum_{S\ne \varnothing} g[S]\cdot (-1)^{|S|-1}. \]

这里是因为 \(g[\varnothing] = ALL\)

问题 (3):

\[\sum_{|S| = K} f[S] = \sum_{|S|=K}\sum_{S\subseteq T} g[T]\cdot (-1)^{|T-S|} \\ = \sum_{|S|= K} g[T] \cdot \sum_{S\subseteq T,|S|= K} (-1)^{|T-S|} \\ = \sum_{|S|= K} g[T] \cdot {|S|\choose K}\cdot (-1)^{|S|-K} \\ \]

问题 (3'):

\[\sum_{|S|\ge K} f[S] = \sum_{|S|\ge K}\sum_{S\subseteq T} g[T]\cdot (-1)^{|T-S|} \\ =\sum_{|T|\ge K} g[T]\sum_{|S|\ge K, S\subseteq T} (-1)^{T-S} \\ = \sum_{|T|\ge K} g[T] \cdot A(|T|, K). \]

其中 \(A(n, k) = \sum_{i = k}^{n} \binom{n}{i}\cdot (-1)^{n-i}\).

(作为一个特殊情形,考虑 \(k=1\) 的时候,\(\sum_{i=1}^{n} (-1)^{n-i}{n\choose i} = (-1)^{n+1}\),此时得到奇偶错位求和)。

\[ANS = \int_{0}^{R} \Pr[\text{第 K 大 数} \ge x] dx \\ =\int_0^R \Pr[ \exists K 个数 \ge x ] dx\\ =\int_0^R (\sum_{|S|\ge K} f[S])dx \\ = \int_0^R (\sum_{|S|\ge K} g[S] \cdot c_{|S|})dx \]

其中 \(f[S]\) 表示 \(X_i \ge x\) 对任意 \(i\in S\) 成立,并且 \(X_i < x\) 对任意的 \(i\notin S\) 成立。

\(g[S]\) 表示 \(X_i\ge x\) 对任意 \(i\in S\) 成立。\(c_{|S|}\) 是一个容斥系数。(这个概率就是 \(\prod_{i\in S} \frac{R-x}{R}\),对 \(x\) 积分即可)。

\(L_i,R_i\) 不一定一样的时候:把 \([0, +\infty)\) 按照 \(L_i, R_i\) 分段,每一段用上述技巧计算多项式,求积分。(ABC:Random K-th Max)


BZOJ 已经没有什么好害怕的了

给定两个长度为 \(N\) 的数组 \(\{A_i\}\)\(\{B_i\}\),现在要把他们配对起来,使得恰好有 \(K\)\((A,B)\) 满足 \(A\ge B\),求方案数。\(N\le 2000\)

考虑一个暴力容斥:

枚举集合 \(A\) 里的 \(t\) 个数构成的集合 \(S\),要求他们必须匹配到比自己小的,其余的\(N-t\) 个数随意,求方案数。

暴力枚举完所有可能情况,可以算出 \(g[t]\):表示在集合A里钦定了 t 个数,保证它们的匹配对象比自己要小,其他随意的方案数。

\(g[t]\) 反演出 \(f[t]\):恰好有 t个A中的数,满足他们的匹配对象比自己小。答案就是 \(f[K]\).

优化:用一个DP来计算 \(g\)。令 \(dp[i][j]\) 表示在集合 A 的前 i 个数里,钦定了 j 个数的匹配对象比自身小,的方案数之和。

\[dp[i][j] \to dp[i + 1][j] \\ dp[i][j] \to \times ([比 A_{i+1} 小的数的个数] - j) \to dp[i + 1][j + 1]. \]

\(dp[N][t]\cdot (N-t)! = g[t]\)

运算量是 \(O(N^2)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
int a[2005],b[2005],loc[2005];
long long fac[2005],inv[2005];
long long dp[2005][2005],f[2005],g[2005];
const long long md=1e9+9;
inline void init(){
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i%md;
	for(int i=2;i<=n;i++)inv[i]=(md-md/i)*inv[md%i]%md;
	for(int i=2;i<=n;i++)inv[i]=inv[i]*inv[i-1]%md;
}
inline long long C(int x,int y){
	return fac[x]*inv[y]%md*inv[x-y]%md;
}
int main(){
	scanf("%d%d",&n,&k);
	init();
	if((n+k)&1){puts("0");return 0;}k=(n+k)>>1;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)scanf("%d",&b[i]);
	sort(a+1,a+n+1);
	sort(b+1,b+n+1);
	for(int i=1,j=0;i<=n;i++){
		while(j<n&&b[j+1]<a[i])j++;
		loc[i]=j;
	}
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=i;j++){
			if(j)dp[i][j]=(dp[i-1][j]+max(0,loc[i]-j+1)*dp[i-1][j-1])%md;
			else dp[i][j]=dp[i-1][j];
		}
	}
	for(int i=1;i<=n;i++)g[i]=dp[n][i]*fac[n-i]%md;
	long long ans=0;
	for(int i=k;i<=n;i++){
		ans=(ans+(((i-k)&1)?-1:1)*g[i]*C(i,k)%md)%md;
	}
	printf("%lld",(ans+md)%md);

	return 0;
}




Atcoder Beginner Contest-???

给定一个 \(1\sim N\) 的排列 \(P\)。问有多少 \(i\ne j\) 满足 \(gcd(i,j) > 1\)\(gcd(P_i, P_j) > 1\)\(N\le 2\times 10^5\)

简单问题:有多少 \((i,j)\) 满足 \(gcd(P_i, P_j) > 1\)(不考虑 \(gcd(i,j) > 1\) 的限制)。容斥(莫比乌斯反演)

  • 计算有多少 \((i, j)\) 满足 $2 | (P_i,P_j) $:加上。
  • 计算多少满足 \(3 | (P_i, P_j)\):加上。
  • 计算有多少 \(6|(P_i, P_j)\):减掉。
  • 。。。
  • 计算有多少 \(d|(P_i, P_j)\):产生 \(-\mu(d)\) 的贡献。

\[ANS = \sum_{d=1}^N \mu(d) \cdot \binom{d的倍数的个数}{2} \]

可以实现一个数据结构,支持插入一个数,删除一个数;询问集合里有多少个互质的pair。

  • 每次插入/删除一个数 \(x\) 的时候,枚举 \(x\) 的约数 \(d\),更新 \(d\) 的倍数的个数。同时更新 ANS。
  • 每次对数据结构的操作时间是\(2^{\omega(x)}\le \sqrt{N}\),其中 \(\omega(x)\)表示 x 不同的质因子的个数。

原题:现在带上了 i j 的限制。

  • 只考虑 \(2|(i, j)\) 的那些下标,计算一遍简单问题。产生 1 的贡献
  • 枚举 \(3 | (i, j)\) 的下标,计算一遍简单问题。产生 1 的贡献
  • 。。。
  • 枚举 \(d|(i,j)\) 的下标,计算一遍简单问题。产生 \(-\mu(d)\) 的贡献。

总共会对数据结构进行 \(O(N\log N)\) 次更新。

计算量\(O(N\sqrt{N}\log N)\).

posted @ 2021-12-14 14:06  一粒夸克  阅读(101)  评论(0编辑  收藏  举报