P4363一双木棋-题解
本篇题解献给和我一样看不懂其他题解的状压DP小白
相信大家都是看了其他题解看不懂才看到这篇题解的(莫名自信),所以什么每行棋子数递减啊,每行的棋子都排在左边啊,就不用我多说了,直接切入正题(大段文字多,请耐心观看)。
轮廓线DP
没见过不用慌,我也没见过(雾
轮廓线,就是把矩阵从右上角到左下角沿着有棋子和没棋子的分界线描一下,往下走就用1表示,往左走就用0表示。这样我们就得到了一个01串,即一个二进制数,这就是我们的DP状态。
例如(图丑勿喷)
这种情况下轮廓线状态为左左下左下下左左下,即
我们设
那么状态有了,怎么转移呢?以上图为例,即将要白棋(后手)下棋,后手肯定希望他下的一步棋能使得从现在开始(以前的我们不管)先手-后手得分尽可能小(如果是负数那他更开心了),所以
同理,如果即将先手下,就把上面的式子里min改成max(因为他希望下这步棋能使得他的得分-对方的得分最大),
这样我们就可以用记忆化搜索很容易地跑出来了。
哎等等,我们还有两个问题没解决,观察上面的式子,我们还不知道怎么枚举“所有再下一步棋可能得到的方案
我们一个一个解决,先看第一个问题。还是以上图为例,白棋(后手)可以下在
原:
新:
我们发现只有第四位和第五位变了,而原先的第四位和第五位就是我们之前说的先出现一个0,再出现一个1,那么转移就是把0变成1,把1变成0。
具体来说,就是当我们枚举到一个
开始讨论第二个问题,我们知道该在轮廓线的哪一位上下棋,怎么知道那步棋的具体坐标呢?这个问题比较容易解决,只需要在枚举
代码
#include<iostream>
using namespace std;
int n,m;
int a[20][20],b[20][20];
int f[1048580];
bool vis[1048580];
int dfs(int s,bool now)//s是轮廓线状态,now记录是先手还是后手(虽然可以根据s算出来,但这样比较方便)
{
if(vis[s])//记忆化搜索标配
return f[s];
vis[s]=1;
if(s==(1<<(n+m))-(1<<m))//已经填满棋子(即n个下加m个左)
return 0;
int num0=0,num1=0;//维护坐标(已经出现多少个左/多少个下
if(now)//先手
{
f[s]=-2147483647;
for(int i=0;i<n+m-1;i++)
{
num1+=((s&(1<<i))>>i);
num0+=(!((s&(1<<i))>>i));
if((s&(1<<i))>0&&(s&(1<<(i+1)))==0)
f[s]=max(f[s],dfs(s^(3<<i),0)+a[n-num1+1][num0+1]);//递归计算
}
return f[s];
}
else//后手
{
f[s]=2147483647;
for(int i=0;i<n+m-1;i++)
{
num1+=((s&(1<<i))>>i);
num0+=(!((s&(1<<i))>>i));
if((s&(1<<i))&&!(s&(1<<(i+1))))
f[s]=min(f[s],dfs(s^(3<<i),1)-b[n-num1+1][num0+1]);//递归计算
}
return f[s];
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>b[i][j];
cout<<dfs((1<<n)-1,1)<<endl;
//(1<<n)-1是初始状态,即m个左加n个下(一步棋也没下)
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步