插头 DP
有些 状压 DP 问题要求我们记录状态的连通性信息,这类问题一般被形象的称为插头 DP 或连通性状态压缩 DP。例如格点图的哈密顿路径计数,求棋盘的黑白染色方案满足相同颜色之间形成一个连通块的方案数,以及特定图的生成树计数等等。这些问题通常需要我们对状态的连通性进行编码,讨论状态转移过程中连通性的变化。
骨牌覆盖与轮廓线 DP¶
温故而知新,在开始学习插头 DP 之前,不妨先让我们回顾一个经典问题。
例题 「HDU 1400」Mondriaan’s Dream
题目大意:在
当
另一种划分阶段的方法是逐格 DP,或者称之为轮廓线 DP。
虽然逐格 DP 中我们的状态增加了一个维度,但是转移的时间复杂度减少为
1 2 3 4 5 6 | if (s >> j & 1) { // 如果已被覆盖
f1[s ^ 1 << j] += u; // 不放
} else { // 如果未被覆盖
if (j != m - 1 && (!(s >> j + 1 & 1))) f1[s ^ 1 << j + 1] += u; // 横放
f1[s ^ 1 << j] += u; // 竖放
}
|
观察到这里不放和竖放的方程可以合并。
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <bits/stdc++.h>
using namespace std;
const int N = 11;
long long f[2][1 << N], *f0, *f1;
int n, m;
int main() {
while (cin >> n >> m && n) {
f0 = f[0];
f1 = f[1];
fill(f1, f1 + (1 << m), 0);
f1[0] = 1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
swap(f0, f1);
fill(f1, f1 + (1 << m), 0);
#define u f0[s]
for (int s = 0; s < 1 << m; ++s)
if (u) {
if (j != m - 1 && (!(s >> j & 3))) f1[s ^ 1 << j + 1] += u; // 横放
f1[s ^ 1 << j] += u; // 竖放或不放
}
}
}
cout << f1[0] << endl;
}
}
|
习题 「SRM 671. Div 1 900」BearDestroys
题目大意:给定 E
或 S
。 对于一个矩阵,有一个计分方案。按照行优先的规则扫描每个格子,如果这个格子之前被骨牌占据,则 skip。 否则尝试放多米诺骨牌。如果放骨牌的方向在矩阵外或被其他骨牌占据,则放置失败,切换另一种方案或 skip。 如果是 E
则优先放一个 S
则优先放一个
术语¶
阶段:动态规划执行的顺序,后续阶段的结果只与前序阶段的结果有关(无后效性)。很多 DP 问题可以有多种划分阶段的方式。例如在背包问题中,我们通常既可以按照物品划分阶段,也可以按照背包容量划分阶段(外层循环先枚举什么)。而在多米诺骨牌问题中,我们可以按照行、列、格子以及对角线等特征划分阶段。
轮廓线:已决策状态和未决策状态的分界线。
插头:一个格子某个方向的插头存在,表示这个格子在这个方向与相邻格子相连。
路径模型¶
多条回路¶
例题 「HDU 1693」Eat the Trees
题目大意:求用若干条回路覆盖
严格来说,多条回路问题并不属于插头 DP,因为我们只需要和上面的骨牌覆盖问题一样,记录插头是否存在,然后成对的合并和生成插头就可以了。
注意对于一个宽度为 roll()
。
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #include <bits/stdc++.h>
using namespace std;
const int N = 11;
long long f[2][1 << (N + 1)], *f0, *f1;
int n, m;
int main() {
int T;
cin >> T;
for (int Case = 1; Case <= T; ++Case) {
cin >> n >> m;
f0 = f[0];
f1 = f[1];
fill(f1, f1 + (1 << m + 1), 0);
f1[0] = 1; //初始化
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
bool bad;
cin >> bad;
bad ^= 1;
swap(f0, f1);
fill(f1, f1 + (1 << m + 1), 0);
for (int s = 0; s < 1 << m + 1; ++s) //具体的dp转移,上面都是初始化
if (f0[s]) {
bool lt = s >> j & 1, up = s >> j + 1 & 1;
if (bad) {
if (!lt && !up) f1[s] += f0[s];
} else {
f1[s ^ 3 << j] += f0[s];
if (lt != up) f1[s] += f0[s];
}
}
}
swap(f0, f1);
fill(f1, f1 + (1 << m + 1), 0);
for (int s = 0; s < 1 << m; ++s) f1[s << 1] = f0[s];
}
printf("Case %d: There are %lld ways to eat the trees.\n", Case, f1[0]);
}
}
|
习题 「ZJU 4231」The Hive II
题目大意:同上题,但格子变成了六边形。
一条回路¶
例题「Andrew Stankevich Contest 16 - Problem F」Pipe Layout¶
例题 「Andrew Stankevich Contest 16 - Problem F」Pipe Layout
题目大意:求用一条回路覆盖
在上面的状态表示中我们每合并一组连通的插头,就会生成一条独立的回路,因而在本题中,我们还需要区分插头之间的连通性(出现了!)。这需要我们对状态进行额外的编码。
状态编码¶
通常的编码方案有括号表示和最小表示,这里着重介绍泛用性更好的最小表示。我们用长度
那么下面两组编码方式表示的是相同的状态:
0 3 1 0 1 3
0 1 2 0 2 1
我们将相同的状态都映射成字典序最小表示,例如在上例中的 0 1 2 0 2 1
就是一组最小表示。
我们用 b[]
数组表示轮廓线上插头的状态。bb[]
表示在最小表示的编码的过程中,每个数字被映射到的最小数字。注意
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | int b[M + 1], bb[M + 1];
int encode() {
int s = 0;
memset(bb, -1, sizeof(bb));
int bn = 1;
bb[0] = 0;
for (int i = m; i >= 0; --i) {
#define bi bb[b[i]]
if (!~bi) bi = bn++;
s <<= offset;
s |= bi;
}
return s;
}
void decode(int s) {
REP(i, m + 1) {
b[i] = s & mask;
s >>= offset;
}
}
|
我们注意到插头总是成对出现,成对消失的。因而 0 1 2 0 1 2
这样的状态是不合法的。合法的状态构成一组括号序列,实际中合法状态可能是非常稀疏的。
手写哈希¶
在一些 状压 DP 的问题中,合法的状态可能是稀疏的(例如本题),为了优化时空复杂度,我们可以使用哈希表存储合法的 DP 状态。对于 C++ 选手,我们可以使用 std::unordered_map,当然也可以直接手写,这样可以灵活的将状态转移函数也封装于其中。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const int MaxSZ = 16796, Prime = 9973;
struct hashTable {
int head[Prime], next[MaxSZ], sz;
int state[MaxSZ];
long long key[MaxSZ];
inline void clear() {
sz = 0;
memset(head, -1, sizeof(head));
}
inline void push(int s) {
int x = s % Prime;
for (int i = head[x]; ~i; i = next[i]) {
if (state[i] == s) {
key[i] += d;
return;
}
}
state[sz] = s, key[sz] = d;
next[sz] = head[x];
head[x] = sz++;
}
void roll() { REP(i, sz) state[i] <<= offset; }
} H[2], *H0, *H1;
|
上面的代码中:
MaxSZ
表示合法状态的上界,可以估计,也可以预处理出较为精确的值。Prime
一个小于MaxSZ
的大素数。head[]
表头节点的指针。next[]
后续状态的指针。state[]
节点的状态。key[]
节点的关键字,在本题中是方案数。clear()
初始化函数,和手写邻接表类似,我们只需要初始化表头节点的指针。push()
状态转移函数,其中d
是一个全局变量(偷懒),表示每次状态转移所带来的增量。如果找到的话就+=
,否则就创建一个状态为s
,关键字为d
的新节点。roll()
迭代完一整行之后,滚动轮廓线。
关于哈希表的复杂度分析,以及开哈希和闭哈希的不同,可以参见 《算法导论》 中关于散列表的相关章节。
状态转移讨论¶
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | REP(ii, H0->sz) {
decode(H0->state[ii]); // 取出状态,并解码
d = H0->key[ii]; // 得到增量 delta
int lt = b[j], up = b[j + 1]; // 左插头,上插头
bool dn = i != n - 1, rt = j != m - 1; // 下插头,右插头
if (lt && up) { // 如果左、上均有插头
if (lt == up) { // 来自同一个连通块
if (i == n - 1 &&
j == m - 1) { // 只有在最后一个格子时,才能合并,封闭回路。
push(j, 0, 0);
}
} else { // 否则,必须合并这两个连通块,因为本题中需要回路覆盖
REP(i, m + 1) if (b[i] == lt) b[i] = up;
push(j, 0, 0);
}
} else if (lt || up) { // 如果左、上之中有一个插头
int t = lt | up; // 得到这个插头
if (dn) { // 如果可以向下延伸
push(j, t, 0);
}
if (rt) { // 如果可以向右延伸
push(j, 0, t);
}
} else { // 如果左、上均没有插头
if (dn && rt) { // 生成一对新插头
push(j, m, m);
}
}
}
|
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | #include <bits/stdc++.h>
using namespace std;
const int M = 10;
const int offset = 3, mask = (1 << offset) - 1;
int n, m;
long long ans, d;
const int MaxSZ = 16796, Prime = 9973;
struct hashTable {
int head[Prime], next[MaxSZ], sz;
int state[MaxSZ];
long long key[MaxSZ];
inline void clear() {
sz = 0;
memset(head, -1, sizeof(head));
}
inline void push(int s) {
int x = s % Prime;
for (int i = head[x]; ~i; i = next[i]) {
if (state[i] == s) {
key[i] += d;
return;
}
}
state[sz] = s, key[sz] = d;
next[sz] = head[x];
head[x] = sz++;
}
void roll() {
for (int i = 0; i < sz; i++) state[i] <<= offset;
}
} H[2], *H0, *H1;
int b[M + 1], bb[M + 1];
int encode() {
int s = 0;
memset(bb, -1, sizeof(bb));
int bn = 1;
bb[0] = 0;
for (int i = m; i >= 0; --i) {
if (!~bb[b[i]]) bb[b[i]] = bn++;
s <<= offset;
s |= bb[b[i]];
}
return s;
}
void decode(int s) {
for (int i = 0; i < m + 1; i++) {
b[i] = s & mask;
s >>= offset;
}
}
void push(int j, int dn, int rt) {
b[j] = dn;
b[j + 1] = rt;
H1->push(encode());
}
int main() {
cin >> n >> m;
if (m > n) swap(n, m);
H0 = H, H1 = H + 1;
H1->clear();
d = 1;
H1->push(0);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
swap(H0, H1);
H1->clear();
for (int ii = 0; ii < (H0->sz); ii++) {
decode(H0->state[ii]);
d = H0->key[ii];
int lt = b[j], up = b[j + 1];
bool dn = i != n - 1, rt = j != m - 1;
if (lt && up) {
if (lt == up) {
if (i == n - 1 && j == m - 1) {
push(j, 0, 0);
}
} else {
for (int i = 0; i < m + 1; i++)
if (b[i] == lt) b[i] = up;
push(j, 0, 0);
}
} else if (lt || up) {
int t = lt | up;
if (dn) {
push(j, t, 0);
}
if (rt) {
push(j, 0, t);
}
} else {
if (dn && rt) {
push(j, m, m);
}
}
}
}
H1->roll();
}
assert(H1->sz <= 1);
cout << (H1->sz == 1 ? H1->key[0] : 0) << endl;
}
|
习题¶
习题 「Ural 1519」Formula 1
题目大意:有障碍。
习题 「USACO 5.4.4」Betsy's Tours
题目大意:一个
习题 「POJ 1739」Tony's Tour
题目大意:著名的男人八题系列之一。解法同上。
习题 「USACO 6.1.1」Postal Vans
题目大意:
习题 「ProjectEuler 393」Migrating ants
题目大意:对于每一个有
一条路径¶
例题「ZOJ 3213」Beautiful Meadow¶
例题 「ZOJ 3213」Beautiful Meadow
题目大意:一个
本题是标准的一条路径问题,在一条路径问题中,编码的状态中还会存在不能配对的独立插头。需要在状态转移函数中,额外讨论独立插头的生成、合并与消失的情况。独立插头的生成和消失对应着路径的一端,因而这类事件不会发生超过两次(一次生成一次消失,或者两次生成一次合并),否则最终结果一定会出现多个连通块。
我们需要在状态中额外记录这类事件发生的总次数,可以将这个信息编码进状态里(注意,类似这样的额外信息在调整轮廓线的时候,不需要跟着滚动),当然也可以在 hashTable
数组的外面加维。下面的范例程序中我们选择后者。
状态转移¶
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | REP(i, n) {
REP(j, m) {
checkMax(ans, A[i][j]); // 需要单独处理一个格子的情况
if (!A[i][j]) continue; // 如果有障碍,则跳过,注意这时状态数组不需要滚动
swap(H0, H1);
REP(c, 3)
H1[c].clear(); // c 表示生成和消失事件发生的总次数,最多不超过 2 次
REP(c, 3) REP(ii, H0[c].sz) {
decode(H0[c].state[ii]);
d = H0[c].key[ii] + A[i][j];
int lt = b[j], up = b[j + 1];
bool dn = A[i + 1][j], rt = A[i][j + 1];
if (lt && up) {
if (lt == up) { // 在一条路径问题中,我们不能合并相同的插头。
// Cannot deploy here...
} else { // 有可能参与合并的两者中有独立插头,但是也可以用同样的代码片段处理
REP(i, m + 1) if (b[i] == lt) b[i] = up;
push(c, j, 0, 0);
}
} else if (lt || up) {
int t = lt | up;
if (dn) {
push(c, j, t, 0);
}
if (rt) {
push(c, j, 0, t);
}
// 一个插头消失的情况,如果是独立插头则意味着消失,如果是成对出现的插头则相当于生成了一个独立插头,
// 无论哪一类事件都需要将 c + 1。
if (c < 2) {
push(c + 1, j, 0, 0);
}
} else {
d -= A[i][j];
H1[c].push(H0[c].state[ii]);
d += A[i][j]; // 跳过插头生成,本题中不要求全部覆盖
if (dn && rt) { // 生成一对插头
push(c, j, m, m);
}
if (c < 2) { // 生成一个独立插头
if (dn) {
push(c + 1, j, m, 0);
}
if (rt) {
push(c + 1, j, 0, m);
}
}
}
}
}
REP(c, 3) H1[c].roll(); // 一行结束,调整轮廓线
}
|
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | #include <bits/stdc++.h>
using namespace std;
template <class T>
inline bool checkMax(T &a, const T b) {
return a < b ? a = b, 1 : 0;
}
const int N = 8, M = 8;
const int offset = 3, mask = (1 << offset) - 1;
int A[N + 1][M + 1];
int n, m;
int ans, d;
const int MaxSZ = 16796, Prime = 9973;
struct hashTable {
int head[Prime], next[MaxSZ], sz;
int state[MaxSZ];
int key[MaxSZ];
inline void clear() {
sz = 0;
memset(head, -1, sizeof(head));
}
inline void push(int s) {
int x = s % Prime;
for (int i = head[x]; ~i; i = next[i]) {
if (state[i] == s) {
checkMax(key[i], d);
return;
}
}
state[sz] = s, key[sz] = d;
next[sz] = head[x];
head[x] = sz++;
}
void roll() {
for (int i = 0; i < sz; i++) state[i] <<= offset;
}
} H[2][3], *H0, *H1;
int b[M + 1], bb[M + 1];
int encode() {
int s = 0;
memset(bb, -1, sizeof(bb));
int bn = 1;
bb[0] = 0;
for (int i = m; i >= 0; --i) {
if (!~bb[b[i]]) bb[b[i]] = bn++;
s <<= offset;
s |= bb[b[i]];
}
return s;
}
void decode(int s) {
for (int i = 0; i < m + 1; i++) {
b[i] = s & mask;
s >>= offset;
}
}
void push(int c, int j, int dn, int rt) {
b[j] = dn;
b[j + 1] = rt;
H1[c].push(encode());
}
void init() {
cin >> n >> m;
H0 = H[0], H1 = H[1];
for (int c = 0; c < 3; c++) H1[c].clear();
d = 0;
H1[0].push(0);
memset(A, 0, sizeof(A));
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) cin >> A[i][j];
}
void solve() {
ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
checkMax(ans, A[i][j]); // 需要单独处理一个格子的情况
if (!A[i][j]) continue; // 如果有障碍,则跳过,注意这时状态数组不需要滚动
swap(H0, H1);
for (int c = 0; c < 3; c++)
H1[c].clear(); // c 表示生成和消失事件发生的总次数,最多不超过 2 次
for (int c = 0; c < 3; c++)
for (int ii = 0; ii < H0[c].sz; ii++) {
decode(H0[c].state[ii]);
d = H0[c].key[ii] + A[i][j];
int lt = b[j], up = b[j + 1];
bool dn = A[i + 1][j], rt = A[i][j + 1];
if (lt && up) {
if (lt == up) { // 在一条路径问题中,我们不能合并相同的插头。
// Cannot deploy here...
} else { // 有可能参与合并的两者中有独立插头,但是也可以用同样的代码片段处理
for (int i = 0; i < m + 1; i++)
if (b[i] == lt) b[i] = up;
push(c, j, 0, 0);
}
} else if (lt || up) {
int t = lt | up;
if (dn) {
push(c, j, t, 0);
}
if (rt) {
push(c, j, 0, t);
}
// 一个插头消失的情况,如果是独立插头则意味着消失,如果是成对出现的插头则相当于生成了一个独立插头,
// 无论哪一类事件都需要将 c + 1。
if (c < 2) {
push(c + 1, j, 0, 0);
}
} else {
d -= A[i][j];
H1[c].push(H0[c].state[ii]);
d += A[i][j]; // 跳过插头生成,本题中不要求全部覆盖
if (dn && rt) { // 生成一对插头
push(c, j, m, m);
}
if (c < 2) { // 生成一个独立插头
if (dn) {
push(c + 1, j, m, 0);
}
if (rt) {
push(c + 1, j, 0, m);
}
}
}
}
}
for (int c = 0; c < 3; c++) H1[c].roll(); // 一行结束,调整轮廓线
}
for (int ii = 0; ii < H1[2].sz; ii++) checkMax(ans, H1[2].key[ii]);
cout << ans << endl;
}
int main() {
int T;
cin >> T;
while (T--) {
init();
solve();
}
}
|
习题¶
习题 「NOI 2010 Day2」旅行路线
题目大意:
- 第 i 个参观的格点 (x, y),满足 T[x][y]= L[i]
- 路径的一端在棋盘的边界上
求可行的方案数
染色模型¶
除了路径模型之外,还有一类常见的模型,需要我们对棋盘进行染色,相邻的相同颜色节点被视为连通。在路径类问题中,状态转移的时候我们枚举当前路径的方向,而在染色类问题中,我们枚举当前节点染何种颜色。在染色模型中,状态中处在相同连通性的节点可能不止两个。但总体来说依然大同小异。我们不妨来看一个经典的例题。
例题「UVA 10572」Black & White¶
例题 「UVA 10572」Black & White
题目大意:在
状态编码¶
我们先考虑状态编码。不考虑连通性,那么就是 SGU 197. Nice Patterns Strike Back,不难用 状压 DP 直接解决。现在我们需要在状态中同时体现颜色和连通性的信息,考察轮廓线上每个位置的状态,二进制的每 Offset
位描述轮廓线上的一个位置,因为只有黑白两种颜色,我们用最低位的奇偶性表示颜色,其余部分示连通性。
考虑第一行上面的节点,和第一列左侧节点,如果要避免特判的话,可以考虑引入第三种颜色区分它们,这里我们观察到这些边界状态的连通性信息一定为 0,所以不需要对第三种颜色再进行额外编码。
在路径问题中我们的轮廓线是由
这样的编码方案中依然保留了很多冗余信息,(连通的区域颜色一定相同,且左上角的格子只需要颜色信息不需要连通性),但是因为已经用了哈希表和最小表示,对时间复杂度的影响不大,为了降低编程压力,就不再细化了。
在最多情况下(例如第一行黑白相间),每个插头的连通性信息都不一样,因此我们需要 Offset
为
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | const int Offset = 5, Mask = (1 << Offset) - 1;
int c[N + 2];
int b[N + 2], bb[N + 3];
T_state encode() {
T_state s = 0;
memset(bb, -1, sizeof(bb));
int bn = 1;
bb[0] = 0;
for (int i = m; i >= 0; --i) {
#define bi bb[b[i]]
if (!~bi) bi = bn++;
s <<= Offset;
s |= (bi << 1) | c[i];
}
return s;
}
void decode(T_state s) {
REP(i, m + 1) {
b[i] = s & Mask;
c[i] = b[i] & 1;
b[i] >>= 1;
s >>= Offset;
}
}
|
手写哈希¶
因为需要构造任意一组方案,这里的哈希表我们需要添加一组域 pre[]
来记录每个状态在上一阶段的任意一个前驱。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | const int Prime = 9979, MaxSZ = 1 << 20;
template <class T_state, class T_key>
struct hashTable {
int head[Prime];
int next[MaxSZ], sz;
T_state state[MaxSZ];
T_key key[MaxSZ];
int pre[MaxSZ];
void clear() {
sz = 0;
memset(head, -1, sizeof(head));
}
void push(T_state s, T_key d, T_state u) {
int x = s % Prime;
for (int i = head[x]; ~i; i = next[i]) {
if (state[i] == s) {
key[i] += d;
return;
}
}
state[sz] = s, key[sz] = d, pre[sz] = u;
next[sz] = head[x], head[x] = sz++;
}
void roll() { REP(ii, sz) state[ii] <<= Offset; }
};
hashTable<T_state, T_key> _H, H[N][N], *H0, *H1;
|
方案构造¶
有了上面的信息,我们就可以容易的构造方案了。首先遍历当前哈希表中的状态,如果连通块数目不超过 pre
数组构造出方案,注意每一行的末尾因为我们执行了 Roll()
操作,颜色需要取 c[j+1]
。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void print() {
T_key z = 0;
int u;
REP(i, H1->sz) {
decode(H1->state[i]);
if (*max_element(b + 1, b + m + 1) <= 2) {
z += H1->key[i];
u = i;
}
}
cout << z << endl;
if (z) {
DWN(i, n, 0) {
B[i][m] = 0;
DWN(j, m, 0) {
decode(H[i][j].state[u]);
int cc = j == m - 1 ? c[j + 1] : c[j];
B[i][j] = cc ? 'o' : '#';
u = H[i][j].pre[u];
}
}
REP(i, n) puts(B[i]);
}
puts("");
}
|
状态转移¶
我们记:
cc
当前正在染色的格子的颜色lf
左边格子的颜色up
上边格子的颜色lu
左上格子的颜色
我们用
状态转移-代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void trans(int i, int j, int u, int cc) {
decode(H0->state[u]);
int lf = j ? c[j - 1] : -1, lu = b[j] ? c[j] : -1,
up = b[j + 1] ? c[j + 1] : -1; // 没有颜色也是颜色的一种!
if (lf == cc && up == cc) { // 合并
if (lu == cc) return; // 2x2 子矩形相同的情况
int lf_b = b[j - 1], up_b = b[j + 1];
REP(i, m + 1) if (b[i] == up_b) { b[i] = lf_b; }
b[j] = lf_b;
} else if (lf == cc || up == cc) { // 继承
if (lf == cc)
b[j] = b[j - 1];
else
b[j] = b[j + 1];
} else { // 生成
if (i == n - 1 && j == m - 1 && lu == cc) return; // 特判
b[j] = m + 2;
}
c[j] = cc;
if (!ok(i, j, cc)) return; // 判断是否会因生成封闭的连通块导致不合法
H1->push(encode(), H0->key[u], u);
}
|
对于最后一种情况需要注意的是,如果已经生成了一个封闭的连通区域,那么我们不能再使用她的颜色染色,否则这种颜色会出现两个连通块。我们似乎需要额度记录这种事件,可以参考 「ZOJ 3213」Beautiful Meadow 中的做法,再开一维记录这个事件。不过利用本题的特殊性,我们也可以特判掉。
特判-代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | bool ok(int i, int j, int cc) {
if (cc == c[j + 1]) return true;
int up = b[j + 1];
if (!up) return true;
int c1 = 0, c2 = 0;
REP(i, m + 1) if (i != j + 1) {
if (b[i] == b[j + 1]) { // 连通性相同,颜色一定相同
assert(c[i] == c[j + 1]);
}
if (c[i] == c[j + 1] && b[i] == b[j + 1]) ++c1;
if (c[i] == c[j + 1]) ++c2;
}
if (!c1) { // 如果会生成新的封闭连通块
if (c2) return false; // 如果轮廓线上还有相同的颜色
if (i < n - 1 || j < m - 2) return false;
}
return true;
}
|
进一步讨论连通块消失的情况。每当我们对一个格子进行染色后,如果没有其他格子与其上侧的格子连通,那么会形成一个封闭的连通块。这个事件仅在最后一行的最后两列时可以发生,否则后续为了不出现
1 2 3 | 2 2 o# #o |
我们特判掉这种,这样在本题中,就可以偷懒不用记录之前是否已经生成了封闭的连通块了。
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | #include <bits/stdc++.h>
using namespace std;
typedef long long T_state;
typedef int T_key;
const int N = 8;
int n, m;
char A[N + 1][N + 1], B[N + 1][N + 1];
const int Offset = 5, Mask = (1 << Offset) - 1;
int c[N + 2];
int b[N + 2], bb[N + 3];
T_state encode() {
T_state s = 0;
memset(bb, -1, sizeof(bb));
int bn = 1;
bb[0] = 0;
for (int i = m; i >= 0; --i) {
if (!~bb[b[i]]) bb[b[i]] = bn++;
s <<= Offset;
s |= (bb[b[i]] << 1) | c[i];
}
return s;
}
void decode(T_state s) {
for (int i = 0; i < m + 1; i++) {
b[i] = s & Mask;
c[i] = b[i] & 1;
b[i] >>= 1;
s >>= Offset;
}
}
const int Prime = 9979, MaxSZ = 1 << 20;
template <class T_state, class T_key>
struct hashTable {
int head[Prime];
int next[MaxSZ], sz;
T_state state[MaxSZ];
T_key key[MaxSZ];
int pre[MaxSZ];
void clear() {
sz = 0;
memset(head, -1, sizeof(head));
}
void push(T_state s, T_key d, T_state u) {
int x = s % Prime;
for (int i = head[x]; ~i; i = next[i]) {
if (state[i] == s) {
key[i] += d;
return;
}
}
state[sz] = s, key[sz] = d, pre[sz] = u;
next[sz] = head[x], head[x] = sz++;
}
void roll() {
for (int ii = 0; ii < sz; ii++) state[ii] <<= Offset;
}
};
hashTable<T_state, T_key> _H, H[N][N], *H0, *H1;
bool ok(int i, int j, int cc) {
if (cc == c[j + 1]) return true;
int up = b[j + 1];
if (!up) return true;
int c1 = 0, c2 = 0;
for (int i = 0; i < m + 1; i++)
if (i != j + 1) {
if (b[i] == b[j + 1]) { // 连通性相同,颜色一定相同
assert(c[i] == c[j + 1]);
}
if (c[i] == c[j + 1] && b[i] == b[j + 1]) ++c1;
if (c[i] == c[j + 1]) ++c2;
}
if (!c1) { // 如果会生成新的封闭连通块
if (c2) return false; // 如果轮廓线上还有相同的颜色
if (i < n - 1 || j < m - 2) return false;
}
return true;
}
void trans(int i, int j, int u, int cc) {
decode(H0->state[u]);
int lf = j ? c[j - 1] : -1, lu = b[j] ? c[j] : -1,
up = b[j + 1] ? c[j + 1] : -1; // 没有颜色也是颜色的一种!
if (lf == cc && up == cc) { // 合并
if (lu == cc) return; // 2x2 子矩形相同的情况
int lf_b = b[j - 1], up_b = b[j + 1];
for (int i = 0; i < m + 1; i++)
if (b[i] == up_b) {
b[i] = lf_b;
}
b[j] = lf_b;
} else if (lf == cc || up == cc) { // 继承
if (lf == cc)
b[j] = b[j - 1];
else
b[j] = b[j + 1];
} else { // 生成
if (i == n - 1 && j == m - 1 && lu == cc) return; // 特判
b[j] = m + 2;
}
c[j] = cc;
if (!ok(i, j, cc)) return; // 判断是否会因生成封闭的连通块导致不合法
H1->push(encode(), H0->key[u], u);
}
void init() {
cin >> n >> m;
for (int i = 0; i < n; i++) scanf("%s", A[i]);
}
void solve() {
H1 = &_H, H1->clear(), H1->push(0, 1, 0);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
H0 = H1, H1 = &H[i][j], H1->clear();
for (int u = 0; u < H0->sz; u++) {
if (A[i][j] == '.' || A[i][j] == '#') trans(i, j, u, 0);
if (A[i][j] == '.' || A[i][j] == 'o') trans(i, j, u, 1);
}
}
H1->roll();
}
}
void print() {
T_key z = 0;
int u;
for (int i = 0; i < H1->sz; i++) {
decode(H1->state[i]);
if (*max_element(b + 1, b + m + 1) <= 2) {
z += H1->key[i];
u = i;
}
}
cout << z << endl;
if (z) {
for (int i = n - 1; i >= 0; i--) {
B[i][m] = 0;
for (int j = m - 1; j >= 0; j--) {
decode(H[i][j].state[u]);
int cc = j == m - 1 ? c[j + 1] : c[j];
B[i][j] = cc ? 'o' : '#';
u = H[i][j].pre[u];
}
}
for (int i = 0; i < n; i++) puts(B[i]);
}
puts("");
}
int main() {
int T;
cin >> T;
while (T--) {
init();
solve();
print();
}
}
|
习题 「Topcoder SRM 312. Div1 Hard」CheapestIsland
题目大意:给一个棋盘图,每个格子有权值,求权值之和最小的连通块。
习题 「JLOI 2009」神秘的生物
题目大意:给一个棋盘图,每个格子有权值,求权值之和最大的连通块。
图论模型¶
例题 「NOI 2007 Day2」生成树计数
题目大意:某类特殊图的生成树计数,每个节点恰好与其前
例题 「2015 ACM-ICPC Asia Shenyang Regional Contest - Problem E」Efficient Tree
题目大意:给出一个
要求: 最小生成树的边权和 所有最小生成树的得分之和。 (
实战篇¶
例题「HDU 4113」Construct the Great Wall¶
例题 「HDU 4113」Construct the Great Wall
题目大意:在 x
和 o
。
有一类插头 DP 问题要求我们在棋盘上构造一组墙,以分割棋盘上的某些元素。不妨称之为修墙问题,这类问题既可视作染色模型,也可视作路径模型。
在本题中,如果视作染色模型的话,不仅需要额外讨论染色区域的周长,还要判断在角上触碰而导致不合法的情况(图 2)。另外与 「UVA 10572」Black & White 不同的是,本题中要求围墙为简单多边形,因而对于下面的回字形的情况,在本题中是不合法的。
1 2 3 4 | 3 3 ooo oxo ooo |
因而我们使用路径模型,转化为 一条回路 来处理。
我们沿着棋盘的交叉点进行 DP(因而长宽需要增加 x
在回路之外,o
在回路之内。因此我们还需要维护当前位置是否在回路内部。对于这个信息我们可以加维,也可以直接统计轮廓线上到这个位置之前出现下插头次数的奇偶性(射线法)。
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | #include <bits/stdc++.h>
using namespace std;
#define REP(i, n) for (int i = 0; i < n; ++i)
template <class T>
inline bool checkMin(T &a, const T b) {
return b < a ? a = b, 1 : 0;
}
const int N = 10, M = N;
const int offset = 3, mask = (1 << offset) - 1;
int n, m;
int d;
const int INF = 0x3f3f3f3f;
int b[M + 1], bb[M + 1];
int encode() {
int s = 0;
memset(bb, -1, sizeof(bb));
int bn = 1;
bb[0] = 0;
for (int i = m; i >= 0; --i) {
#define bi bb[b[i]]
if (!~bi) bi = bn++;
s <<= offset;
s |= bi;
}
return s;
}
void decode(int s) {
REP(i, m + 1) {
b[i] = s & mask;
s >>= offset;
}
}
const int MaxSZ = 16796, Prime = 9973;
struct hashTable {
int head[Prime], next[MaxSZ], sz;
int state[MaxSZ];
int key[MaxSZ];
inline void clear() {
sz = 0;
memset(head, -1, sizeof(head));
}
inline void push(int s) {
int x = s % Prime;
for (int i = head[x]; ~i; i = next[i]) {
if (state[i] == s) {
checkMin(key[i], d);
return;
}
}
state[sz] = s, key[sz] = d;
next[sz] = head[x];
head[x] = sz++;
}
void roll() { REP(i, sz) state[i] <<= offset; }
} H[2], *H0, *H1;
char A[N + 1][M + 1];
void push(int i, int j, int dn, int rt) {
b[j] = dn;
b[j + 1] = rt;
if (A[i][j] != '.') {
bool bad = A[i][j] == 'o';
REP(jj, j + 1) if (b[jj]) bad ^= 1;
if (bad) return;
}
H1->push(encode());
}
int solve() {
cin >> n >> m;
int ti, tj;
REP(i, n) {
scanf("%s", A[i]);
REP(j, m) if (A[i][j] == 'o') ti = i, tj = j;
A[i][m] = '.';
}
REP(j, m + 1) A[n][j] = '.';
++n, ++m, ++ti, ++tj;
H0 = H, H1 = H + 1;
H1->clear();
d = 0;
H1->push(0);
int z = INF;
REP(i, n) {
REP(j, m) {
swap(H0, H1);
H1->clear();
REP(ii, H0->sz) {
decode(H0->state[ii]);
d = H0->key[ii] + 1;
int lt = b[j], up = b[j + 1];
bool dn = i != n - 1, rt = j != m - 1;
if (lt && up) {
if (lt == up) {
int cnt = 0;
REP(i, m + 1) if (b[i])++ cnt;
if (cnt == 2 && i == ti && j == tj) {
checkMin(z, d);
}
} else {
REP(i, m + 1) if (b[i] == lt) b[i] = up;
push(i, j, 0, 0);
}
} else if (lt || up) {
int t = lt | up;
if (dn) {
push(i, j, t, 0);
}
if (rt) {
push(i, j, 0, t);
}
} else {
--d;
push(i, j, 0, 0);
++d;
if (dn && rt) {
push(i, j, m, m);
}
}
}
}
H1->roll();
}
if (z == INF) z = -1;
return z;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
#endif
int T;
cin >> T;
for (int Case = 1; Case <= T; ++Case) {
printf("Case #%d: %d\n", Case, solve());
}
}
|
习题 「HDU 4796」Winter's Coming
题目大意:在
习题 「ZOJ 2125」Rocket Mania
题目大意:9*6 的地图上每个格子里是一种管道(-,T,L,+ 型或没有),可以把管道旋转 0°,90°,180°,270°, 问地图最多能有几行的右边界与第 X 行的左边界通过管道相连。
习题 「ZOJ 2126」Rocket Mania Plus
题目大意:9*6 的地图上每个格子里是一种管道(-,T,L,+ 型或没有),可以把管道旋转 0°,90°,180°,270°, 问地图最多能有几行的右边界与左边界通过管道相连。
习题 「World Finals 2009/2010 Harbin」Channel
题目大意:一张方格地图上用'.'表示空地、'#'表示石头,找到最长的一条路径满足: 1、起点在左上角,终点在右下角。 2、不能经过石头 3、路径自身不能在八连通的意义下成环。(即包括拐角处也不能接触)
习题 「HDU 3958」Tower Defence
题目大意:可以转化为求解一条从
习题 「UVA 10531」Maze Statistics
题目大意:有一个
习题 「AIZU 2452」Pipeline Plans
题目大意:现有一共 12 种图案的瓷砖,每种瓷砖数量给定。要求铺到一块可视为
习题 「SDOI 2014」电路板
题目大意:一块
习题 「SPOJ CAKE3」Delicious Cake
题目大意:一块可视为
本章注记¶
插头 DP 问题通常编码难度较大,讨论复杂,因而属于 OI/ACM 中相对较为 偏门的领域。这方面最为经典的资料,当属 2008 年 陈丹琦 的集训队论文——基于连通性状态压缩的动态规划问题。其次,HDU 的 notonlysuccess 2011 年曾经在博客中连续写过两篇由浅入深的专题,也是不可多得的好资料,不过现在需要在 Web Archive 里考古。
多米诺骨牌覆盖¶
「HDU 1400」Mondriaan’s Dream 也出现在 《算法竞赛入门经典训练指南》 中,并作为《轮廓线上的动态规划》一节的例题。多米诺骨牌覆盖(Domino tiling) 是一组非常经典的数学问题,稍微修改其数据范围就可以得到不同难度,需要应用不同的算法解决的子问题。
当限定
当
当
- 「51nod 1031」骨牌覆盖
- 「51nod 1033」骨牌覆盖 V2|「Vijos 1194」Domino
- 「51nod 1034」骨牌覆盖 V3|「Ural 1594」Aztec Treasure
- Wolfram MathWorld, Chebyshev Polynomial of the Second Kind
一条路径¶
「一条路径」是 哈密度路径(Hamiltonian Path) 问题在 格点图(Grid Graph) 中的一种特殊情况。哈密顿路径的判定性问题是 NP-complete 家族中的重要成员。Niconico 上有一个『フカシギの数え方』おねえさんといっしょ!みんなで数えてみよう(和大姐姐一起学习计算系列)的科普向视频,就使用这个问题作为例子,来说明 NPC 问题的计算时间如何随着问题的规模的线性增长而指数增长。
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:OI-wiki
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用