一类分割棋盘的 DP

1

HDU5180
给定一个 n*n 的棋盘,要在上面放 k 个车和任意多个王(车走直线,王走八连通),求有多少种方案使得车不能够吃王,车不能吃车,王不能吃王。

观察到放置一辆车以后这一行、这一列都不能放旗子。因此 K 辆车占领 K 行 K 列。于是我们不直接枚举车的位置,可以枚举车占领了哪些行列,然后王只能放在每个被划分成的小矩形内。因此我们可以预处理每个大小的矩形内随便放王的方案数,然后枚举车占领了哪些行,对列进行dp即可。

 #include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fir first
#define sec second
#define ld long double
using namespace std;
const int mod=1000000007;
typedef pair <int,int> P;
P trans[2000010];
int sta[100000],cnt,p,d[21];
ll fac[21],h[21],g[21][21];
int read()
{
	int x=0;char c=getchar(),flag='+';
	while(!isdigit(c)) flag=c,c=getchar();
	while(isdigit(c)) x=x*10+c-'0',c=getchar();
	return flag=='-'?-x:x;
}
inline void add(int &a,int b) {a+b>=mod?a+=b-mod:a+=b;}
int f[17][1600],sum[21][21];
int main()
{
	for(int i=0;i<(1<<15);i++)
	{
		if((i&(i<<1))||(i&(i>>1))) continue;
		sta[++cnt]=i;
	}
	for(int i=1;i<=cnt;i++)
	{
		for(int j=1;j<=cnt;j++)
		{
			int ss=sta[i],tt=sta[j];
			if((ss&tt)||(ss&(tt<<1))||(ss&(tt>>1))) continue;
			trans[++p]=P(i,j);
		}
	}
	for(int i=1;i<=15;i++)
	{
		memset(f,0,sizeof(f));
		f[0][1]=1;
		for(int j=1;j<=15;j++)
		{
			for(int k=1;k<=p;k++)
			{
				int ss=trans[k].fir,tt=trans[k].sec;
				if((sta[ss]>=(1<<i))||(sta[tt]>=(1<<i))) continue;
				add(f[j][tt],f[j-1][ss]);
				add(sum[i][j],f[j-1][ss]);
			}
		}
	}
	fac[0]=1;
	for(int i=1;i<=15;i++) fac[i]=fac[i-1]*i%mod;
	int T=read();
	while(T--)
	{
		ll ans=0;
		int n=read(),K=read();
		for(int s=0;s<(1<<n);s++)
		{
			if(__builtin_popcount(s)!=K) continue;
			int lst=0,e=0;
			for(int i=1;i<=n;i++)
			{
				if(s&(1<<i-1)) 
				{
					if(i-lst>1) d[++e]=i-lst-1;
					lst=i;
				}
			}
			if(lst<n) d[++e]=n-lst;
			h[0]=1;
			for(int i=1;i<=n;i++) 
			{
				h[i]=1;
				for(int j=1;j<=e;j++) h[i]=h[i]*sum[d[j]][i]%mod;
			}
			memset(g,0,sizeof(g));
			g[0][0]=1;
			for(int i=1;i<=n;i++)
			{
				for(int j=1;j<=i;j++)
				{
					for(int k=0;k<i;k++)
					{
						g[i][j]=(g[i][j]+g[k][j-1]*h[i-k-1])%mod;
					}
				}
				ans=(ans+g[i][K]*h[n-i])%mod;
			}
			ans=(ans+g[0][K]*h[n])%mod;
		}
		cout<<ans*fac[K]%mod<<'\n';
	}
	return 0;
}
/*by DT_Kang*/

2

51nod 1518
在这里插入图片描述
n , m ≤ 16 n,m\leq 16 n,m16

我们可以容斥。假设枚举了某个行、列的集合是不稳定的,那么也就把棋盘划分成了若干个区域,每个区域是独立的。我们同样预处理每个区域的方案数。但是枚举行、列的集合复杂度太高了。一个策略是枚举行的集合,对列进行 DP。预处理我们采用轮廓线 DP,转移复杂度 O ( 1 ) O(1) O(1),于是复杂度为 O ( n 2 2 n ) O(n^22^n) O(n22n)

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fir first
#define sec second
#define ld long double
using namespace std;
const int mod=1e9+7;
typedef pair <int,int> P;
int f[21][1<<18];
ll h[21],g[21],sum[21][21],d[21];
int read()
{
	int x=0;char c=getchar(),flag='+';
	while(!isdigit(c)) flag=c,c=getchar();
	while(isdigit(c)) x=x*10+c-'0',c=getchar();
	return flag=='-'?-x:x;
}
inline void add(int &a,int b) {a+b>=mod?a+=b-mod:a+=b;}
int main()
{
	for(int w=1;w<=16;w++)
	{
		int U=(1<<w)-1;
		memset(f[0],0,sizeof(f[0]));
		f[0][(1<<w)-1]=1;
		int now=0;
		for(int i=1;i<=16;i++)
		{
			for(int j=1;j<=w;j++,now^=1)
			{
				memset(f[now^1],0,sizeof(f[now]));
				for(int s=0;s<(1<<w);s++)	//轮廓线DP,枚举当前格子向上放还是向左放
				{
					if(!f[now][s]) continue;
					if(!(s&(1<<w-1))) add(f[now^1][(s<<1|1)&U],f[now][s]);
					else 
					{
						if(!(s&1)&&j>1) add(f[now^1][(s<<1|3)&U],f[now][s]);
						add(f[now^1][(s<<1)&U],f[now][s]);
					}
				}
			}
			sum[i][w]=f[now][(1<<w)-1];
		}
	}
	int n,m;
	while(scanf("%d%d",&n,&m)!=EOF)
	{
		ll ans=0;
		for(int s=0;s<(1<<m-1);s++)
		{
			int lst=0,e=0;
			for(int i=1;i<m;i++) if(s&(1<<i-1)) d[++e]=i-lst,lst=i;
			d[++e]=m-lst;
			for(int i=1;i<=n;i++)
			{
				h[i]=1;
				for(int j=1;j<=e;j++) h[i]=h[i]*sum[d[j]][i]%mod;
			}
			g[0]=1;ll tmp=0;
			for(int i=1;i<n;i++)
			{
				g[i]=0;
				for(int j=0;j<i;j++) g[i]=(g[i]-g[j]*h[i-j])%mod;
				tmp=(tmp+g[i]*h[n-i])%mod;
			}
			tmp=(tmp+g[0]*h[n])%mod;
			if(__builtin_popcount(s)&1) ans=(ans-tmp)%mod;
			else ans=(ans+tmp)%mod;
		}
		cout<<(ans%mod+mod)%mod<<'\n';
	}
	return 0;
}
/*by DT_Kang*/

小结

这两道题有一个共同点:都有着“分界线”一样的东西把棋盘划分成若干部分,而且每个部分是独立的,因此我们可以预处理每个部分的信息,然后对分界线进行处理。

还有就是,对于行和列的限制,如果我们要容斥,一种策略是枚举行的集合,对列进行 DP。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值