插头 DP

插头 DP

插头 DP 主要用于处理一种基于连通性状压 DP 问题。

约定:

  • 阶段:DP 的顺序。

  • 轮廓线:已决策状态和未决策状态的分界线。

  • 插头:一个格子存在某方向的插头,则其在这个方向与相邻格子相连。

主要思想是状压轮廓线上的状态,然后逐格转移。

实现技巧

状态表示

大多情况只需要用 0/1 来代表这个位置是否存在插头即可,一般会以二进制或者四进制的形式压缩成一个 int ,操作的时候视情况展开成一个数组或者就直接对位进行操作。

一般取第 \(1 \sim n\) 位对应纵向插头,第 \(0\) 位对应横向插头。

编码形式

  • 无脑 \(0\)\(1\) 记录:适用于没有太多要求的情况。

  • 括号序列:适用于强制要求只有一个回路的情况。

    轮廓线上从左到右四个插头 \(a, b, c, d\) ,若 \(a, c\) 连通且不与 \(b\) 连通,则 \(b, d\) 一定不连通。

    两两匹配、不会交叉,很容易联想到括号匹配。

    将轮廓线上每一个连通分量中左边那个插头标记为左括号,右边那个插头标记为右括号即可。

  • 最小表示法:适用于希望选择一个连通块的情况,按顺序将连通块标号。如可以将 \(\{ \{ 1, 3, 5 \}, \{ 2, 6 \}, \{ 4 \} \}\) 编码为 \((0, 1, 0, 2, 0, 1)\) 。注意每次插入更新时都要转化成最小表示.

状态转移

插头 DP 每个阶段存在的合法态不会很多,故可以考虑把合法态哈希存储,然后在下一阶段只要找出哈希表内所有元素即可。

这一部分是可以滚动的,可以进一步压缩空间。

对于一个状态,要转移到下一个阶段,就只需要提取出这个状态对应的上插头和左插头,然后加上自己的状态进行一大堆分类讨论即可。

常见模型

覆盖模型

HDU1400 Mondriaan's Dream

给出一个 \(n \times m\) 的棋盘,求用 \(1 \times 2\)\(2 \times 1\) 的多米诺骨牌覆盖整个棋盘的方案数。

\(n, m \leq 11\)

\(f_{i, j, s}\) 表示已经考虑到 \((i, j)\) ,且当前轮廓线下状态为 \(s\) 的方案数。转移时分类讨论 \((i, j)\) 的情况:

  • \((i, j)\) 已被覆盖:\(s \to s \setminus \{ j \}\)
  • \((i, j)\) 未被覆盖
    • 横放:\(s \to s \cup \{ j + 1 \}\) ,此时需要满足 \((i, j + 1)\) 在棋盘内,且 \(j + 1 \not \in s\)\(j + 1\) 未被覆盖)。
    • 竖放:\(s \to s \cup \{ j \}\) ,此时需要满足 \((i + 1, j)\) 在棋盘内。

时间复杂度 \(O(nm 2^m)\)

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

int n, m;

signed main() {
    while (~scanf("%d%d", &n, &m)) {
        if (!n && !m)
            return 0;

        vector<ll> f(1 << m);
        f[0] = 1;

        for (int i = 0; i < n; ++i)
            for (int j = 0; j < m; ++j) {
                vector<ll> g(1 << m);

                for (int s = 0; s < (1 << m); ++s) {
                    if (s >> j & 1) // 已被覆盖
                        g[s ^ 1 << j] += f[s]; // 不放
                    else { // 未被覆盖
                        if (j != m - 1 && ~s >> (j + 1) & 1)
                            g[s | 1 << (j + 1)] += f[s]; // 横放

                        if (i != n - 1)
                            g[s | 1 << j] += f[s]; // 竖放
                    }
                }

                f = g;
            }

        printf("%lld\n", f[0]);
    }

    return 0;
}

P3272 [SCOI2011] 地板

给出一个 \(n \times m\) 的棋盘,其中一些格子是障碍。求用任意大小的 L 型砖铺满棋盘的方案数。

\(n \times m \leq 100\)

考虑插头 DP,由于 \(n \times m \leq 100\) ,因此 \(\min(n, m) \leq 10\) ,选取小的一维状压。

考虑状压的状态表示,\(0\) 表示没有插头,\(1\) 表示有插头但 L 型尚未拐弯,\(2\) 表示有插头且 L 型已经拐弯,因此可以用四进制存储状态。

若当前格为障碍,需要满足没有左插头和上插头。否则分类讨论:

  • 没有左插头和上插头:可以向下或向右新建一个 \(1\) 插头,或连接下和右建立一个 \(2\) 插头。
  • 左插头和上插头均为 \(1\) 插头:在这里连接起来。
    • 此时若为最后一个非障碍点,则更新答案。
  • 只有一个插头 \(1\) :可以顺延建立插头 \(1\) ,也可以拐弯建立插头 \(2\)
  • 只有一个插头 \(2\) :可以顺延建立插头 \(2\) ,也可以停下。
    • 此时若为最后一个非障碍点,则更新答案。
  • 一个插头 \(1\) 和一个插头 \(2\) :插头 \(2\) 必须在上一步停下,因此不会出现这种情况。
  • 两个插头 \(2\) :由于一个插头 \(2\) 必须在上一步停下,因此不会出现这种情况。
#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;
using namespace __gnu_pbds;
const int Mod = 20110520;
const int N = 1e2 + 7;

int pw[N];
char str[N][N];

int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 0; i < n; ++i)
        scanf("%s", str[i]);

    if (n < m) {
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < i; ++j)
                swap(str[i][j], str[j][i]);

        swap(n, m);
    }

    int edx = -1, edy = -1;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j)
            if (str[i][j] == '_')
                edx = i, edy = j;

    if (edx == -1 && edy == -1)
        return puts("1"), 0;

    pw[0] = 1;

    for (int i = 1; i <= m; ++i)
        pw[i] = pw[i - 1] << 2;

    cc_hash_table<int, int> f;
    f[0] = 1;
    int ans = 0;

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            cc_hash_table<int, int> g;

            auto update = [&](int bit, int val) {
                g[bit] = add(g[bit], val);
            };

            for (auto it : f) {
                int bit = it.first, val = it.second, le = bit / pw[j] & 3, up = bit / pw[j + 1] & 3;

                if (str[i][j] == '*') {
                    if (!le && !up)
                        update(bit, val);
                } else if (!le && !up) {
                    if (i + 1 < n && str[i + 1][j] == '_')
                        update(bit + pw[j], val);

                    if (j + 1 < m && str[i][j + 1] == '_')
                        update(bit + pw[j + 1], val);

                    if (i + 1 < n && str[i + 1][j] == '_' && j + 1 < m && str[i][j + 1] == '_')
                        update(bit + pw[j] * 2 + pw[j + 1] * 2, val);
                } else if (le == 1 && up == 1) {
                    if (i == edx && j == edy)
                        ans = add(ans, val);

                    update(bit - pw[j] - pw[j + 1], val);
                } else if (!le && up == 1) {
                    if (i + 1 < n && str[i + 1][j] == '_')
                        update(bit + pw[j] - pw[j + 1], val);

                    if (j + 1 < m && str[i][j + 1] == '_')
                        update(bit + pw[j + 1], val);
                } else if (le == 1 && !up) {
                    if (i + 1 < n && str[i + 1][j] == '_')
                        update(bit + pw[j], val);

                    if (j + 1 < m && str[i][j + 1] == '_')
                        update(bit - pw[j] + pw[j + 1], val);
                } else if (!le && up == 2) {
                    if (i == edx && j == edy)
                        ans = add(ans, val);
                    
                    if (i + 1 < n && str[i + 1][j] == '_')
                        update(bit + pw[j] * 2 - pw[j + 1] * 2, val);

                    update(bit - pw[j + 1] * 2, val);
                } else if (le == 2 && !up) {
                    if (i == edx && j == edy)
                        ans = add(ans, val);
                    
                    if (j + 1 < m && str[i][j + 1] == '_')
                        update(bit - pw[j] * 2 + pw[j + 1] * 2, val);

                    update(bit - pw[j] * 2, val);
                }
            }

            f = g;
        }

        cc_hash_table<int, int> g;

        for (auto it : f)
            g[it.first << 2] = it.second;

        f = g;
    }

    printf("%d", ans);
    return 0;
}

多条回路模型

P5074 Eat the Trees

给出一个 \(n \times m\) 的棋盘,其中一些格子不能布线,剩下的格子必须布线。

求用若干条回路覆盖整个的棋盘的方案数。

\(n, m \leq 12\)

类似于骨牌覆盖模型,状态需要记录插头是否存在,然后成对的合并和生成插头即可。

由于要形成回路,因此每个格子应恰有两个插头。

\(f_{i, j, s}\) 表示考虑到 \((i, j)\) ,此时轮廓线下的状态为 \(s\) 的方案数,转移不难。注意对于一个宽度为 \(m\) 的棋盘,轮廓线的长度为 \(m + 1\)\(m\) 个上插头和 \(1\) 个左插头)。

当一行转移完成之后,最右边的左插头会转化为最左边的右插头,此时需要更新状态。

时间复杂度 \(O(nm 2^m)\)

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

int n, m;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &m);
        vector<ll> f(1 << (m + 1));
        f[0] = 1;

        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                int exist;
                scanf("%d", &exist);
                vector<ll> g(1 << (m + 1));
                // s 第 j 位由 f 的 (i, j) 左边界转移到 g 的 (i, j) 下边界
                // s 第 j + 1 位由 f 的 (i, j) 上边界转移到 g 的 (i, j) 右边界

                for (int s = 0; s < (1 << (m + 1)); ++s) {
                    int le = s >> j & 1, up = s >> (j + 1) & 1;

                    if (exist) {
                        if (le && up) // 左、上都有插头
                            g[s ^ 3 << j] += f[s]; // 由于布线不能相交,因此此时必须把它们连起来
                        else if (le ^ up) // 左、上恰有一个插头
                            g[s ^ 3 << j] += f[s], g[s] += f[s]; // 可以延续到下边或右边
                        else // 左、上都没有插头
                            g[s ^ 3 << j] += f[s]; // 此时必须放一个拐点
                    } else {
                        if (!le && !up)
                            g[s] += f[s]; // 若有障碍物,则不能有任何一个插头
                    }
                }

                f = g;
            }

            vector<ll> g(1 << (m + 1));

            for (int s = 0; s < (1 << m); ++s)
                g[s << 1] = f[s]; // 两行交界处左插头由最右到最左,而最左应为空,因此状态整体左移一位

            f = g;
        }

        printf("%lld\n", f[0]);
    }
    
    return 0;
}

单条回路模型

P5056 【模板】插头 DP

给出一个 \(n \times m\) 的棋盘,其中一些格子不能布线,剩下的格子必须布线。

求用一条回路覆盖整个的棋盘的方案数。

\(n, m \leq 12\)

\(f_{i, j, s}\) 表示考虑到 \((i, j)\) ,轮廓线下状态为 \(s\) 的方案数。转移分三类情况:

  • 新建一个连通分量:当前格放右插头和下插头时出现该情况。新建的两个插头联通且不与其他插头连通。

  • 合并两个连通分量:当前格放上插头和左插头时出现该情况。若两个插头不连通,则将两个插头所处连通分量合并,标记为相同的连通块标号,重新得到最小表示。否则相当于出现了回路,这种情况只能出现在最后一个非障碍格子。

  • 保持原来的连通分量:当前格上插头和左插头不同时出现时出现该情况。此时插头相当于原来的延续,连通块标号相同,并且不会影响其他插头的连通块标号。

注意处理完一行后需要对轮廓线进行更新。

具体的,记 \(le\) 表示左插头,\(up\) 表示上插头。用四进制表示单点状态, \(0\) 表示无插头,\(1\) 表示左端点,\(2\) 表示右端点。

若当前点是障碍,则 \(le = up = 0\) 才能产生状态转移,有插头就代表会走到该点。转移后当然这两小段插头也为空,直接添加状态即可。

否则需要分类讨论:

  • \(le = up = 0\) :则要加两个插头确保该点被布线。
  • \(le = 0, up \neq 0\) :上插头可以选择直走或右拐。
    • 此时插头的括号状态不变,但是插头在轮廓线上的位置会变。
  • \(le \neq 0, up = 0\) :与上条类似。
  • \(le = up = 1\) :合并两个连通分量,直接删去两个插头。但由于二者都是左插头,需要将其对应的两个右插头匹配。
  • \(le = up = 2\) :与上条类似。
  • \(le = 2, up = 1\) :合并两个连通分量,直接删去两个插头即可。此时不需要处理另一端的插头,因为会自动匹配。
  • \(le = 1, up = 2\) :说明达到了闭合状态,仅在当前点为终点时更新答案,否则状态不合法。

时间复杂度 \(O(nm^2 3^m)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 13;

int pw[N];
char str[N][N];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);
    pw[0] = 1;

    for (int i = 1; i <= m; ++i)
        pw[i] = pw[i - 1] << 2;

    int edx = -1, edy = -1;

    for (int i = 0; i < n; ++i) {
        scanf("%s", str[i]);

        for (int j = 0; j < m; ++j)
            if (str[i][j] == '.')
                edx = i, edy = j;
    }

    if (edx == -1 && edy == -1)
        return puts("1"), 0;

    ll ans = 0;
    unordered_map<int, ll> f;
    f[0] = 1;

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            unordered_map<int, ll> g;

            for (auto it : f) {
                int bit = it.first, le = bit / pw[j] & 3, up = bit / pw[j + 1] & 3;
                ll val = it.second;

                if (str[i][j] == '*') {
                    if (!le && !up)
                        g[bit] += val;
                } else if (!le && !up) {
                    if (i + 1 < n && str[i + 1][j] == '.' && j + 1 < m && str[i][j + 1] == '.')
                        g[bit + pw[j] + pw[j + 1] * 2] += val;
                } else if (!le && up) {
                    if (j + 1 < m && str[i][j + 1] == '.')
                        g[bit] += val;

                    if (i + 1 < n && str[i + 1][j] == '.')
                        g[bit + pw[j] * up - pw[j + 1] * up] += val;
                } else if (le && !up) {
                    if (i + 1 < n && str[i + 1][j] == '.')
                        g[bit] += val;

                    if (j + 1 < m && str[i][j + 1] == '.')
                        g[bit - pw[j] * le + pw[j + 1] * le] += val;
                } else if (le == 1 && up == 1) {
                    for (int l = j + 2, sum = 1; l <= m; ++l) {
                        if ((bit / pw[l] & 3) == 1)
                            ++sum;
                        else if ((bit / pw[l] & 3) == 2)
                            --sum;

                        if (!sum) {
                            g[bit - pw[j] - pw[j + 1] - pw[l]] += val;
                            break;
                        }
                    }
                } else if (le == 2 && up == 2) {
                    for (int l = j - 1, sum = 1; ~l; --l) {
                        if ((bit >> (l * 2) & 3) == 1)
                            --sum;
                        else if ((bit >> (l * 2) & 3) == 2)
                            ++sum;

                        if (!sum) {
                            g[bit - pw[j] * 2 - pw[j + 1] * 2 + pw[l]] += val;
                            break;
                        }
                    }
                } else if (le == 2 && up == 1)
                    g[bit - pw[j] * 2 - pw[j + 1]] += val;
                else if (i == edx && j == edy)
                    ans += val;
            }

            f = g;
        }

        unordered_map<int, ll> g;

        for (auto it : f)
            g[it.first << 2] = it.second;

        f = g;
    }

    printf("%lld", ans);
    return 0;
}

P3190 [HNOI2007] 神奇游乐园

给出一个 \(n \times m\) 的棋盘,每格内有一个权值,求经过权值和最大的回路的权值和。

\(n \leq 100\)\(m \leq 6\)\(|a_{i, j}| \leq 10^3\)

和上题不同的地方在于一个格子可以走或不走,转移是类似的。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e2 + 7, M = 6;

int a[N][M], pw[M];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);
    pw[0] = 1;

    for (int i = 1; i <= m; ++i)
        pw[i] = pw[i - 1] << 2;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j)
            scanf("%d", a[i] + j);

    int ans = -inf;
    unordered_map<int, int> f;
    f[0] = 0;

    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            unordered_map<int, int> g;
            g[0] = 0;

            auto update = [&](int bit, int val) {
                if (g.find(bit) == g.end())
                    g[bit] = val;
                else
                    g[bit] = max(g[bit], val);
            };

            for (auto it : f) {
                int bit = it.first, val = it.second, le = bit / pw[j] & 3, up = bit / pw[j + 1] & 3;

                if (!le && !up)
                    update(bit, val);

                if (!le && !up) {
                    if (i + 1 < n && j + 1 < m)
                        update(bit + pw[j] + pw[j + 1] * 2, val + a[i][j]);
                } else if (!le && up) {
                    if (j + 1 < m)
                        update(bit, val + a[i][j]);

                    if (i + 1 < n)
                        update(bit + pw[j] * up - pw[j + 1] * up, val + a[i][j]);
                } else if (le && !up) {
                    if (i + 1 < n)
                        update(bit, val + a[i][j]);

                    if (j + 1 < m)
                        update(bit - pw[j] * le + pw[j + 1] * le, val + a[i][j]);
                } else if (le == 1 && up == 1) {
                    for (int l = j + 2, sum = 1; l <= m; ++l) {
                        if ((bit / pw[l] & 3) == 1)
                            ++sum;
                        else if ((bit / pw[l] & 3) == 2)
                            --sum;

                        if (!sum) {
                            update(bit - pw[j] - pw[j + 1] - pw[l], val + a[i][j]);
                            break;
                        }
                    }
                } else if (le == 2 && up == 2) {
                    for (int l = j - 1, sum = 1; ~l; --l) {
                        if ((bit >> (l * 2) & 3) == 1)
                            --sum;
                        else if ((bit >> (l * 2) & 3) == 2)
                            ++sum;

                        if (!sum) {
                            update(bit - pw[j] * 2 - pw[j + 1] * 2 + pw[l], val + a[i][j]);
                            break;
                        }
                    }
                } else if (le == 2 && up == 1)
                    update(bit - pw[j] * 2 - pw[j + 1], val + a[i][j]);
                else
                    ans = max(ans, val + a[i][j]);
            }

            f = g;
        }

        unordered_map<int, int> g;

        for (auto it : f)
            g[it.first << 2] = it.second;

        f = g;
    }

    printf("%d", ans);
    return 0;
}

连通块模型

P3886 [JLOI2009] 神秘的生物

给出一个 \(n \times n\) 的棋盘,每格内有一个权值,求经过权值和最大的连通块的权值和。

\(n \leq 9\)

\(f_{i, j, s}\) 表示考虑到 \((i, j)\) ,轮廓线下状态为 \(s\) (连通性的最小表示)的方案数。不难发现轮廓线上至多五种连通块,算上空状态,可以用一个八进制数存储。

转移只要枚举当前格子是否选取,若选取则分类讨论:

  • 同时存在上插头和左插头:合并两个连通块,注意判断上插头和左插头是否在一个连通块内。
  • 只存在上插头或只存在左插头:延续连通块状态。
  • 不存在插头:新建连通块。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 11;

int a[N][N], pw[N];

int n;

inline int relabel(int state) {
    int res = 0, cnt = 0;
    vector<int> id(9);

    for (int i = 1; i <= n; ++i) {
        int x = state / pw[i] & 7;

        if (!x)
            continue;

        if (!id[x])
            id[x] = ++cnt;

        res += pw[i] * id[x];
    }

    return res;
}

signed main() {
    scanf("%d", &n);
    pw[0] = 1;

    for (int i = 1; i <= n; ++i)
        pw[i] = pw[i - 1] << 3;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            scanf("%d", a[i] + j);

    map<int, int> f;
    f[0] = 0;
    int ans = -inf;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j) {
            map<int, int> g;
            g[0] = 0;

            auto update = [&](int bit, int val) {
                if (g.find(bit) == g.end())
                    g[bit] = val;
                else
                    g[bit] = max(g[bit], val);
            };

            for (auto it : f) {
                int bit = it.first, val = it.second, le = bit / pw[j - 1] & 7, up = bit / pw[j] & 7;

                if (!up && bit)
                    update(relabel(bit - pw[j] * up), val);
                else {
                    for (int k = 1; k <= n; ++k)
                        if (k != j && (bit / pw[k] & 7) == up) {
                            update(relabel(bit - pw[j] * up), val);
                            break;
                        }
                }

                if (le && up) {
                    if (le != up) {
                        int nxt = bit;

                        for (int k = 1; k <= n; ++k)
                            if ((nxt / pw[k] & 7) == up)
                                nxt += pw[k] * (le - up);

                        update(relabel(nxt), val + a[i][j]);
                    } else
                        update(bit, val + a[i][j]);
                } else if (le)
                    update(bit + pw[j] * le, val + a[i][j]);
                else if (up)
                    update(bit, val + a[i][j]);
                else
                    update(relabel(bit + pw[j] * 7), val + a[i][j]);
            }

            f = g;

            for (auto it : f) {
                int bit = it.first, val = it.second, flag = 0;

                for (int k = 1; k <= n; ++k) {
                    int x = bit / pw[k] & 7;

                    if (x) {
                        if (!flag)
                            flag = x;
                        else if (x != flag) {
                            flag = 0;
                            break;
                        }
                    }
                }

                if (flag)
                    ans = max(ans, val);
            }
        }

    printf("%d", ans);
    return 0;
}
posted @ 2025-03-20 11:24  wshcl  阅读(42)  评论(0)    收藏  举报