轮廓线动态规划(插头DP) \, 能解决的一类特殊的问题的共同点是:在一个比较“窄”(行数少或者列数少)的棋盘上进行复杂操作。轮廓线,是采用多阶段决策DP解决这类问题时,已决策状态和未决策状态的分界线。尽管轮廓线的形态复杂,但棋盘比较窄,状态总数仍然在可以接受的范围内。
若棋盘的宽度为m,则轮廓线的长度为m+1,解决具体问题时通常有将轮廓线上方格子作为状态位或者将轮廓线上的边作为状态位两种不同方式,每个状态位为2进制、3进制、8进制等限定下的数值。有时候为了便于取特定位,会对所有状态进行移位(称为 滚动操作,通常是向左移位)变成特定排列顺序(比如左侧始终是最低位,上方始终是最高位)。在处理连通分量相关的问题时,会用到 最小表示法或者括号表示法来进行状态压缩。还有一些问题棋盘虽然窄,但是非常长,这时候可以将状态转移写成矩阵形式,借助快速矩阵幂来优化时间复杂度。
接下来将插头DP能解决的问题进行分类并结合实际题目给出算法实现。
覆盖模型
骨牌覆盖是插头DP入门的经典问题,很多覆盖问题都可以参照骨牌覆盖的解决思路。
UVa11270 Tiling Dominoes
用 1×2 骨牌覆盖n×m 棋盘(nm≤100),有多少种方法?下图为 m=n=6 时的一种铺放方法。

骨牌覆盖以轮廓线上方格子作为状态位,以当前格子为右下角进行三种决策:不放骨牌、放竖的骨牌、放横的骨牌。

/* UVa11270 铺放骨牌 */
#include <iostream>
#include <cstring>
using namespace std;
#define N 10
long long d[2][1<<N]; int m, n;
long long solve() {
if (m*n & 1) return 0;
if (m < n) swap(m, n);
int t = 1<<n;
memset(d[0], 0, sizeof(d[0])); d[0][t-1] = 1;
for (int i=0, c=1; i<m; ++i) for (int j=0; j<n; ++j, c^=1) {
memset(d[c], 0, sizeof(d[c]));
for (int k=0; k<t; ++k) if (d[c^1][k]) {
d[c][k ^ 1<<j] += d[c^1][k];
if (j && (k & 3<<j-1) == (1<<j)) d[c][k | 1<<j-1] += d[c^1][k];
}
}
return d[m*n & 1][t-1];
}
int main() {
while (cin >> m >> n) cout << solve() << endl;
return 0;
}
UVa1408/LA4018 Flight Control
有一个N行M列的数组(1 ≤ N ≤ 50, 1 ≤ M ≤ 9)记录机场各个航班的飞行传感数据,其每个元素都是整数。如果某元素小于等于0,则其一定不是航班的飞行数据。如果某个元素大于0,则其可能是一个航班的飞行数据,也可能和所在行(或列)连续严格递增(或严格递减)的子序列一起构成一个航班的飞行数据。不同航班的传感数据不能重叠,求数组最少代表的航班数。
按照覆盖模型,用3进制作为状态位:未定(代表当前位置为新航班)、水平、竖直。可以结合记忆化搜索优化时间。
UVa11741 Ignore the Blocks
在一个 R ×C 的网格中有 n 个黑格,其余均为白格。要求用 1×2 的骨牌覆盖所有白格(每个白格恰好被一块骨牌覆盖,且所有黑格均没有被覆盖),计算有多少种方案,并输出方案总数除以 10000007 后的余数。其中,1≤R≤4,1≤C≤100000000, 0≤N≤100。
骨牌覆盖的升级版:1、有障碍格;2:C可达
1
0
8
10^8
108规模,需要借助快速矩阵幂加速。要构造三种状态转移矩阵:障碍格的矩阵,两种非障碍格的矩阵(需要区分是否为列首)。
多条回路
HDU 1693 Eat the Trees
求用若干条回路覆盖N行M列棋盘的方案数,有些位置有障碍(1<=N, M<=11)。

多条回路问题仍然可以基于覆盖模型来做(每个非障碍格恰好覆盖2次),用3进制作为状态位,以当前格子为左上角进行四种决策:已经覆盖2次,不再铺放骨牌;覆盖了1次,往下铺放骨牌;覆盖了1次,往右铺放骨牌;覆盖了0次,往下和右都铺放骨牌。状态转移次数为
3
m
×
m
n
3^m\times mn
3m×mn。
如果将轮廓线上的边作为状态位,则是2进制,状态转移次数为
2
m
+
1
×
m
n
2^{m+1}\times mn
2m+1×mn,效率更优。根据当前格子的左和上的取值left、up进行四种决策:left=1且up=1,不再铺放骨牌;left+up=1,往下铺放骨牌;left+up=1,往右铺放骨牌;left=0且up=0,往下和右都铺放骨牌。

要注意一点,每行处理完后,要进行滚动操作(向左移位)来调整轮廓线形状。
/* HDU 1693 Eat the Trees */
#include <iostream>
#include <cstring>
using namespace std;
#define N 12
long long d[2][1<<N]; int m, n;
long long solve() {
cin >> n >> m;
memset(d[0], 0, sizeof(d[0])); d[0][0] = 1;
for (int i=0, c=1; i<n; ++i) {
for (int j=0, b; j<m; ++j, c^=1) {
memset(d[c], 0, sizeof(d[c])); cin >> b;
for (int k=0, t=1<<m+1; k<t; ++k) if (d[c^1][k]) {
int l = k & 1<<j, f = k & 2<<j;
if (b) {
if (l ^ f>>1) d[c][k^l^f^1<<j] += d[c^1][k], d[c][k^l^f^2<<j] += d[c^1][k];
else d[c][k^3<<j] += d[c^1][k];
} else if (!l && !f) d[c][k] += d[c^1][k];
}
}
for (int j=(1<<m)-1; j>=0; --j) d[c^1][j<<1] = d[c^1][j], d[c^1][j<<1 | 1] = 0;
}
return d[m*n&1][0];
}
int main() {
int t; cin >> t;
for (int k=1; k<=t; ++k) cout << "Case " << k << ": There are " << solve() << " ways to eat the trees." << endl;
return 0;
}
一条回路(哈密顿圈)
URAL 1519 Formula 1
求用一条回路覆盖N行M列棋盘的方案数,有些位置有障碍(2<=N, M<=12)。

和多条回路不同的是,一条回路还需要区分插头之间的连通性,除最后一个非障碍格外,不得提前出现回路。可以用最小表示法或者括号表示法来对连通信息进行状态编码。在处理简单路径问题(即除了起点和终点可能相同外,其余顶点均不相同的路径)时,括号表示法时间效率更优,实现也更简单。
最小表示法处理一条回路问题:将轮廓线上的边看成插头,无插头则此位取值为0,有插头则此位取值为其连通标号。要注意在状态转移的过程中,除了最后一个非障碍格所在行外,任何一个连通分量内要保证至少有一个格子有下插头。
括号表示法处理一条回路问题,引用自2008年陈丹琦的集训队论文:《基于连通性状态压缩的动态规划问题》
括号表示法用3进制表示轮廓线上的插头:0表示无插头,1表示左括号插头,2表示右括号插头。不妨用#表示无插头,那么上面的三幅图分别对应的是(())#(),(()#)(),(()###),即 ( 1122012 ) 3 , ( 1120212 ) 3 , ( 1120002 ) 3 (1122012)_3,(1120212)_3,(1120002)_3 (1122012)3,(1120212)3,(1120002)3。
记(i,j-1)的右插头为p,(i-1, j)的下插头为q,(i, j)的下插头为p’,(i, j)的右插头为q’,那么每次状态转移相当于轮廓线上插头p的信息修改成p’的信息,插头q的信息修改成q’的信息,设 W ( p ) = 0 , 1 , 2 W(p) = 0, 1, 2 W(p)=0,1,2表示插头x的状态.
分三类情况来讨论状态转移:
- 1 新建一个连通分量,这种情况下 W ( p ) = 0 , W ( q ) = 0 W(p) = 0,W(q) = 0 W(p)=0,W(q)=0,p’和q’两个插头构建了一条新的路径,相当于p’为左括号,q’为右括号,即W(p’) ← 1,W(q’) ← 2。
- 2 保持原来的连通分量, W ( p ) 和 W ( q ) W(p)和W(q) W(p)和W(q)中恰好一个为0,p’和q’中也恰好一个为0。那么无论p’和q’中哪个插头存在,都相当于是p, q中那个存在的插头的延续,括号性质一样,因此 W ( p ′ ) ← W ( p ) + W ( q ) , W ( q ′ ) ← 0 W(p') ← W(p) + W(q),W(q') ← 0 W(p′)←W(p)+W(q),W(q′)←0或者 W ( q ′ ) ← W ( p ) + W ( q ) , W ( p ′ ) ← 0 W(q') ← W(p) + W(q),W(p') ← 0 W(q′)←W(p)+W(q),W(p′)←0。
- 3 合并两个连通分量,这种情况下 W ( p ) > 0 , W ( q ) > 0 , W ( p ′ ) ← 0 , W ( q ′ ) ← 0 W(p) > 0,W(q) > 0,W(p') ← 0,W(q') ← 0 W(p)>0,W(q)>0,W(p′)←0,W(q′)←0,根据p, q为左括号还是右括号分四类情况讨论:
3.1 W ( p ) = 1 , W ( q ) = 1 W(p) = 1,W(q) = 1 W(p)=1,W(q)=1,那么需要将q这个左括号与之对应的右括号v修改成左括号,即W(v) ← 1。
3.2 W ( p ) = 2 , W ( q ) = 2 W(p) = 2,W(q) = 2 W(p)=2,W(q)=2,那么需要将p这个右括号与之对应的左括号v修改成右括号,即W(v) ← 2。
3.3 W ( p ) = 1 , W ( q ) = 2 W(p) = 1,W(q) = 2 W(p)=1,W(q)=2,那么p和q是相对应的左括号和右括号,连接p, q相当于将一条路径的两端连接起来形成一个回路,这种情况下只能出现在最后一个非障碍格子
3.4 W ( p ) = 2 , W ( q ) = 1 W(p) = 2,W(q) = 1 W(p)=2,W(q)=1,那么p和q连接起来后,p对应的左括号和q对应的右括号恰好匹配,不需要修改其他的插头的状态。
/* URAL 1519 Formula 1 */
#include <iostream>
#include <cstring>
using namespace std;
#define M 14001 // 13943
#define N 13
long long d[2][M]; int hs[M], st[2][M], cc[2], m, n; char s[N][N];
int r_t_l(int k, int i) {
for (int c=1, j=m<<1; i<=j; i+=2) {
int t = k>>i & 3;
if (t == 1) ++c;
else if (t == 2 && --c == 0) return k ^ 3<<i;
}
return k;
}
int l_t_r(int k, int i) {
for (int c=1; i>=0; i-=2) {
int t = k>>i & 3;
if (t == 2) ++c;
else if (t == 1 && --c == 0) return k ^ 3<<i;
}
return k;
}
void update(int s, long long v, int t) {
int x = s % M;
while (hs[x] >= 0 && st[t][hs[x]] != s) x = (x+1) % M;
hs[x] < 0 ? (d[t][cc[t]] = v, st[t][cc[t]] = s, hs[x] = cc[t]++) : d[t][hs[x]] += v;
}
long long solve() {
int x = 0, y = 0; long long ans = 0;
for (int i=0; i<n; ++i) {
cin >> s[i];
for (int j=0; j<m; ++j) if (s[i][j] == '.') x = i, y = j;
}
st[0][0] = 0; d[0][0] = cc[0] = 1;
for (int i=0, c=1; i<n; ++i) {
for (int j=0, b=0; j<m; ++j, b+=2, c^=1) {
memset(hs, -1, sizeof(hs)); cc[c] = 0;
for (int a=0; a<cc[c^1]; ++a) {
long long e = d[c^1][a]; int k = st[c^1][a], l = k>>b & 3, t = k>>b>>2 & 3;
if (s[i][j] == '.') {
if (!l && !t) {
if (i+1 < n && j+1 < m) update(k | 9<<b, e, c);
} else if (!l || !t) {
int w = k ^ (t<<2)+l<<b; l += t;
if (i+1 < n) update(w | l<<b, e, c);
if (j+1 < m) update(w | l<<b<<2, e, c);
} else if (l == 1 && t == 2) {
if (i == x && j == y && k == (9<<b)) ans += e;
} else if (l == 2 && t == 1) update(k ^ 6<<b, e, c);
else if (t == 1) update(r_t_l(k ^ 5<<b, b+4), e, c);
else update(l_t_r(k ^ 10<<b, b-2), e, c);
} else if (!l && !t) update(k, e, c);
}
if (i == x && j == y) return ans;
}
for (int j=0; j<cc[c^1]; ++j) st[c^1][j] <<= 2;
}
return 0;
}
int main() {
while (cin >> n >> m) cout << solve() << endl;
return 0;
}
UVa1501/LA5762 Construct the Great Wall
给定一个H 行W 列(1≤H,W≤8)的网格,用一个边沿着网格线的简单多边形围住所有的O,但不能围住任何一个X。要求长度最短。左下图所示是合法解,右下图所示是非法解(简单多边形的非相邻边不能有公共点)。

本题难点是在给定轮廓线状态下要判断当前格子处于简单多边形的内部还是外部,但其实算法如果是用括号表示法进行状态编码,则判定内外非常容易。
一个坑点:题面明确交代1≤H,W≤8,但官方测试数据有H或W=9的数据。
一条路径
一条路径问题比一条回路问题多出来一种插头:独立插头。需要将原来的3进制状态修改成4进制:0表示无插头,1表示左括号插头,2表示右括号插头,3表示独立插头。
除了处理一条回路问题的那些状态转移情况外,还多出与独立插头有关的转移。以下引用自2008年陈丹琦的集训队论文:《基于连通性状态压缩的动态规划问题》
- 1 W ( p ) = 0 , W ( q ) = 0 W(p) = 0,W(q) = 0 W(p)=0,W(q)=0。当前格子可能成为路径的一端,即右插头或下插头是独立插头,因此W(p’) ← 3,W(q’) ← 0或者W(q’) ← 3,W(p’) ← 0。
- 2 W ( p ) > 0 , W ( q ) > 0 W(p) > 0,W(q) > 0 W(p)>0,W(q)>0,那么W(p’) ← 0,W(q’) ← 0。
2.1 W ( p ) = 3 , W ( q ) = 3 W(p) =3,W(q) = 3 W(p)=3,W(q)=3,将插头p和q连接起来就相当于形成了一条完整的路径,这种情况只能出现在最后一个非障碍格子。
2.2 W ( p ) , W ( q ) W(p) ,W(q) W(p),W(q) 中有一个为3,如果p为独立插头,那么无论q是左括号插头还是右括号插头,与q相匹配的插头v成为了独立插头,因此,W(v) ←3。如果q为独立插头,类似处理。- 3 $W§ ,W(q) $中有一个>0,即p, q中有一个插头存在。
3.1 如果这个插头为独立插头,若在最后一个非障碍格子,这个插头可以成为路径的一端,否则可以用右插头或下插头来延续这个独立插头。
3.2 如果这个插头是左括号或右括号,那么可以将这个插头“封住”,使它成为路径的一端,需要将这个插头所匹配的另一个插头的状态修改成为独立插头。
特别需要注意,任何时候轮廓线上独立插头的个数不可以超过2个,这是一个重要的剪枝依据。
POJ 1739 Tony’s Tour
一个 m * n 的棋盘(m, n ≤ 8),有的格子是障碍(用 # 表示),要求从左下角走到右下角,每个格子恰好经过一次,问方案总数。
这是标准的一条路径问题,不过限定了起点和终点。对于起点终点都在棋盘边界上的一条路径问题,可以外扩原图将其变成一条回路问题。

/* POJ 1739 Tony's Tour */
#include <iostream>
#include <cstring>
using namespace std;
#define M 401 // 341
int d[2][M], hs[M], st[2][M], cc[2], m, n; char b;
int r_t_l(int k, int i) {
for (int c=1, j=m<<1; i<=j; i+=2) {
int t = k>>i & 3;
if (t == 1) ++c;
else if (t == 2 && --c == 0) return k ^ 3<<i;
}
return k;
}
int l_t_r(int k, int i) {
for (int c=1; i>=0; i-=2) {
int t = k>>i & 3;
if (t == 2) ++c;
else if (t == 1 && --c == 0) return k ^ 3<<i;
}
return k;
}
int r_t_d(int k, int i) {
for (int c=1, j=m<<1; i<=j; i+=2) {
int t = k>>i & 3;
if (t == 1) ++c;
else if (t == 2 && --c == 0) return k | 3<<i;
}
return k;
}
int l_t_d(int k, int i) {
for (int c=1; i>=0; i-=2) {
int t = k>>i & 3;
if (t == 2) ++c;
else if (t == 1 && --c == 0) return k | 3<<i;
}
return k;
}
void update(int s, int v, int t) {
int x = s % M;
while (hs[x] >= 0 && st[t][hs[x]] != s) x = (x+1) % M;
hs[x] < 0 ? (d[t][cc[t]] = v, st[t][cc[t]] = s, hs[x] = cc[t]++) : d[t][hs[x]] += v;
}
int solve() {
int ans = 0;
st[0][0] = 0; d[0][0] = cc[0] = 1;
for (int i=0, c=1; i<n; ++i) {
for (int j=0, f=0; j<m; ++j, f+=2, c^=1) {
memset(hs, -1, sizeof(hs)); cc[c] = 0; cin >> b;
for (int a=0; a<cc[c^1]; ++a) {
int e = d[c^1][a], k = st[c^1][a], l = k>>f & 3, t = k>>f>>2 & 3, w = k ^ (t<<2)+l<<f;
if (b == '.') {
if (!l && !t) {
if (i+1 < n && j+1 < m) update(k | 9<<f, e, c);
else if (i+1 == n && !j) update(k | 12<<f, e, c);
} else if (!l || !t) {
l += t;
if (i+1 < n) update(w | l<<f, e, c);
if (j+1 < m) update(w | l<<f<<2, e, c);
if (i+1 == n && !j && l < 3) update(r_t_d(w, f+4), e, c);
if (i+1 == n && j+1 == m && l == 3) ans += e;
} else if (l == 2 && t == 1) update(w, e, c);
else if (l == 1 && t == 1) update(r_t_l(w, f+4), e, c);
else if (l == 2 && t == 2) update(l_t_r(w, f-2), e, c);
else if (l == 3 && l != t) update(t<2 ? r_t_d(w, f+4) : l_t_d(w, f-2), e, c);
else if (t == 3 && l != t) update(l<2 ? r_t_d(w, f+4) : l_t_d(w, f-2), e, c);
} else if (!l && !t) update(k, e, c);
}
}
for (int j=0; j<cc[c^1]; ++j) st[c^1][j] <<= 2;
}
return ans;
}
int main() {
while (cin >> n >> m && n) cout << solve() << endl;
return 0;
}
UVa1094/LA4789 Channel
有一个 r(2≤r≤20)行c(2≤c≤9)列的农田,有些格子是土地(用“.”表示),有些格子是石块(用“#”表示)。你的任务是修筑一条从左上角流到右下角的水道。水道应该尽量长,这样才能灌溉到尽量多的土地。
水道不能穿过任何一个石块,而且为了保证水总是沿着水道流,水道不能和自己接触,哪怕只接触一个角也不行(水可能会从这个角流过去)。下图所示是两个接触到自身(因此非法)的水道,你的任务是输出最长的水道。输入保证最长的水道是唯一的。

在一条路径问题的基础上,新增了水道不能和自己接触,哪怕只接触一个角也不行的限制,因此需要将一条路径问题的4进制状态修改成5进制:0表示无插头且非水道,1表示左括号插头,2表示右括号插头,3表示独立插头,4表示无插头且是水道。并且还需要给状态增加一维(记录左上格是否为水道)。
对于状态转移,需要在一条路径问题状态转移基础上新增处理取值为4的位并做合法性检验,有4种非法情形:左取值为4将当前格安排为水道;上取值为4将当前格安排为水道;左和上均为0但左上为水道时将当前格安排为水道;左和上均非0但左上为0时当前格不安排为水道。

ZOJ 3213 Beautiful Meadow
一个
N
×
M
N\times M
N×M 的方阵(
N
,
M
≤
8
N,M\le 8
N,M≤8),每个格点有一个权值,求一段路径(不得经过0权的格点),最大化路径覆盖的格点的权值和。
将任何时候轮廓线上独立插头的个数不可以超过2个这一点用上,可以将单个格点状态转移数量控制在2000以内,可以用哈希表高效解决。
/* ZOJ 3213 Beautiful Meadow */
#include <iostream>
#include <cstring>
using namespace std;
#define M 2001 // 1850
int d[2][M], hs[M], st[2][M], cc[2], m, n;
int r_t_l(int k, int i) {
for (int c=1, j=m<<1; i<=j; i+=2) {
int t = k>>i & 3;
if (t == 1) ++c;
else if (t == 2 && --c == 0) return k ^ 3<<i;
}
return k;
}
int l_t_r(int k, int i) {
for (int c=1; i>=0; i-=2) {
int t = k>>i & 3;
if (t == 2) ++c;
else if (t == 1 && --c == 0) return k ^ 3<<i;
}
return k;
}
int r_t_d(int k, int i) {
for (int c=1, j=m<<1; i<=j; i+=2) {
int t = k>>i & 3;
if (t == 1) ++c;
else if (t == 2 && --c == 0) return k | 3<<i;
}
return k;
}
int l_t_d(int k, int i) {
for (int c=1; i>=0; i-=2) {
int t = k>>i & 3;
if (t == 2) ++c;
else if (t == 1 && --c == 0) return k | 3<<i;
}
return k;
}
int cd(int k) {
int c = 0;
for (int i=0, j=m<<1; i<=j; i+=2) if ((k>>i & 3) == 3) ++c;
return c;
}
void update(int s, int v, int t) {
int x = s % M;
while (hs[x] >= 0 && st[t][hs[x]] != s) x = (x+1) % M;
if (hs[x] < 0) d[t][cc[t]] = v, st[t][cc[t]] = s, hs[x] = cc[t]++;
else if (v > d[t][hs[x]]) d[t][hs[x]] = v;
}
int solve() {
int ans = 0;
cin >> n >> m; d[0][0] = st[0][0] = 0; cc[0] = 1;
for (int i=0, c=1; i<n; ++i) {
for (int j=0, f=0, b; j<m; ++j, f+=2, c^=1) {
memset(hs, -1, sizeof(hs)); cc[c] = 0; cin >> b; ans = max(ans, b);
for (int a=0; a<cc[c^1]; ++a) {
int e = d[c^1][a], k = st[c^1][a], l = k>>f & 3, t = k>>f>>2 & 3, w = k ^ (t<<2)+l<<f;
if (b) {
if (!l && !t) {
int dd = cd(k);
if (i+1 < n && j+1 < m) update(k | 9<<f, e+b, c);
if (i+1 < n && dd < 2) update(k | 3<<f, e+b, c);
if (j+1 < m && dd < 2) update(k | 12<<f, e+b, c);
update(k, e, c);
} else if (!l || !t) {
l += t;
if (i+1 < n) update(w | l<<f, e+b, c);
if (j+1 < m) update(w | l<<f<<2, e+b, c);
if (l < 3 && cd(k) < 2) update(l<2 ? r_t_d(w, f+4) : l_t_d(w, f-2), e+b, c);
if (l==3 && ((t && k==(t<<f<<2)) || (!t && k==(l<<f)))) ans = max(ans, e+b);
} else if (l == 2 && t == 1) update(w, e+b, c);
else if (l == 1 && t == 1) update(r_t_l(w, f+4), e+b, c);
else if (l == 2 && t == 2) update(l_t_r(w, f-2), e+b, c);
else if (l == 3 && l != t) update(t<2 ? r_t_d(w, f+4) : l_t_d(w, f-2), e+b, c);
else if (t == 3 && l != t) update(l<2 ? r_t_d(w, f+4) : l_t_d(w, f-2), e+b, c);
else if (t == 3 && l == 3 && k == (15<<f)) ans = max(ans, e+b);
} else if (!l && !t) update(k, e, c);
}
}
for (int j=0; j<cc[c^1]; ++j) st[c^1][j] <<= 2;
}
return ans;
}
int main() {
int t; cin >> t;
while (t--) cout << solve() << endl;
return 0;
}
简单路径相关题目
UVa1214/LA3620 Manhattan Wiring
n×m网格里有空格和障碍(1≤n,m≤9),还有两个2和两个3。要求把这两个2和两个3各用一条折线连起来,使得总长度尽量小(线必须穿过格子的中心,每个单位正方形的边长为1)。限制条件如下:障碍格里不能有线,而每个空格里最多只能有一条线。由此可知,两条折线不能相交,每条折线不能自交。
下图的例子,折线总长度为18(别忘了两个2和两个3所在的4个格子中各有一条长度为0.5 的线)。

参照简单路径问题的处理思路即可,轮廓线上的每个位置有3 种可能:0(无线)、1(2 线)、2(3 线),不需要用到括号表示法。
UVa11276 Magical Seven
给出一个 7 × n 7×n 7×n( 1 ≤ n < 2 64 1≤n<2^{64} 1≤n<264)的网格,每个格子看成一个点,有公共边的格子连一条弧,可以得到一个无向图G。设A 为G 的完美匹配个数,B 为G 的哈密顿圈的个数,C 为满足以下条件的G 的生成子图的个数:该生成子图的每个连通分量均为一个圈。你的任务是计算A+B+C。
A、B、C分别对应轮廓线动态规划里面的三个经典问题:A、覆盖模型;B、一条回路;C、多条回路。
n达到
2
64
2^{64}
264规模,而题目卡2s限时,主要难点还是在矩阵运算加速上。
首先,三个问题单独求解,其状态转移矩阵都是稀疏的(每个状态最多转移到2个其他状态),那么做矩阵乘法运算时,对元素做一个非零检查将大大提速。
void mul(const int (&a)[T][T], const int (&b)[T][T], int (&c)[T][T]) {
memset(c, 0, sizeof(c));
for (int i=0; i<t; ++i) for (int k=0; k<t; ++k) if (a[i][k]) for (int j=0; j<t; ++j) if (b[k][j])
c[i][j] = (c[i][j] + a[i][k]*b[k][j]) % M;
}
其次,构建矩阵时从唯一初始状态开始做dfs,可以一开始就将那些根本转移不到的状态过滤掉,预先运行一下可以发现A、B、C各自实际的规模分别是70、319、112。并且,由于B、C问题存在滚动操作,每一行的7个矩阵相乘完后最终的矩阵会存在第 i i i行和第 i i i列全部为0的情况,二者规模还可以精简到261、82(A问题也可以用滚动操作但不是必须的)。
非简单路径问题
在处理简单路径问题时,用括号表示法进行状态编码时间效率优于最小表示法,实现也更简单。但在处理非简单路径问题时采用最小表示法更合适,虽然非简单路径问题可以采用效率更高的广义括号表示法,但代码相对容易写错。
最小表示法处理非简单路径问题时,需要考虑连通分量的生成、合并与消失。
UVa11443 Tree in a Grid
有一个 r 行c 列的点阵(1≤r≤200,1≤c≤8),行从上到下编号为0 ~ r-1,列从左到右编号为0 ~ c-1。第i 行j 列的点记为(i,j),它的上、左、下、右相邻点的坐标分别为(i-1,j)、(i,j-1)、(i+1,j)和(i,j+1)。(i,j)和这4 个点(如果存在的话)之间可以连一条边。
目前,这个点阵中有一些边已经连好,你的任务是继续连边,使得点阵中的所有点连通,但不构成环,然后统计方案总数除以md 的余数(1≤md≤1000000)。
本题最后要生成一棵树,因此dp时连通分量消失的情况非法并且连通分量合并时不得产生环。
染色模型
除了路径模型之外,还有一类常见的模型,需要对棋盘进行染色,相邻的相同颜色节点被视为连通。在路径类问题中,状态转移的时候枚举的是当前路径的方向,而在染色类问题中,枚举的是当前节点染何种颜色。在染色模型中,状态中处在相同连通性的节点可能不止两个。
UVa10572 Black & White
在一个m行n列的网格中已经有一些格子涂上了黑色或者白色。你的任务是把其他格子也涂上黑色或者白色,使得任意2×2子网格不会全黑或者全白,且所有黑格四连通,所有白格也四连通。输出方案总数和其中一组方案。
比如,在下图所示的4幅图中,第一幅中黑格不连通,第三幅中存在2×2的全黑子网格,其余两幅图合法。

可以用多维状态表示,其中最小表示法只管连通分量编号,状态里面再多出轮廓线首格颜色以及当前格左上的颜色这两个维度即可。利用首格颜色和连通信息,可以推出当前格左格的颜色和上格的颜色。



&spm=1001.2101.3001.5002&articleId=148834321&d=1&t=3&u=364987f0a9ef4d9f9f76f4181521ecc4)
1590

被折叠的 条评论
为什么被折叠?



