题目类型 插头DP
题目意思
给出一个 由n * m(1 <= n,m <= 8) 个小方格组成的矩阵 其中某些小方格上有障碍物 问从矩阵的左下角走到右下角,途中经过所有非障碍空格的方法数有多少个
解题方法
插头DP
参考论文 基于连通性状态压缩的动态规划问题 中的方法
从上往下 从左到右 逐格进行动态规划
dp[i][j][S] 表示当前已经决策完格子(i, j) 后轮廓线上从左到右m+1个插头是否存在以及它们的连通性为S的方案总数
(插头 -> 一个格子某个方向的插头存在表示这个格子在这个方向与相邻格子相连
轮廓线->已决策格子和未决策格子的分界线 由于是有顺序的逐格dp 所以轮廓线与已决策格子的接触边数最多就是 m+1, 而这些接触的边所对应的已决策格子在接触边这一方向上是否有插头且插头间的连通性是需要记录的信息)
根据轮廓线上方与它相连的插头的情况来描述轮廓线的状态
用最小表示法
从左到右依次标记-> 无插头标记 0 有插头标记一个正整数 其中连通的插头标记同一个数字
关键在于状态转移的时候根据上一个状态的情况与当前考虑的那一个格子的情况 计算出转移后的状态的最小表示
自己画一个图并画上一条轮廓线就会发现 影响状态转移的就是当前轮廓线上面下一个需要决策的格子(i,j+1)的左插头和上插头(转移后轮廓线上相应位置的状态就变成是(i,j+1)下插头和右插头的状态)
参考代码 - 有疑问的地方在下方留言 看到会尽快回复的
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 12;
const int MOD = 1e4 + 7;
const int STATU = 1e6;
typedef long long LL;
struct Hash {// 用hash处理状态压缩后的状态
int next[STATU], head[MOD], size;// % MOD后结果相同的状态放在以 head[%MOD] 为链表头的链表中 用头插法
LL status[STATU], count[STATU]; // status 保存不同的状态 count 保存不同的状态对应的方法数
void init() {
size = 0;
memset(head, -1, sizeof(head));
}
void add(LL statu, LL num) {// 把方法数为num的状态statu加到哈希表中
int i;
int x = statu % MOD;
for( int i=head[x]; i!=-1; i=next[i] ) {
if(statu == status[i]) {// 状态相同直接累加方法数
count[i] += num;
return ;
}
}
status[size] = statu; //新的状态
count[size] = num;
next[size] = head[x];
head[x] = size++;
}
}H[2];//因为下一格的状态只与上一格决策后的状态有关 所以用滚动数组优化
struct XDP {
int n, m, ex, ey;// ex, ey为非障碍格子最后一个
char str[maxn];
bool M[maxn][maxn];// 矩阵状态
int code[maxn];// 轮廓线上的 m + 1的插头的状态
bool init() {
scanf("%d%d", &n, &m);
if(n == 0 && m == 0) return false;
memset(M, 0, sizeof(M));
for( int i=1; i<=n; i++ ) {
scanf("%s", str);
for( int j=0; j<m; j++ ) {
if(str[j] == '.') M[i][j+1] = true;
else M[i][j+1] = false;
}
}
M[n+1][1] = M[n+1][m] = true;// 在后面添加两行这样状态的格子就可以转化成求回路方案法
for( int i=2; i<m; i++ ) M[n+1][i] = false;
for( int i=1; i<=m; i++ ) M[n+2][i] = true;
n += 2;
ex = n, ey = m;
return true;
}
LL encode() {// 用最小表示法处理轮廓线的状态
int cnt = 1, ch[maxn];
memset(ch, -1, sizeof(ch));
ch[0] = 0;
LL statu = 0;
for( int i=0; i<=m; i++ ) {
if(ch[code[i]] == -1) ch[code[i]] = cnt++;// 从左到右从小到大给插头编号(连通的插头编号相同)
code[i] = ch[code[i]];// 1 <= n, m <= 8 的话连通的插头对数最多为4对, 编号从1-4, 那么二进制最少要3位才能保存
statu <<= 3; // 所以相当于用一个 8 进制 的数来保存轮廓线的状态
statu |= code[i];
}
return statu;
}
void decode(LL statu) {// 把状态对应的那个 8 进制数分解成 m + 1 个插头的状态
for( int i=m; i>=0; i-- ) {
code[i] = statu & 7;
statu >>= 3;
}
}
void shift() {// 当决策完一行的末尾的时候处理下 画下图就会发现 即把最后一位去掉在前面添加一个表示无插头的0
for( int i=m; i>0; i-- ) code[i] = code[i-1];
code[0] = 0;
}
void dpblank(int i, int j, int cur) { // 处理非障碍格子
int left, up;
for( int k=0; k<H[cur].size; k++ ) {
decode(H[cur].status[k]);// 首先把状态解压出来
left = code[j-1]; // 当前决策格子的左插头
up = code[j];// 当前决策格子的上插头
if(left && up) {// 如果两个方向的插头均有的话 如果两个插头又是连通的 那么只能是最后一个非障碍格子
if(left == up) {
if(i == ex && j == ey) {
code[j] = code[j-1] = 0; // 这时两个插头构成回路 那么决策完后左插头和上插头变成下插头和右插头的状态即0,0
if(j == m) shift();
H[cur^1].add(encode(), H[cur].count[k]);
}
}
else { // 两个插头不连通的话 变成连通 那么就要把和左插头值相同(即连通的)插头的值修改成和上插头的值一样
//相当于把原本分别与左插头和上插头连通的两个插头变成连通
code[j-1] = code[j] = 0;
for( int l=0; l<=m; l++ ) {
if(code[l] == left) {
code[l] = up;
}
}
if(j == m) shift();
H[cur^1].add(encode(), H[cur].count[k]);
}
}
else if((left && (!up)) || ((!left) && up)) {// 只有一个方向有插头的话可以变成下方有插头或右方有插头
int tmp = left ? left : up;
if(M[i][j+1]) {
code[j-1] = 0;
code[j] = tmp;
H[cur^1].add(encode(), H[cur].count[k]);
}
if(M[i+1][j]) {
code[j-1] = tmp;
code[j] = 0;
if(j == m) shift();
H[cur^1].add(encode(), H[cur].count[k]);
}
}
else {// 如果都没有插头的话新建一个连通分量 赋一个比较大的保证不和前面重复的值就可以了
if(M[i][j+1] && M[i+1][j]) {
code[j-1] = code[j] = maxn-1;
H[cur^1].add(encode(), H[cur].count[k]);
}
}
}
}
void dpblock(int i, int j, int cur) { //障碍格子直接把需要改变的这两位改为无插头即可
for( int k=0; k<H[cur].size; k++ ) {
decode(H[cur].status[k]);
code[j-1] = code[j] = 0;
if(j == m) shift();
H[cur^1].add(encode(), H[cur].count[k]);
}
}
void solve() {
int i, j, cur = 0;
H[cur].init();
H[cur].add(0, 1);
for( int i=1; i<=n; i++ ) {
for( int j=1; j<=m; j++ ) {
H[cur^1].init();
if(M[i][j]) dpblank(i, j, cur);
else dpblock(i, j, cur);
cur ^= 1;
}
}
LL ans = 0;
if(H[cur].size) ans = H[cur].count[0]; // 如果决策完最后一个格子后还有合法的轮廓线状态那么这个状态只有1
//个且是全0
printf("%lld\n", ans);
}
};
int main() {
freopen("in", "r", stdin);
XDP x;
while(x.init()) x.solve();
return 0;
}