容斥原理的思想大家都应该挺熟悉的,然后补集转化其实就是容斥原理的一种应用。
一篇讲容斥的博文https://www.cnblogs.com/gzy-cjoier/p/9686787.html
当我们遇到正面解决很困难的问题,我们可以考虑从它的反面去思考,如果反面容易计算的话那么我们就可以用补集转化的思想先计算反面再计算正面(尤其是计数类问题)。
Min-Max容斥是一个十分有用的定理,尤其是在计算概率期望上有
一般来说:这里的Emax(S)是代表出现S所有元素的期望,Emin(T)是出现T任何一个元素的期望。
这里直接总结几道题目:
洛谷P3349 小星星
题意:给出n个点m条边的图,再给n个点的树。问有多少种方案把树映射到图上,且树上对应边图上也有。
解法:这道题解法十分巧妙地运用了容斥原理。
一个比较容易[想到的暴力解法是设计dp[i][j][S]代表把树上i点映射到图上j点后的图上点使用情况是S。那么转移也比较简单dp[x][i][S]+=dp[y][j][S’] (y是x树上的儿子,ij两点在图上有边,S-S’=i)。但是此题的n达到17,这个解法严重超时。
我们想办法在上一个办法优化,思考这个暴力办法超时的主要原因是加入了S这个状态,假如我们去掉S这个纬度,那么点的使用情况就无法得到掌握就会出现图上某个点被重复映射的不合法情况,同时去掉的好处是dp方程时间很好写且时间复杂度大大降低:dp[x][i]+=dp[y][j] (y是x树儿子,ij两点在图上有边)。于是我们想办法去掉重复映射这种情况,怎么做呢?洛谷题解上大佬提供方法十分巧妙:既然出现重复映射就是图上有某些点没用到,那么我们运用容斥原理,恰好用了n个点方案=不加限制的全集-不加限制的用n-1点方案+不加限制用n-2个点方案……..这样一直下去。
#include<bits/stdc++.h> using namespace std; const int N=20; typedef long long LL; int n,m,mp[N][N]; vector<int> G[N]; LL dp[N][N]; //dp[i][j]表示点i映射到j的方案数(可重复映射) bool vis[N],ban[N]; //是否已经映射/点是否是禁止映射的 void dfs(int x) { vis[x]=1; for (int i=1;i<=n;i++) dp[x][i]=1; for (int i=0;i<G[x].size();i++) { int y=G[x][i]; if (vis[y]) continue; dfs(y); for (int j=1;j<=n;j++) { LL tmp=0; for (int k=1;k<=n;k++) tmp+=dp[y][k]*(!ban[j] && !ban[k] &&mp[j][k]); dp[x][j]*=tmp; } } } int main() { cin>>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); G[x].push_back(y); //树 G[y].push_back(x); } int ALL=(1<<n)-1; long long ans=0; for (int i=0;i<=ALL;i++) { int cnt=0; for (int j=1;j<=n;j++) vis[j]=ban[j]=0; for (int j=1;j<=n;j++) if ((1<<j-1)&i) cnt++,ban[j]=1; dfs(1); long long tmp=0; for (int j=1;j<=n;j++) tmp+=dp[1][j]; ans+=(cnt%2 ? -1 : 1)*tmp; } cout<<ans<<endl; return 0; }
View Code
洛谷P3175 按位或
题意:刚开始你有一个数字0,每一秒钟你会随机选择一个[0,2^n-1]的数字与之做或操作。选择数字i的概率是p[i]。保证0<=p[i]<=1,Σp[i]=1问期望多少秒后,你手上的数字变成2^n-1。
解法:这道题学了Min-Max容斥和FWT之后会变得一些简单。解法是参考https://blog.csdn.net/qq_30974369/article/details/81911124 yyb巨佬的。
这题看到比较容易想到Min-Max容斥,是计算Emax(2^n-1)这个东西并不好算,我们考虑用Min-Max容斥转化为计算Emin(T€2^n-1)。
怎么计算Emin呢?然后根据离散随机变量的期望公式 E(x)=1/p 得到
那么问题变成怎么计算sigma(p[G]) (GΛT≠ø),即所有和T有交集的几个G的概率和,其实这个东西也很难算,我们只好再次利用补集转化思想:所有有交集的G=1-所有没有交集的L(且没有交集的L=T的补集的所有子集)。
于是我们就想办法预处理出元素数据所有的子集的概率和,这个可以用FWT计算得到。
#include<bits/stdc++.h> using namespace std; const int M=1<<20; const double eps=1e-10; int n,cnt[M],N; double P[M],ans; void prework() { for(int i=1;i<N;i<<=1) for(int p=i<<1,j=0;j<N;j+=p) for(int k=0;k<i;++k) P[i+j+k]+=P[j+k]; } int main() { scanf("%d",&n);N=1<<n; for(int i=0;i<N;++i)scanf("%lf",&P[i]),cnt[i]=cnt[i>>1]+(i&1); prework(); for(int i=1;i<N;++i) if(1-P[(N-1)^i]>1e-8) ans+=((cnt[i]&1)?1:-1)/(1-P[(N-1)^i]); if(ans<eps)puts("INF");else printf("%.10lf ",ans); return 0; }
View Code
BZOJ 2169 连边
题意:有N个点(编号1到N)组成的无向图,已经为你连了M条边。请你再连K条边,使得所有的点的度数都是偶数。求有多少种连的方法。要求你连的K条边中不能有重边,但和已经连好的边可以重。
解法:日常不会做,解法参考https://www.cnblogs.com/NaVi-Awson/p/7581516.html这位大佬的,写得非常好。
dp[i][j]代表添加i条边之后剩下j奇点的方案数。那么写状态转移方程
dp[i][j]+=dp[i-1][j+2] * (C(j+2,2)) 代表从j+2个奇点中选2个连一条边
dp[i][j]+=dp[i-1][j-2] * (C(n-j+2,2)) 代表从n-j+2个非奇点中选两个连一条边
dp[i][j]+=dp[i-1][j] * ((j)*(n-j)) 代表从j个奇点和n-j个偶点中各选一个连边
但是到目前为止我们还没考虑去掉题目中禁止的重边问题。这里要用到容斥原理减去。
先说结果:dp[i][j]-=dp[i-2][j] * (C(n,2)-(i-2)) ,解释一下:因为n个点完全图上有n*(n-1)/2条边,dp[i-2][j]代表的是已经选的前i-2条边是没有重复的方案。那么到了i条边我们就得考虑减去第i条边与之前某一条边重复了那么第i条边有多少种选择(C(n,2)-(i-2)),那么重复的方案数就是 dp[i-2][j]*(C(n,2)-(i-2)) 。
对于这个重复数可能还是会有些疑惑:例如为什么可选边是C(n,2)-(i-2),因为其实通过开始的三条方程计算得到的结果会包含n点完全图的任一条边且此时选的第i条边会与每一条边重合,换句话来说就是只计算完前3条方程时候第i条边的方案数是不加任何限制能任意重合的,那么我们也就要减去第i条边的任意必重合方案。
综合上面4条就得到状态转移方程了,但是要注意上面的方程是带有顺序的,但是题目要求方案数是没有顺序的,所有dp[i][j]/=i。
HDU-5072 补集转化+容斥原理
题意:抽象起来核心问题就是在完全图上求同色/异色三角形数量。
解法:这道题很有意思值得一做,解法我曾经写过https://www.cnblogs.com/clno1/p/11490374.html。
#include<bits/stdc++.h> using namespace std; const int N=1e5+10; int n,m,a[N],mul[N],e[N]; vector<int> fac[N]; void prework() { //预处理1-100000的因子 for (int i=1;i<=100000;i++) { int n=i; for (int j=2;j*j<=n;j++) { if (n%j==0) { fac[i].push_back(j); while (n%j==0) n/=j; } } if (n>1) fac[i].push_back(n); } } int main() { prework(); int T; cin>>T; while (T--) { scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&a[i]); memset(mul,0,sizeof(mul)); memset(e,0,sizeof(e)); for (int i=1;i<=n;i++) { int ALL=1<<fac[a[i]].size(); for (int j=0;j<ALL;j++) { int sum=1; for (int k=0;k<fac[a[i]].size();k++) if (j&(1<<k)) sum=sum*fac[a[i]][k]; mul[sum]++; //代表是sum倍数的a[i]的个数++ } } for (int i=1;i<=n;i++) { int ALL=1<<fac[a[i]].size(); for (int j=1;j<ALL;j++) { int sum=1,sig=-1; for (int k=0;k<fac[a[i]].size();k++) if (j&(1<<k)) sum=sum*fac[a[i]][k],sig*=-1; e[i]+=sig*mul[sum]; //容斥原理求与a[i]不互质的数个数(包括自己) } e[i]=n-e[i]; //补集就是与a[i]互质的数个数(不包括自己) if (a[i]==1) e[i]=n-1; } long long ans=1,tmp=0; for (int i=n;i>n-3;i--) ans=ans*i; ans=ans/6; //计算全集C(n,3) for (int i=1;i<=n;i++) tmp+=(long long)(e[i])*(n-e[i]-1); //计算异色三角形数量 printf("%lld ",ans-tmp/2); } return 0; }
View Code