插头 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;
}