容斥

容斥


本质的思想是,给集合里的元素标正负号,使得累加起来刚好抵消,得到所要的信息

特征:贡献大小可以抵消


例题

\[令被5,6,8整除的集合分别为A_1,A_2,A_3\\ 则有ans = S - |A_1\cup A_2\cup A_3|\\ -|A_1\cup A_2\cup A_3| = \sum_{S}-1^{|S|}\cap_{i\in S}A_i\\ 直接回带计算即可 \]

CF1060F

先考虑暴力怎么做,即确定一个合法的缩边顺序,然后再计算每个点上现在编号的出现次数

考虑怎么快速处理上述过程

钦定一个点\(RT\)是最后的点

即考虑子树的贡献怎么转移到父亲

需要处理,以某个子树为根,然后最后成为子树内某个编号的次数

然后在每一个多叉节点哪里转移一下

貌似就可以了

但多叉节点转移比较恶心


另一思考方向

进行一步转化:

初始树上没有边,每个节点权值为1,有一个长度\((n - 1)!\)的边集序列,依次加入这些边,每次加入一条边,让所连接的两个连通块的 权值 乘上 \(\frac{1}{2}\),显然有: 最后每个点身上的权值,就是在这个边集顺序下,留下来的概率

考虑如何解决

前置题目:[CTS2019] 氪金手游

考虑一条链

\[1----n\\ P(1->n) = \prod_{i = 1}^n\frac{W_i}{\sum_{j = i}^nW_j}\\ 存在一个反向边\\ 1------>k<--------(k + 1)----->n\\ P = P(1->K) \times p(K + 1->n) - p(1->n)\\ \]

有k个反向边,就是K个容斥直接\(O(n^3)暴力算好了\)

#include<bits/stdc++.h>
#define MAXN 1005
typedef long long ll;
const ll mod = 998244353;
using namespace std;

int n;
ll a[MAXN][4];
int h[MAXN],tot;
struct node{int from,to,next;ll tp;}e[MAXN << 1];
void add(int x , int y , int z){
	tot++;
	e[tot].from = x;
	e[tot].to = y;
	e[tot].tp = z;
	e[tot].next = h[x];
	h[x] = tot;
}

ll poww(ll x , int y){
	ll zz = 1;
	while(y){
		if(y & 1)zz = zz * x % mod;
		x = x * x % mod;
		y = y >> 1;
	}
	return zz;
}

int sz[MAXN];
ll f[MAXN][MAXN * 5],g[MAXN * 5];

void dfs(int now , int fa){
	f[now][0] = 1;
	for(int i = h[now] ; i != (-1) ; i = e[i].next){
		if(e[i].to == fa)continue;
		dfs(e[i].to , now);
		memset(g , 0 , sizeof(g));
		for(int j = 0 ; j <= sz[now] ; j++){
			for(int k = 0 ; k <= sz[e[i].to] ; k++){
				g[j + k] = (g[j + k] + f[now][j] * f[e[i].to][k] % mod * e[i].tp % mod) % mod;
				if(e[i].tp != 1)g[j] = (g[j] + f[now][j] * f[e[i].to][k] % mod) % mod;	
			}
		}
		memcpy(f[now] , g , sizeof(g));
		sz[now] += sz[e[i].to];
	}
	memset(g , 0 , sizeof(g));
	for(int j = 0 ; j <= sz[now] ; j++){
		for(int k = 1 ; k <= 3 ; k++){
			g[j + k] = (g[j + k] + f[now][j] * k % mod * poww(j + k , mod - 2) % mod * a[now][k] % mod) % mod;
		}
	}
	memcpy(f[now] , g , sizeof(g));
	sz[now] += 3;
}

int main(){
	memset(h , -1 , sizeof(h)) , tot = 0;
	scanf("%d" , &n);int x,y;
	for(int i = 1 ; i <= n ; i++){
		ll sum = 0;
		for(int j = 1 ; j <= 3 ; j++)scanf("%lld" , &a[i][j]) , sum += a[i][j];	
		sum = poww(sum , mod - 2);
		for(int j = 1 ; j <= 3 ; j++)a[i][j] = a[i][j] * sum % mod;	
	}
	for(int i = 1 ; i < n ; i++){
		scanf("%d%d" , &x , &y);
		add(x , y , 1);
		add(y , x , mod - 1);
	}
	dfs(1 , 1);
	ll ans = 0;
	for(int i = 1 ; i <= 3 * n ; i++)ans = (ans + f[1][i]) % mod;
	cout<<ans<<endl;
} 

回到上一题

本质的思想是:首先钦定\(RT\),然后考虑加边的顺序

考虑加入\((RT,v)\)\(S为v所在的连通块\) ,那么现在 两个端点都在\(S\)里面的边,一定先选,

如果一个存在一个端点不在\(S\)里面,一定后选,且对答案产生\(\frac{1}{2}\)的贡献,并且可以继续递归

通过这样的一系列操作

可以得到边与边之间的偏序关系,并且可以DP

划定主动边与被动边

考虑\((u,v)的加入需要在(p,q)加入之后加入\),则连边\(( (u,v) ,(p,q) )\)

主动边与被动边连边

主动边与最近的主动边连接

即满足拓扑序

但这个玩意做出来是一个树形图

需要通过前面那个题的思想,容斥为一个内向森林

然后直接dp就好了

#include<bits/stdc++.h>
#define MAXN 65
typedef long double ll;
using namespace std;

int n,h[MAXN],tot;
struct node{int from,to,next;}e[MAXN << 1];
void add(int x , int y){
	tot++;
	e[tot].from = x;
	e[tot].to = y;
	e[tot].next = h[x];
	h[x] = tot;
}

int sz[MAXN];
ll f[MAXN][MAXN],g[MAXN];

void dfs(int now , int fa){
	sz[now] = f[now][0] = 1;
	for(int i = h[now] ; i != (-1) ; i = e[i].next){
		if(e[i].to == fa)continue;
		dfs(e[i].to , now);
		memset(g , 0 , sizeof(g));
		for(int j = 0 ; j < sz[now] ; j++){
			for(int k = 0 ; k < sz[e[i].to] ; k++){
				g[j + k] = g[j + k] + f[now][j] * f[e[i].to][k] * (0.5 / (1.0 * (sz[e[i].to] - k)) - 1.0);
				g[j + k + 1] += f[now][j] * f[e[i].to][k];
			}
		}
		memcpy(f[now] , g , sizeof(g)) , sz[now] += sz[e[i].to];
	}
}

int main(){
	memset(h , -1 , sizeof(h)) , tot = 0;
	scanf("%d" , &n);int x,y;
	for(int i = 1 ; i < n ; i++)scanf("%d%d" , &x , &y) , add(x , y) , add(y , x);
	for(int i = 1 ; i <= n ; i++){
		memset(f , 0 , sizeof(f));
		dfs(i , i);
		ll ans = 1;
		for(int j = h[i] ; j != (-1) ; j = e[j].next){
			ll zz = 0;
			for(int k = 0 ; k < sz[e[j].to] ; k++){
				zz = zz + f[e[j].to][k] * 0.5 / (1.0 * (sz[e[j].to] - k));
			}
			ans *= zz;
		}
		cout<<setprecision(10)<<ans<<endl;
	}
}

AGC005D

考虑容斥

\[ans = \sum_{i = 0}^n g_i(-1)^i(n - i)!\\ g_i表示至少i个位置不合法 \]

之后直接dp做一下就好了(把二分图拉出来形成一条链)

或者

这种排列计数与 一张图构成双射,直接在这张图上计数就好了

可以参考这个


AGC035F

考虑结果相同的情况

\(r_i = j,c_j = i - 1 \\ r_i = j - 1 , c_j = i\)

只有这两种情况相同

直接容斥即可

\[f(k) = \binom{n}{k}\binom{m}{k}(n + 1)^{m-k}(m + 1)^{n-k}k!\\ ans = \sum_{i = 0}(-1)^if(i) \]


[THUWC2017]随机二分图

\[f[S1][S2]左右两边的匹配情况\\ 直接dp,建边特殊规化一下就好了 \]


典中典了属于是

[清华集训2012]串珠子


AGC036F

可以发现,每一个\(P_i\)都有一个合法上下界

并且合法上下界单减

考虑如何dp出\(f_k,至少k个位置不合法\)

之后的操作就比较高妙了

\[考虑i\in[0,N - 1] , lb_i = \sqrt{n^2-i^2} , rb_i = \sqrt{(2n) ^2-i^2}\\ i\in[N+1,2n - 1],lb_i = 0 , rb_i =\sqrt{(2n) ^2-i^2}\\ \]

考虑\(lb_i\)全部为0,还是比较好做的

考虑容斥吧,强制至少有k个数满足\(p_i < lb_i\)

之后直接暴力dp一下就好了


先把所有\(R_i\)减去\(L_i\)

考虑对下界进行容斥,即每一个 \(A_i ? R_i-L_i\)

之间的关系

记当前的一个容斥下界为\(S\)

则一个值\(V\)的拼凑方案为

\[\binom{n - 1 + V - S}{n - 1} \]

\[考虑V->V + aD^b\\ \binom{n - 1 + V + aD^b - S}{n - 1} = \sum_{i = 0}^{n - 1} \binom{n-1+V-S}{i}\binom{aD^b}{n-1-i}\\ 直接转移就好了 \]

此时考虑不同的\(V\)可以一起转移,只要考虑\(V + aD^b\)在下界之外就好了

直接进行数位dp


有一个比较劣的做法

还是先把\(R_i减去L_i\)

\[f[i][S][j]从高到低第i为,与每个界R的关系为S,需要往前面补多少个数\\ 直接就有了O(3^nn^2d^2)的dp\\ 也可以使用高维前缀和优化到O(d^2 2^n n ^4) \]

posted @ 2022-03-29 15:26  After_rain  阅读(153)  评论(0)    收藏  举报