洛谷

洛谷题单指南-状态压缩动态规划-P4363 [九省联考 2018] 一双木棋 chess

原题链接:https://www.luogu.com.cn/problem/P4363

题意解读:n*m棋盘上,每个格子有a、b两种分数,A、B轮流按最优方案(自己得分-对方得分最大化)下棋(落子位置必须左边和上边格子全有棋子),最终求A的所有得分-B的所有得分。

解题思路:

1、状态表示

先看落子规则:可落子的位置必须左边和上边所有格子全有棋子,也就是说,从落子的格子到左上角构成的矩形中,只有落子处是空的!

有了这样一个实时,落子后棋盘会形成的状态如图所示:

也就是,所有棋子都会集中在每一行的前面,并且上面行的棋子数量一定大于或等于下面行的棋子数量。

这样一来,有棋子的格子和没有棋子的格子交界处就形成了轮廓线!

沿着轮廓线,从左下角开始,到右上角为止,我们定义向右走为0,向上走为1,上图轮廓线可以表示为001011010,注意一开始从左下角是往右、再往上,因此最低两位是0、1。

这样一来,就可以状态压缩轮廓线来表示所有棋盘状态,棋盘是n*m,状态中必有n个1,m个0。

2、状态转移

定义f[s]为轮廓线状态为s时,到棋盘满时可以得到的最优得分;

当A下棋时,f[s]意味着最大值;当B下棋时,f[s]意味着最小值。

从样例出发,初始状态为:00011

第一个棋只有一种下法,落子后状态为:00101

第二个棋有两种下法,

  如果下到1行2列,落子后状态为:01001

  如果下到2行1列,落子后状态为:00110

经过分析,下第一步棋时,状态从00011->00101;下第二步棋时,状态从00101->01001或者00101->00110

可以得出,状态的变化就是所有的01变成10,取最优,如此以来,就可以枚举搜索来递推。

设s是上一个状态,t是将s中某一处01替换为10后的状态,也就是下一个状态,将01替换为10也意味着落地区域向右下扩张一格,设这一格的坐标是x,y

那么有:

如果当前是A下棋:f[s] = max(f[s], f[t] + a[x][y])

如果当前是B下棋:f[s] = min(f[s], f[t] - b[x][y])

3、初始化

根据定义,我们可以知道终止状态为11…100…0,n个1,m个0,且由于棋下满了,f[11…100…0] = 0

当轮到A下棋时,f[s]初始化为INF;当轮到B下棋时,f[s]初始化为-INF

4、结果

根据定义,棋盘初始状态为00…011…1,m个0,n个1,f[00…011…1]即是结果。

直接枚举不好处理,通过递归+记忆化搜索来实现DP更直观。

100分代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 10, S = 20, INF = 0x3f3f3f3f;
int a[N][N], b[N][N];
int f[1 << S];
int n, m;

//dp(s,st)表示当前轮廓线状态是s,轮到st来下棋所能得到的最优得分
//st为1表示A下棋,0表示B下棋
int dp(int s, bool st)
{
    if(~f[s]) return f[s];
    f[s] = st? -INF : INF; //求最大值初始化为-INF,求最小值初始化为INF

    int x = n, y = 0; //x,y表示当状态从01变到10时,走的是哪个格子
    for(int i = 0; i < n + m - 1; i++) //每次枚举两个二进制位i、i+1
    {
        if(s >> i & 1) x--; else y++; //更新可能扩展的格子坐标
        if((s >> i & 3) != 1) continue; //第i+1、i位不是01,则跳过
        int t = s ^ (3 << i); //将第i+1,i位的01修改成10,实现状态转移
        if(st) f[s] = max(f[s], dp(t, st ^ 1) + a[x][y]);
        else f[s] = min(f[s], dp(t, st ^ 1) - b[x][y]);
    }
    return f[s];
}

int main()
{
    cin >> n >> m;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            cin >> a[i][j];
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            cin >> b[i][j];

    memset(f, -1, sizeof(f));
    f[(1 << n) - 1 << m] = 0; //初始化终止状态
    cout << dp((1 << n) - 1, 1);
    return 0;
}