【洛谷P3160】[CQOI2012]局部极小值

前言

【题目传送门】
好难的题......

心路历程:

  • 干想没思路。
  • 疯狂翻题解。
  • 看代码太长,想放弃。
  • 又整理了下思路,发现核心其实没有那么复杂。
  • 依据一片题解的代码开始写。
  • 写完之后调到 \(90pts\)
  • 终于调过。

实现细节巨多,根本注意不到。
用时:\(3h\)

题解

DP 的基础思路

说实话丢人的我连朴素 DP 都没设计出来,或者说不知道怎么转移
看到数据范围,大概能想到状压 DP。
问题来了,压什么?
仔细想想,和题目有关而且能压的,也就是最小点了(最多只有 \(8\) 个)。
所以一维表示最小点的选择状态。
那么为了转移,自然地再加一维表示选了几个数字,正好符合空间,不错。
接下来确定转移顺序
假设从小往大填数字,那么填某一个最小点之前,它相邻的点都一定不能被填。
据此,可以知道每种状态下有哪些点能选(其实需要的是知道有多少个点能选)。
转移就可以分成两种情况:放在最小点/不放在最小点。

进一步的思考+容斥

但是这求出的答案是什么?
实际上,我们保证了每一个X的位置一定是局部最小值,但是没有保证每一个.的位置一定不是局部最小值。
所以要减去放错一个高点的情况。
然而这样又会减多,例如同时放错两个高点的情况又被多减了一次。
这就是一个容斥问题了。

关于容斥,其实我也没有系统学过,我的理解大概是发现减去一部分,会多减去重叠的部分,根据数学归纳每一层都是这样“加一层,减一层”的形式,这样推导到最底层即可。(其实有个公式的)
(PS:简单的容斥还可以用位运算实现,不过和本题无关啦)

可以用 DFS 枚举每一个能够更改成X.,重新跑 DP 计算答案,根据容斥进行增减。(这个 DFS 也挺恶心人的)

一点细节

如果一开始就不符合条件,可以直接特判掉。

代码(保姆级注释)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f,N = 1<<8,mod = 12345678;
char s[10][10];
int n,m,px[31],py[31];
ll dp[N][31],ans;
bool vis[10][10];
int dx[9] = {0,1,0,-1,0,1,1,-1,-1};
int dy[9] = {0,0,1,0,-1,1,-1,1,-1};
inline ll read()
{
	ll ret=0;char ch=' ',c=getchar();
	while(!(c>='0'&&c<='9')) ch=c,c=getchar();
	while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
	return ch=='-'?-ret:ret;
}
void print_map()
{
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++) printf("%c",s[i][j]);
		printf("\n");
	}
}
int calc()//按照当前状态算出方案数 
{
	int cnt=0; 
	for(int i=1;i<=n;i++)	
		for(int j=1;j<=m;j++)	
			if(s[i][j]=='X') px[++cnt]=i,py[cnt]=j;
	//print_map();
	memset(dp,0,sizeof(dp));
	dp[0][0]=1;
	for(int mask=0;mask<(1<<cnt);mask++)//枚举状态 
	{
		memset(vis,0,sizeof(vis));
		for(int i=1;i<=cnt;i++)//标记当前状态下哪些点不能选
			if(!(mask&(1<<(i-1))))//每个最小点周围的点都不能选 
				for(int j=0;j<=8;j++)
					vis[px[i]+dx[j]][py[i]+dy[j]]=1;
		int tot=0;
		for(int i=1;i<=n;i++)	 
			for(int j=1;j<=m;j++) tot+=!vis[i][j];//计算有多少个能选的点 
		//printf("tot=%d\n",tot);
		for(int i=0;i<=tot;i++)	//刷表法,i从0开始 
			if(dp[mask][i])
			{
				(dp[mask][i+1]+=dp[mask][i]*(tot-i))%=mod;//当前数字不放在最小点 
				for(int j=0;j<cnt;j++)	
					if(!(mask&(1<<j))) 
						(dp[mask|(1<<j)][i+1]+=dp[mask][i])%=mod;//当前数字放在最小点 
			}
	}
	//printf("dp:%d\n",dp[(1<<cnt)-1][n*m]); 
	return dp[(1<<cnt)-1][n*m];
}
void dfs(int x,int y,int op)//op是容斥的正负号 
{//dfs从左上向右下一个个填新的状态 
	if(x==n+1) {ans=(ans+op*calc()%mod+mod)%mod;return;}
	if(y==m+1) {dfs(x+1,1,op);return;}
	dfs(x,y+1,op);//不改当前这个点的类型 
	bool flag=1;
	for(int i=0;i<=8;i++)
	{//判断是否能改当前这个点的类型 
		int tx=x+dx[i],ty=y+dy[i];
		if(tx>=1&&tx<=n&&ty>=1&&ty<=m&&s[tx][ty]=='X') flag=0;
	}	
	if(flag) 
	{
		s[x][y]='X';
		dfs(x,y+1,-op);//改当前这个点的类型 
		s[x][y]='.';
	}
		
}
int main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++) scanf("%s",s[i]+1);
	for(int i=1;i<=n;i++)	
		for(int j=1;j<=m;j++)	
			if(s[i][j]=='X')
				for(int k=1;k<=8;k++)
				{
					int x=i+dx[k],y=j+dy[k];
					if(x>=1&&x<=n&&y>=1&&y<=m&&s[x][y]=='X')
						return puts("0"),0;//特判初始状态不合法 
				}
	dfs(1,1,1);
	printf("%lld\n",(ans+mod)%mod);
	return 0;
}

posted @ 2021-11-02 22:33  conprour  阅读(36)  评论(0编辑  收藏  举报