【题解】[九省联考 2018] 一双木棋 chess(轮廓线 dp)
题目分析:
之前一直听说过轮廓线 \(dp\),但是没有真正见过,这次真正见到了让我大为震撼。
轮廓线 \(dp\) 顾名思义就是维护轮廓线,这类题目一般都是在网格图上。
而对于任意一条轮廓线也就是一条从左上到右下或者从左下到右上的线,而且一般这种轮廓线都满足一定的性质,也就是不可能存在向内凹陷的情况,即一定可以使用长度为 \(n+m\) 的 01 序列表示出这条轮廓线,一般也就是 0/1 分别代表横着的轮廓边或者竖着的轮廓边。
例如下图:
左下到右上的轮廓线即:01110011100,从左往右读的话会发现的确如此。
来看这道题目。
我们可以发现一条性质我们选择的棋子的列数由上到下一定是不增的,所以其就可以构成类似上图这样的轮廓线,也就是可以直接设 \(dp[S]\) 表示轮廓线为 \(S\) 时到全部填满时答案的变化,而转移的顺序不好弄,所以可以直接 \(dfs\) 从初始状态即 1111...0000... 开始搜,每选择一个方块其实就是相当于将 10 变成 01,末状态即 dp[0000...1111...] = 0,对于菲菲肯定是选择尽可能的大也就是取 \(\max\) 而对于牛牛肯定是选择尽可能的小也就是取 \(\min\)。
也要注意一点在代码中为了方便我一般都会选择从低位到高位遍历,所以其实代码里的 \(S\) 与题解中我分析时用到的 \(S\) 是翻转关系。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF = 1e9+5;
int n,m,f[1<<22],a[20][20],b[20][20];
bool vis[1<<22];
int dfs(int S,int now){ //特别需要注意的一点:我们写代码的时候为了好写,是将分析中的 S 倒过来写的
if(vis[S]) return f[S];
f[S] = now ? -INF : INF;
int x = n,y = 0;
for(int i=0; i<n+m-1; i++){
if((S>>i)&1) x--;
else y++;
if(((S >> i) & 3) != 1) continue;
int nxt = S ^ (3 << i);
if(now) f[S] = max(f[S],dfs(nxt,now^1) + a[x][y]);
else f[S] = min(f[S],dfs(nxt,now^1) - b[x][y]);
}
vis[S] = true;
return f[S];
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
scanf("%lld",&a[i][j]);
}
}
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
scanf("%lld",&b[i][j]);
}
}
memset(f,0x3f,sizeof(f));
f[((1<<n)-1)<<m]=0;vis[((1<<n)-1)<<m]=true;
printf("%lld\n",dfs((1<<n)-1,1));
return 0;
}