[GDKOI2016]地图[题解]
地图
题意
给定一个 \(n\times m\) 的矩阵,由字符 'S','E','X','.','?','#' 组成,其中 '#' 表示障碍物,'.' 表示空地,'?' 可以随便定义成其他五种字符中的一种。
一个合法的矩阵满足一下条件:
-
在四连通的前提下,'S','E','X' 彼此可达。
-
'S','E','X' 三种字符存在且仅存在一个。
求有多少个矩阵合法,答案取模 \(1e9+7\)。
\(1\leq n\leq 7\)
分析
学习过插头 \(dp\) 相关知识后不难看出这是一道插头 \(dp\) 的题目。
首先要考虑的是我们如何压缩状态,由于最多有 7 列, 且不难看出 \(dp\) 过程中我们肯定需要维护连通性,保险起见,把压缩状态开到 \(8\),分别对应不同的连通块编号。则如果将我们的状态看成一个八进制的数,每一位从左往右分别表示对应的格子属于的连通块编号。
考虑如何维护。
我们还是从左往右,从上往下的按格枚举。分别考虑左边和上边的格子,插入一个格子的情况大致如下:
-
如果是 '#' ,将对应一列的格子状态更新为 \(0\),不属于任何连通块,直接扔进下一轮。
-
如果是三种特殊块之一,查找当前状态是否已经存在这种特殊块,如果存在,则舍弃这种情况。否则,更新如空地,但不要忘了在目标状态上标记已选。
-
如果是 '.',即空地,大致有三种情况:
-
左边和上面状态都是 \(0\),直接插入。
-
左边和上面任意一边是 \(0\),合并到不是 \(0\) 的位置上去,即用和那个位置所属连通块编号的编号插入当前位置。
-
左边和上面状态全部都存在,钦定一种情况更新,这里我选的是合并到编号小的块上去。
-
状态记录用 \(hash\) 表,最后的答案是所有完全更新后且三个特殊块在一个连通块内的状态。
具体的,你需要记录每种特殊块分别属于哪个连通块,这并不困难。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const char sp[5] = {'S', 'X', 'E', '.', '#'};
const int N = 14, H = 3e4 + 7, S = 1e5 + 10, mod = 1e9 + 7;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
int n, m, ans, opt;
int x[3], y[3], ch[N], code[N];
/*
全局变量中的 code 表示当前状态中每一列的块属于连通块的编号
末尾的三个位置用于记录三种特殊块所在连通块的编号情况
*/
char mp[N][N];
struct node{
//hash表
int siz;
int f[S], bl[S], nex[S], first[H];
inline void clear() { siz = 0, memset(first, -1, sizeof(first)); }
inline void push(int x, int k){
int pos = x % H, i = first[pos];
for(; i != -1; i = nex[i])
if(bl[i] == x) { f[i] = (f[i] + k) % mod; return ; }
bl[siz] = x, f[siz] = k;
nex[siz] = first[pos], first[pos] = siz++;
}
}vec[2];
//判断在当前状态中,是否回和选定的块发生冲突
inline bool check(int i, int j)
{
if(mp[i][j] == 'S' && code[m + 1]) return true;
else if(mp[i][j] == 'X' && code[m + 2]) return true;
else if(mp[i][j] == 'E' && code[m + 3]) return true;
else return false;
}
//取出状态中储存的 code 信息
inline void Get_code(int x) { for(register int i = 1; i <= m + 3; i++) code[i] = x & 7, x >>= 3; }
//计算当前 code 对应的状态(即将 code 压缩)
inline int Get_val(int i, int j)
{
if(mp[i][j] == 'S') code[m + 1] = code[j];
else if(mp[i][j] == 'X') code[m + 2] = code[j];
else if(mp[i][j] == 'E') code[m + 3] = code[j];
int res = 0, cnt = 0;
memset(ch, -1, sizeof(ch)), ch[0] = 0; // ch 记录新的编号
for(register int t = m + 3; t >= 1; t--){
if(ch[code[t]] == -1) ch[code[t]] = ++cnt; //重新给不同的连通块编号
res <<= 3, res |= ch[code[t]];
}
return res;
}
inline void update(int i, int j, int opt) //更新
{
for(register int k = 0; k < vec[opt].siz; k++){ //遍历当前轮次的状态
Get_code(vec[opt].bl[k]);
if(check(i, j)) continue ;
int p = code[j - 1], q = code[j], id = 13; //取出左和上的状态
if(p) id = min(id, p);
if(q) id = min(id, q);
//直接进行更新
if(p) for(register int t = 1; t <= m + 3; t++) if (code[t] == p) code[t] = id;
if(q) for(register int t = 1; t <= m + 3; t++) if (code[t] == q) code[t] = id;
code[j] = id, vec[opt ^ 1].push(Get_val(i, j), vec[opt].f[k]); //扔进下一轮
}
}
inline void change(int i, int j, int opt) //专门更新 '#',其他状态都不变,但当钱列的块的状态需要改变为 0
{
for(register int k = 0; k < vec[opt].siz; k++)
Get_code(vec[opt].bl[k]), code[j] = 0, vec[opt ^ 1].push(Get_val(i, j), vec[opt].f[k]); //扔进下一轮
}
inline void Sol()
{
vec[0].clear(), vec[1].clear(), vec[0].push(0, 1); //初始化邻接表
for(register int i = 1; i <= n; i++){
for(register int j = 1; j <= m; j++){
if(mp[i][j] != '?'){
if(mp[i][j] == '#') change(i, j, opt); //如果是 '#'
else update(i, j, opt); //正常更新
}
else{
for(register int t = 0; t < 5; t++){ //枚举可以变成的 5 中块
if(t < 3 && x[t] != -1) continue; //如果特殊块在给定的块中已经存在,则不考虑这种情况
mp[i][j] = sp[t];
if(mp[i][j] == '#') change(i, j, opt);
else update(i, j, opt);
}
}
vec[opt].clear(), opt ^= 1;
}
}
}
signed main()
{
n = read(), m = read();
memset(x, -1, sizeof(x)), memset(y, -1, sizeof(y));
for(register int i = 1; i <= n; i++){
scanf("%s", mp[i] + 1);
//记录一下已经给定的位置中三种特殊块的存在情况
for(register int j = 1; j <= m; j++){
if(mp[i][j] == 'S') x[0] = i, y[0] = j;
else if(mp[i][j] == 'X') x[1] = i, y[1] = j;
else if(mp[i][j] == 'E') x[2] = i, y[2] = j;
}
}
Sol();
for(register int i = 0; i < vec[opt].siz - 1; i++){
Get_code(vec[opt].bl[i]);
if((!code[m + 1]) || (!code[m + 2]) || (!code[m + 3])) continue;
int t = code[m + 1];
if(code[m + 2] != t || code[m + 3] != t) continue;
ans = (ans + vec[opt].f[i]) % mod;
}
printf("%lld\n", ans);
return 0;
}