【Coel.学习笔记】插头动态规划

前言

某些状态压缩动态规划(如网格覆盖问题)需要在某些位置记录联通性信息,这种就被称为“插头”动态规划(下文简称插头 DP)。此类问题在陈丹琦(又是她)在集训队论文《基于连通性状态压缩的动态规划问题》中首次被总结。

插头 DP 通常使用轮廓线解决,本质上是状态压缩的一个方法。

例题讲解

插头 DP 知识点并不难,但分类讨论很多。这也是插头 DP 很多黑题的原因。

【模板】插头dp

洛谷传送门
给定一个 n×m 的棋盘,某些格子上有障碍,试求出哈密顿回路个数。2n,m12

解析:考虑朴素的状态压缩做法。记 fs,i 表示当前走过点的状态为 s,到达点 i 时的回路个数,那么走到 j 时就可以转移到 fs+2j,j 的答案。时间复杂度为 O(2nm×(nm)2)

由于整张图是一个棋盘,所以求解可以得到优化。我们首先把转移方式改成按格子转移,这时转移过的部分和没转移的部分就会有一条分界线,也就是轮廓线。对于一个格子而言,其上下左右四个方向(即“插头”)只能选择两个,一个作为出边,一个作为入边。那么按照每格进行状态转移,可能的结果只有 C42=6 种。

此时,我们需要记录哪几个边属于同一个连通块,因为回路的本质就是联通整个棋盘的连通块。记录连通块有两种方法:最小表示法和括号表示法。

最小表示法需要遍历所有出边,当找到一个没有标记的连通块时进行标号,然后从头继续遍历,直到所有连通块都被标记过。当出边不存在时,记作 0

括号表示法的原理基于回路的性质,由于经过轮廓线的边必然满足两两配对且不存在交叉,所以联系括号序列,用 1 表示左括号(即入边), 2 表示右括号(即出边),0 表示没有连边,那么每一个配对的括号组就可以表示当前能够连通的括号状态。括号表示法的值域为 {0,1,2},所以更新状态更加方便。

fs,i,j 表示已经处理到的格子为 (i,j),轮廓线状态为 s 时的回路个数,则转移可能如下(为方便,用二元组 (x,y) 替代 s):

  • (i,j) 为一个障碍物,则当 x=y=0 时不可更新(因为障碍物不可能连边),否则保持状态不变;
  • (i,j) 不是障碍物(下同)且 x=y=0,则必然会利用到相邻格子之外的边(即除掉 x,y 之后剩下的边);
  • x=0,y0,则剩下的两条边一个连上,一个不连,做一个枚举即可;
  • x0,y=0,同理进行枚举;
  • x=y=1,显然 x,y 要连起来,但此时两个连通块会连起来,所以另一边的两个插头需要调整,即前一个改成 1
  • x=y=2,同样需要微调,把后一个变成 2
  • x=2,y=1,直接相连即可得到合法结果,无需调整;
  • x=1,y=2,此时 (x,y) 必然在同一个连通块中,所以这种情况只会发生在回路最后一个格子上,在此进行方案计数即可。

由于直接存状态的数量级为 313,而实际上可能的状态很少(因为不能交叉等性质会大大消除可能的状态个数),所以我们考虑用哈希表存状态,并且用上滚动数组。STL 和 pb_ds 的哈希表效率都很一般,需要手写哈希表。存储状态则需要用一个数组存下每次滚动的结果。

总的时间复杂度为 O(S×n2m),其中 S 为状态个数,与 O(3n) 同阶,但除去不合法方案后其实很小。

代码看着很长,实际上大部分都在分类讨论,所以也不怎么难写。

#include <iostream>
#include <cstring>

const int maxn = 5e4 + 10, mod = 1e6 + 3;

typedef long long ll;

int n, m, dx, dy, cur;
int M[20][20], Q[2][maxn], cnt[maxn];
int Hash[2][mod];
ll v[2][mod], res;

using namespace std;

int getHash(int cur, int x) { //查探法哈希
    int t = x % mod;
    while (Hash[cur][t] != -1 && Hash[cur][t] != x)
        if (++t == mod) t = 0;
    return t;
}

inline void insPlug(int cur, int st, ll w) { //插入插头
    int t = getHash(cur, st);
    if (Hash[cur][t] == -1) {
        Hash[cur][t] = st, v[cur][t] = w;
        Q[cur][++cnt[cur]] = t;
    } else v[cur][t] += w;
}

inline int getState(int st, int k) { return st >> k * 2 & 3; } //得到四进制下第 k 位数字(三进制比较难写)

inline int setState(int k, int v) { return v * (1 << k * 2); } //构造四进制第 k 位为 v 的数字

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        char s[25];
        cin >> (s + 1);
        for (int j = 1; j <= m; j++)
            if (s[j] == '.') M[i][j] = 1, dx = i, dy = j;
    }
    memset(Hash, -1, sizeof(Hash));
    insPlug(cur, 0, 1);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= cnt[cur]; j++)
            Hash[cur][Q[cur][j]] <<= 2; //先把队列里的状态左移一位(二进制下两位)
        for (int j = 1; j <= m; j++) {
            int lst = cur;
            cur ^= 1, cnt[cur] = 0;
            memset(Hash[cur], -1, sizeof(Hash[cur]));
            for (int k = 1; k <= cnt[lst]; k++) {
                int st = Hash[lst][Q[lst][k]];
                ll w = v[lst][Q[lst][k]];
                int x = getState(st, j - 1), y = getState(st, j);
                if (!M[i][j]) { //可能1:有障碍物
                    if (!x && !y) insPlug(cur, st, w);
                } else if (!x && !y) { //可能2:x=y=0
                    if (M[i + 1][j] && M[i][j + 1]):
                        insPlug(cur, st + setState(j - 1, 1) + setState(j, 2), w);
                } else if (!x && y) { //可能3:x=0,y!=0
                    if (M[i][j + 1]) insPlug(cur, st, w);
                    if (M[i + 1][j]) insPlug(cur, st + setState(j - 1, y) - setState(j, y), w);
                } else if (x && !y) { //可能4:x!=0,y=0
                    if (M[i][j + 1]) insPlug(cur, st - setState(j - 1, x) + setState(j, x), w);
                    if (M[i + 1][j]) insPlug(cur, st, w);
                } else if (x == 1 && y == 1) { //可能5:x=y=1
                    for (int u = j + 1, s = 1;; u++) {
                        int curState = getState(st, u);
                        if (curState == 1) s++;
                        else if (curState == 2)
                            if (--s == 0) { //调整插头
                                insPlug(cur, st - setState(j - 1, x) - setState(j, y) - setState(u, 1), w);
                                break;
                            }
                    }
                } else if (x == 2 && y == 2) { //可能6:x=y=2
                    for (int u = j - 2, s = 1;; u--) {
                        int curState = getState(st, u);
                        if (curState == 2) s++;
                        else if (curState == 1)
                            if (--s == 0) { //调整插头
                                insPlug(cur, st - setState(j - 1, x) - setState(j, y) + setState(u, 1), w);
                                break;
                            }
                    }
                } else if (x == 2 && y == 1) //可能7:x=2,y=1
                    insPlug(cur, st - setState(j - 1, x) - setState(j, y), w);
                else if (i == dx && j == dy) res += w; //可能8:x=1,y=2
                /*此时所有情况都排掉了,所以只要判断是不是到终点*/
            }
        }
    }
    cout << res;
    return 0;
}

插头 DP 的应用基本还是在哈密顿回路上,所以下面的几道题都和哈密顿回路有关。

[HNOI2004]邮递员

洛谷传送门
给定一个 n×m 的棋盘,棋盘上没有障碍物,求有向哈密顿回路的个数。
解析:其实就是去掉障碍物的限制,然后无向变有向而已,答案乘以二即可。另外这题没有让取模而且棋盘范围比较大,需要写高精度(虽然 __int128 就够了)。

代码和上一个大同小异,就不放了。

[HNOI2007]神奇游乐园

洛谷传送门
给定一个 n×m 的棋盘,每个格子带有一个权值,求权值最大的哈密顿回路。n100,m6,权值可能为负数。

解析由于问题只是把方案计数换成了最优化,所以模型上没太大区别。唯一的不同在于,当 x=0,y=0 时这个格子可以走也可以不走(上一题中每个格子都要走),所以要分开插头。

/*前面部分没什么区别(除了把 insPlug 的加法改成取 max)*/

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    memset(Hash, -1, sizeof(Hash));
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> a[i][j];
    insPlug(cur, 0, 0);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= cnt[cur]; j++)
            Hash[cur][Q[cur][j]] <<= 2;
        for (int j = 1; j <= m; j ++ ) {
            int lst = cur;
            cur ^= 1, cnt[cur] = 0;
            memset(Hash[cur], -1, sizeof(Hash[cur]));
            for (int k = 1; k <= cnt[lst]; k++) {
                int st = Hash[lst][Q[lst][k]], w = v[lst][Q[lst][k]];
                int x = getSt(st, j - 1), y = getSt(st, j);
                if (!x && !y) {
                    insPlug(cur, st, w);
                    if (i < n && j < m)
                        insPlug(cur, st + setSt(j - 1, 1) + setSt(j, 2), w + a[i][j]);
                } else if (!x && y) {
                    if (i < n) insPlug(cur, st + setSt(j - 1, y) - setSt(j, y), w + a[i][j]);
                    if (j < m) insPlug(cur, st, w + a[i][j]);
                } else if (x && !y) {
                    if (i < n) insPlug(cur, st, w + a[i][j]);
                    if (j < m) insPlug(cur, st - setSt(j - 1, x) + setSt(j, x), w + a[i][j]);
                } else if (x == 1 && y == 1) {
                    for (int u = j + 1, s = 1;; u++) {
                        int z = getSt(st, u);;
                        if (z == 1) s++;
                        else if (z == 2)
                            if (--s == 0) {
                                insPlug(cur, st - setSt(j - 1, x) - setSt(j, y) - setSt(u, 1), w + a[i][j]);
                                break;
                            }
                    }
                } else if (x == 2 && y == 2) {
                    for (int u = j - 2, s = 1;; u--) {
                        int z = getSt(st, u);
                        if (z == 2) s++;
                        else if (z == 1)
                            if (--s == 0) {
                                insPlug(cur, st - setSt(j - 1, x) - setSt(j, y) + setSt(u, 1), w + a[i][j]);
                                break;
                            }
                    }
                } else if (x == 2 && y == 1) {
                    insPlug(cur, st - setSt(j - 1, x) - setSt(j, y), w + a[i][j]);
                } else if (st == setSt(j - 1, x) + setSt(j, y))
                    gma(res, w + a[i][j]);
            }
        }
    }
    cout << res;
    return 0;
}

[SCOI2011]地板

洛谷传送门
给定一个 n×m(原文为 r×c,这里改成符合习惯的写法)的棋盘,某些格子上有障碍。试求出用若干个 L 型地板铺满整个棋盘的方案数对 20110520 取模的结果。L 型地板的定义参见原题、

解析:与模板题相比,这道题换成了 L 型地板。看起来很复杂,但仔细一想,L 型地板其实就是限制为有且只有一个拐弯点。根据这一点,我们同样可以用一个三进制表示插头状态:0 表示没有插头,1 表示插头为直线,2 表示插头为拐弯点。

接下来继续分类讨论就行了,这里限于篇幅我比较懒没有放出来,请读者参考代码画图思考。

另外为了让状态个数少一些,从而节省哈希表空间,我们保持 mn,具体来说当 n<m 的时候翻转一下即可。

/*insPlug 换成加法取模*/

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    memset(Hash, -1, sizeof(Hash));
    for (int i = 1; i <= n; i++) {
        cin >> (s + 1);
        for (int j = 1; j <= m; j++)
            if (s[j] == '_')
                a[i][j] = 1, dx = i, dy = j;
    }
    if (n < m) {
        swap(n, m), swap(dx, dy);
        for (int i = 1; i <= n; i++)
            for (int j = 1; j < i; j++)
                swap(a[i][j], a[j][i]);
    }
    insPlug(cur, 0, 1);
    for (int i = 1; i <= n; i++) {
        for (int j  = 1; j <= cnt[cur]; j++)
            Hash[cur][Q[cur][j]] <<= 2;
        for (int j = 1; j <= m; j++) {
            int lst = cur;
            cur ^= 1, cnt[cur] = 0;
            memset(Hash[cur], -1, sizeof(Hash[cur]));
            for (int k = 1; k <= cnt[lst]; k++) {
                int st = Hash[lst][Q[lst][k]], w = v[lst][Q[lst][k]];
                int x = getSt(st, j - 1), y = getSt(st, j);
                if (!a[i][j]) { //有障碍物,状态不变
                    if (!x && !y) insPlug(cur, st, w);
                } else if (!x && !y) { //x=y=0
                    if (a[i][j + 1]) insPlug(cur, st + setSt(j, 1), w); //右边插头
                    if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, 1), w); //下面插头
                    if (a[i][j + 1] && a[i + 1][j])
                        insPlug(cur, st + setSt(j - 1, 2) + setSt(j, 2), w); //两个都插头
                } else if (!x && y == 1) { //x=0,y=1
                    if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, y) - setSt(j, y), w); //右边插头
                    if (a[i][j + 1]) insPlug(cur, st + setSt(j, 1), w); //下面插头
                } else if (x == 1 && !y) { // x=1,y=0
                    if (a[i][j + 1]) insPlug(cur, st - setSt(j - 1, x) + setSt(j, x), w); //下面插头
                    if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, 1), w); // 右边插头
                } else if (!x && y == 2) { // x=0,y=2
                    if (i == dx && j == dy) (res += w) %= mod; //到达边界
                    else if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, y) - setSt(j, y), w);
                    insPlug(cur, st - setSt(j, y), w); // 下面插头
                } else if (x == 2 && !y) {
                    if (i == dx && j == dy) (res += w) %= mod; // 到达边界
                    else if (a[i][j + 1]) insPlug(cur, st - setSt(j - 1, x) + setSt(j, x), w);
                    insPlug(cur, st - setSt(j - 1, x), w); // 下边插头
                } else if (x == 1 && y == 1) { // x=1,y=1
                    if (i == dx && j == dy) (res += w) %= mod; // 到达边界
                    insPlug(cur, st - setSt(j - 1, x) - setSt(j, y), w); //其余都是不合法方案,想一想为什么
                }
            }
        }
    }
    cout << res;
    return 0;
}

本文作者:Coel's Blog

本文链接:https://www.cnblogs.com/Coel-Flannette/p/16773742.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   秋泉こあい  阅读(95)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
🔑
  1. 1 アイノマテリアル (feat. 花里みのり&桐谷遥&桃井愛莉&日野森雫&MEIKO) MORE MORE JUMP!
アイノマテリアル (feat. 花里みのり&桐谷遥&桃井愛莉&日野森雫&MEIKO) - MORE MORE JUMP!
00:00 / 00:00
An audio error has occurred.