【洛谷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;
}