JZOJ.5230.队伍统计[状压dp]

Description

现在有n个人要排成一列,编号为1->n 。但由于一些不明原因的关系,人与人之间可能存在一些矛盾关系,具体有m条矛盾关系(u,v),表示编号为u的人想要排在编号为v的人前面。要使得队伍和谐,最多不能违背k条矛盾关系(即不能有超过k条矛盾关系(u,v),满足最后v排在了u前面)。问有多少合法的排列。答案对10^9+7取模。

--------------------------------------

有1~n个小学生排队抢宵夜,由于一些f**k原因,m条矛盾关系(u,v),表示编号为u的小学生想要排在编号为v的人前面否则打架,不能使k队小学生打架,求合法序列的方案数mod 1e9+7

input&output

第一行包括三个整数n,m,p。
接下来m行,每行两个整数u,v,描述一个矛盾关系(u,v)。
保证不存在两对矛盾关系(u,v),(x,y),使得u=x且v=y 。

输出包括一行表示合法的排列数。

数据范围&提示

对于30%的数据,n<=10
对于60%的数据,n<=15
对应100%的数据,n,k<=20,m<=n*(n-1),保证矛盾关系不重复。

样例

in

10 12 3
2 6
6 10
1 7
4 1
6 1
2 4
7 6
1 4
10 4
10 9
5 9
8 10

out

123120

solution

30%
暴力dfs,全排列然后计算方案数
60%
暴力加剪枝
100%
状压dp,即状态压缩动态规划
FAQ:
从何看出?

对应100%的数据,n,k<=20,m<=n*(n-1),保证矛盾关系不重复。

所以我们可以通过枚举 2 n 2^n 2n 次来压缩1~n个小学生选还是不选的情况
通俗地讲,设 1 ≤ i ≤ 2 n 1\leq i\leq 2^n 1i2n ,那么i转换二进制后就是
10011010100100…
1表示选,0表示不选,还是不会就->_->位运算——百度百科
所以设 F i , j F_{i,j} Fi,j i i i这个状态下,已经有 j j j对小学生打架的方案数总和
那么就有 f[i][j] 转移到f[i or 2 k − 1 2^{k-1} 2k1][j+tot](前提是k这个人并不在i这个状态内并且j+tot ≤ \leq p)
k表示这时候如果加进来一个人k,tot表示加进来k后会多几个小学生打架
那么就有转移方程
在这里插入图片描述
dp完了
答案就是
在这里插入图片描述
上代码
tips:in C++,__builtin_popcount(x)可快速计算x的二进制下有多少个1
有关C++__builtin_popcount(x)的用法戳
兄dei,不是c++的快转吧,**s*al会错的

#include <cstdio>
#define ll long long
#define cnt(x) __builtin_popcount(x)//定义cnt(x)等价于__builtin_popcount(x)
#define mod (int)(1e9+7)
#define open(x) freopen(x".in","r",stdin);freopen(x".out","w",stdout);
using namespace std;
ll n,m,K,f[1<<20][30],g[1<<20][30],fal[50],cnt[1<<20],ans=0,max=0;
int main()
{
	//open("count");
	freopen("data4.in","r",stdin);
	scanf("%lld%lld%lld",&n,&m,&K);
	for(int i=1;i<=m;++i)
	{
		ll x,y;scanf("%lld%lld",&x,&y);
		fal[x]|=(1<<(y-1));//和i同样的方法,把和u号小学生打架的人压缩成状态存进fal[u]
	}
	f[0][0]=1;max=(1<<n);
	for(int i=0;i<max;++i)
		for(int j=0;j<=K;++j)
			if(f[i][j])
			for(int k=1;k<=n;++k)
			{
				ll sum=(1<<k-1);
				if((i&sum)==0)
				{
					ll tot=cnt(fal[k]&i);//fal[k] and i表示状态i中有多少个和k打架的小学生
					//cnt函数快速统计and后有多少个1
					if(j+tot<=K)(f[i|sum][j+tot]+=f[i][j])%=mod;
				}
			}
	for(int i=0;i<=K;++i)(ans+=f[max-1][i])%=mod;
	printf("%lld",ans);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值