前言

某次模拟赛的 \(D\) 题。这道题……说实话,我赛时看到的时候已经只剩一个小时了。果断放弃去对拍,前三题真的太不稳了。不过 \(AC\) 后再回来看,其实赛时如果给我三个小时,我也未必能够做出正解,大概只能拿几档暴力的部分分。这道题赛后也是看了题解才能 \(AC\) ,不得不感慨大佬就是能把大家都能看出来的 并查集 变成具体做法的神奇存在。

这道题对我而言其实有点超出认知了。这种做法除非我发散思维的时候运气好,否则根本无法通过已知的任何经验来推断。这也恰恰说明了做题的 经验和手感 是多么重要。

题目大意

给定一个 \(n \times m\) 的初始矩阵,该矩阵包含 .# 两种字符,且该矩阵的边框一定是 #。现在需要在若干个 . 上画水,使得矩阵符合物理原理。也就是:假设方格 \(a\) 画水,如果方格 \(a\) 可以经过若干满足以下条件的方格到达方格 \(b\)

  1. 高度不超过 \(a\)

  2. 字符不是 #

  3. 与方格 \(a\) 四联通

那么方格 \(b\) 中也应该画水。试求有多少种不同的矩阵满足该条件,结果对 \(10^9 + 7\) 取模。

\(1 \leq n, m \leq 1000\)

解题思路

这道题是计数问题,但是涉及到 连通性,所以排除 \(dp\) 做法。我们考虑如何使用并查集来维护方案总数。

这里直接给出做法。我们从倒数第二行开始枚举。对于高度为 \(2\) 的连通块,可以选择给整个连通块倒满水或是不倒。方案总数为 \(2\)。当我们从第 \(k\) 行走到第 \(k - 1\) 行时,第 \(k\) 行及以下的子矩阵中的连通块可能会通过第 \(k\) 行连通,再次合并。例如下表中位置分别为 \((3, 2)\)\((3, 4)\) 的两个连通块,在由第 \(3\) 行和第 \(4\) 行组成的子矩阵中并不连通。但是在第 \(2\) 行及以下组成的子矩阵中,它们是连通的。

#####
#...#
#.#.#
#####

因此,我们可以枚举第 \(k - 1\) 行的每一个字符和第 \(k\) 行与其连通的字符,然后将它们所在的并查集合并。设连通块 \(i\) 的方案总数为 \(f_i\),则连通块 \(i\) 和连通块 \(j\) 合并后,新连通块的方案总数为 在第 \(k - 1\) 行画水的方案总数 \(+\) 不在第 \(k - 1\) 画水的方案总数。因为在 \(k - 1\) 行画水都会导致整个连通块被灌水,而不在第 \(k - 1\) 行画水就可以在原本的两个小连通块内 自由划水(这么好?!),根据加乘原理,方案总数为 \(f_i \times f_j + 1\)

根据乘法原理,最后的答案是每一个连通块的方案总数的乘积,也就是 \(\prod\limits_{i} f_i\),其中 \(i\) 满足 \(fa_i = i\)

参考代码
#include <cstdio>
using namespace std;

const int maxn = 1e3 + 5;
const int mod = 1e9 + 7;

int n, m;
int id[maxn][maxn], fa[maxn * maxn];
int dx[3] = {1, 0, 0}, dy[3] = {0, -1, 1};
long long dp[maxn * maxn];
bool vis[maxn * maxn];
char s[maxn][maxn];

int get(int x) {
	if (fa[x] == x) {
		return x;
	}
	return fa[x] = get(fa[x]);
}

void merge(int x, int y) {
	x = get(x);
	y = get(y);
	if (x != y) {
		fa[y] = x;
		dp[x] = (dp[x] * dp[y]) % mod;
	}
}

int main() {
	int tx, ty;
	long long ans = 1;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%s", s[i] + 1);
		for (int j = 1; j <= m; j++) {
			id[i][j] = (i - 1) * m + j;
			fa[id[i][j]] = id[i][j];
			dp[id[i][j]] = 1;
		}
	}
	for (int i = n - 1; i >= 2; i--) {
		for (int j = 2; j <= m - 1; j++) {
			if (s[i][j] == '#') {
				continue;
			}
			for (int k = 0; k < 3; k++) {
				tx = i + dx[k], ty = j + dy[k];
				if (s[tx][ty] != '#') {
					merge(id[i][j], id[tx][ty]);
				}
			}
		}
		for (int j = 2; j <= m - 1; j++) {
			if (s[i][j] == '#') {
				continue;
			}
			if (!vis[get(id[i][j])]) {
				vis[get(id[i][j])] = true;
				dp[get(id[i][j])] = (dp[get(id[i][j])] + 1) % mod;
			}
		}
		for (int j = 2; j <= m - 1; j++) {
			if (s[i][j] == '#') {
				continue;
			}
			vis[get(id[i][j])] = false;
		}
	}
	for (int i = 2; i <= n - 1; i++) {
		for (int j = 2; j <= m - 1; j++) {
			if (s[i][j] == '#') {
				continue;
			}
			if (fa[id[i][j]] == id[i][j]) {
				ans = (ans * dp[id[i][j]]) % mod;
			}
		}
	}
	printf("%lld\n", ans);
	return 0;
}