hdu3811 Permutation

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3811

题意:给定1~N个数,求出至少满足一个条件的排列总数

m个条件如下:Ai 个数为Bi (0<i <m)

分析:“本意是一个复杂的容斥原理,但是开场不久被秒杀,才发现又一个比较简单的状态DP方法” 这是武大预赛解题报告上的原句。

确实,用容斥原理来解决确实十分蛋疼,最直接就是满足各个条件之后去重,好SB的样子==!。可是用状态DP却很好解决了。

我们通过求出一个条件都不满足的排列总数,从而间接的求出满足至少一个条件的排列总数。

对于当前的位置,利用前面已经求得的种数求得。

结合代码比较好解释,看代码吧 。

#include<iostream>
#include<algorithm>
using namespace std;
__int64 fact[18],dp[1<<18];
int g[18][18];
void init()
{
	fact[0]=1;
	for(int i=1;i<=17;i++)
		fact[i]=i*fact[i-1];
}
int main()
{
	int T,cas=0,n,m,a,b;
	init();
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d %d",&n,&m);
		memset(g,0,sizeof(g));
		for(int i=0;i<m;i++)
		{
			scanf("%d %d",&a,&b);
			a--;b--;
			g[a][b]=1;//标记一下,a位置不能选择b
		}
		memset(dp,0,sizeof(dp));
		dp[0]=1;//初始化为1,
		for(int i=0;i<n;i++)//枚举每一个长度
			for(int j=(1<<n)-1;j>=0;j--)//枚举每一个状态,利用位运算,j的每一个位表示该数字是否已被用
			{//这里注意j的枚举顺序,不能颠倒,想想为什么?
				//其实从小到大枚举的话,会改掉之前所保存的值
				if(dp[j]==0) continue;
				for(int k=0;k<n;k++)//枚举当前位置的所有可选择的数字
				{
					if((j&(1<<k))!=0)//j 状态中已有了k这个数字
						continue;
					if(g[i][k]==1)//i位置不能选择k
						continue;
					dp[j|(1<<k)]+=dp[j];//j状态变成j|(1<<k)]状态产生的排列数,j|(1<<k)]状态是j状态第i+1个位置选择了k之后的状态
                                                                  //状态转移,扩展序列的长度
				}
			}
		//fact[n]表示n!,即n个数的全排列,减掉不满足条件的排列数,即为所求。
		//dp[(1<<n)-1]表示不满足条件的排列总数,其中(1<<n)-1  对应的二进制每一个的前N个位均为,即该状态下n个数字都已经选择
		printf("Case %d: %I64d\n",++cas,fact[n]-dp[(1<<n)-1]);
	}
	return 0;
}
原文地址:https://www.cnblogs.com/nanke/p/2225912.html