P4363 [九省聯考2018]一雙木棋chess(輪廓線狀壓)
這兩天學了這個。
是輪廓線狀壓的或許算是裸題。
關鍵在於怎麼壓狀態。
題意
-
有一個 \(n \times m\) 的棋盤,兩個人輪流下棋。
-
一個位置可以落子當且僅當這個位置的左側和上面都有棋子。
-
兩個人落在對應的位置會收穫各自的貢獻值。
-
最大化自己的得分減去對方的得分。
做法
博弈論?
是有 max_min搜尋還有一些別的做法的。
看眼資料範圍,都是 \(10\) 以內的。
於是考慮狀壓。
問題隨之而來:如何存狀態?
由於落子是有限制的,所以需要考慮在什麼情況下可以落子。
由於落子是合法的需要滿足左邊和上邊都有棋子,於是在整個過程中必然會有這樣的情況:
可以發現這樣的輪廓一定是連續的。
於是這就是我們需要的輪廓線。
可以考慮儲存右下角的輪廓線作為狀態。
如何儲存呢?
首先這個輪廓線的長度肯定是 \(n+m\) 的。
我們用 \(0\) 來表示橫邊,用 \(1\) 來表示豎邊。
可以舉個例子:
對於當前的狀態,從右上角開始,向左下角延伸,如果有橫邊那麼當前位就是 \(0\),有豎邊就是 \(1\);
所以狀態串就是 \(000101010101010101\)
之後對於狀態的判斷以及轉移過程,都可以利用這個輪廓線。
可以發現,經過這樣的狀壓之後,判斷是否可以落子的條件就變成了:是否存在一個位置,滿足串中有 \(01\) 這樣的兩位,如果有,那麼就可以落子。
落子以後,該位置的 \(01\)
對於轉移過程中的具體操作,由於需要判斷狀態串中是否存在 \(01\),可以採用一種手法:
if(((now>>i)&3)!=1)continue;
由於 \(3\) 的二進位制是 \(011\) ,如果也就是判斷是否存在 \(01\)的情況。
之後改變狀態:
int nxt=now^(3<<i);
之後起始狀態和終止狀態就很明顯了:
起始狀態:\(000...00111...11\)
終止狀態:\(111...11000...00\)
之後就是關於如何 DP 的問題了。
其實這個事情是不好辦的。
首先這個題目要求我們兩個人都要走最優策略。
如果按照我們常規的 DP 思路的話,自然是要設 \(f[S]\)
但是對於這種博弈論 DP 的題目是不可以這樣做的。
由於兩方都要採取最優策略,所以我們不得不考慮將來的行動對現在現在的影響。
如果正序 dp 的話也許會發生當前確實取到了最大值但是其實並不是最優策略。
於是我們可以考慮倒過來 dp。
可以假裝我們可以一步看到結局,然後選擇對自己最有利的狀態。
顯然棋盤被佈滿的狀態下沒有人可以獲得新的分數,那麼如果我們用 \(f[S]\) 表示從 \(S\) 輪廓線的狀態距離遊戲結束還能得多少分。
那麼我們會發現,如果這個局面是先手下完以後形成的,那麼如何這個局面如何轉移的主動權顯然是攥在後手的手裡,所以此時這個 dp 值應該由所有後繼狀態的相對於先手最劣的狀態轉移過來,也就是所有的後繼狀態取個 \(\min\)。
如果是後手的話,dp值就應該是對於後手最劣的狀態轉移過來,也就是所有的後繼狀態取個 \(\max\)。
也就是第一個人想要最大化兩者的差值,第二個人想要最小化這個差值。
//#define LawrenceSivan
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define re register
const int maxn=1e5+5;
#define INF 0x3f3f3f3f
int n,m;
int f[1<<21];
int a[15][15],b[15][15];
bool vis[1<<21];
int dfs(int now,int who){
if(vis[now])return f[now];//記憶化搜尋
vis[now]=true;
f[now]= who?-INF:INF;//先手取max,所以初始最小,後手取min,所以初始最大
int x=n,y=0;
for(re int i=0;i<n+m-1;i++){
if((now>>i)&1)x--;
else y++;
if(((now>>i)&3)!=1)continue;
int nxt=now^(3<<i);
if(who)f[now]=max(f[now],dfs(nxt,0)+a[x][y]);
else f[now]=min(f[now],dfs(nxt,1)-b[x][y]);
}
return f[now];
}
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
return x*f;
}
int main(){
#ifdef LawrenceSivan
freopen("aa.in","r",stdin);
freopen("aa.out","w",stdout);
#endif
n=read();m=read();
for(re int i=0;i<n;i++){
for(re int j=0;j<m;j++){
a[i][j]=read();
}
}
for(re int i=0;i<n;i++){
for(re int j=0;j<m;j++){
b[i][j]=read();
}
}
vis[((1<<n)-1)<<m]=true;//倒過來DP,即從結束狀態開始
f[((1<<n)-1)<<m]=0;
dfs((1<<n)-1,1);
printf("%d\n",f[(1<<n)-1]);//最後找到開始狀態下對應的答案
return 0;
}