bzoj 2669: [cqoi2012]局部极小值(dp+容斥原理)

2669: [cqoi2012]局部极小值

Time Limit: 3 Sec   Memory Limit: 128 MB
Submit: 537   Solved: 280
[ Submit][ Status][ Discuss]

Description

有一个nm列的整数矩阵,其中1到nm之间的每个整数恰好出现一次。如果一个格子比所有相邻格子(相邻是指有公共边或公共顶点)都小,我们说这个格子是局部极小值。
给出所有局部极小值的位置,你的任务是判断有多少个可能的矩阵。

Input

输入第一行包含两个整数nm(1<=n<=4, 1<=m<=7),即行数和列数。以下n行每行m个字符,其中“X”表示局部极小值,“.”表示非局部极小值。
 

Output

输出仅一行,为可能的矩阵总数除以12345678的余数。

Sample Input

3 2
X.
..
.X

Sample Output

60

HINT

Source

[ Submit][ Status][ Discuss]


题解:dp +容斥原理。

这道题的数据范围很小。我们很容易想到状压动归,但是如果我们枚举每一个位置放置什么就一定会存在判断当前数是否已经用过的问题,2^28很显然不能承受。

我们发现局部极小值的点最多8个,所以我们考虑把局部最小值是否已经填过状压成一维,然后考虑从小到大往格子中填数,因为每个数枚举一次所以也就只能填一次。

f[i][j]表示填到数i,局部最小值的填写状态为j。

i数可以填写到的位置可以分成两种,‘x'位置,和非’x'位置。

如果考虑填入‘x'位置,因为是从小到大开始填写,所以直接填就好,因为周围的点要么没填要么填的比这个位置小。我们只需要令j状态中填写的位置k,f[i][j]+=f[i-1][j^(1<<k-1)]

如果考虑填入非‘x’位置,很显然有些位置是不能填的(就是那些未填的'x'位置和他周围的位置),我们可以预处理出每种状态对应的可以填写的位置的个数(即除去那些未填的'x'位置和他周围的位置剩下的位置),但是这些位置中已经填写了(i-1)个数,

所以在递推的时候要减去,即f[i][j]+=f[i-1][j]*max(num[j]-i+1,0)。

但是我们这样算完后发现答案还是不对,为什么呢?因为我们是保证了'x'位置是局部极小值,但是并没有保证'.'的位置不是,局部最小值,所以Ans=多包含至少0个局部极小值-多包含至少1个局部极小值+多包含至少2个局部极小值-...

这个容斥的时候用dfs搜索出那些不是'x'但是有可能成为局部极小值的位置,然后用上面的方式计算一下,在更新最终答案即可。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define N 30
#define p 12345678
using namespace std;
int f[N][1<<8],n,m,ans;
int x[N],y[N],num[1<<8],tot,vis[N][N];
int posx[10]={-1,-1,-1,0,0,1,1,1},posy[10]={-1,0,1,-1,1,-1,0,1};
char s[N][N];
int solve()
{
	tot=0;
	for (int i=1;i<=n;i++)
	 for (int j=1;j<=m;j++)
	  if (s[i][j]=='X')
	   x[++tot]=i,y[tot]=j;
	for (int i=0;i<(1<<tot);i++)
	 {
	 	int cnt=0;
	 	memset(vis,0,sizeof(vis));
		for (int j=1;j<=tot;j++)
	 	 if (!((i>>(j-1))&1))
	 	  {
	 	  	vis[x[j]][y[j]]=1;
	 	  	for (int k=0;k<8;k++)
	 	  	 {
	 	  	 	 int xx=x[j]+posx[k];
	 	  	 	 int yy=y[j]+posy[k];
	 	  	 	 if (xx>0&&yy>0&&xx<=n&&yy<=m)
	 	  	 	  vis[xx][yy]=1;
			 }
		  }
		for (int j=1;j<=n;j++)
		 for (int k=1;k<=m;k++) 
		  if (vis[j][k])  cnt++;
		num[i]=n*m-cnt;
	 }
	memset(f,0,sizeof(f));
	f[0][0]=1;
	for (int i=1;i<=n*m;i++)
	 for (int j=0;j<(1<<tot);j++)
	  {
	  	 f[i][j]=(f[i][j]+f[i-1][j]*max(num[j]-i+1,0))%p;
	  	 for (int k=1;k<=tot;k++)
	  	  if (j&(1<<(k-1)))
	  	   f[i][j]=(f[i][j]+f[i-1][j^(1<<k-1)])%p;
	  }
	return f[n*m][(1<<tot)-1];
}
void dfs(int x,int y,int k)
{
	if (y==m+1)
	 {
	 	dfs(x+1,1,k);
	 	return;
	 }
	if (x==n+1)
	 {
	 	if (k%2==0)  ans=(ans+solve())%p;
	 	else ans-=solve()%p;
	 	ans=(ans%p+p)%p;
	 	return;
	 }
	dfs(x,y+1,k);
	bool f=true;
	for (int i=0;i<8;i++)
	 if (s[x+posx[i]][y+posy[i]]=='X')
	  {
	  	f=false;
	  	break;
	  }
	if (f&&s[x][y]!='X'){
	    s[x][y]='X';
	    dfs(x,y+1,k+1);
	    s[x][y]='.';
	}
}
int main()
{
	freopen("a.in","r",stdin);
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
    { 
    	scanf("%s",s[i]+1);
    	for(int j=1;j<=m;j++)
    	 if (s[i][j]=='X')
    	  x[++tot]=i,y[tot]=j;
	}
	dfs(1,1,0);
	printf("%d\n",(ans%p+p)%p);
}



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值