P6008 [USACO20JAN] Cave Paintings P 题解
P6008 [USACO20JAN] Cave Paintings P 题解
简化题意
我们有一个 \(n\) 行 \(m\) 列的网格,每格内会有一个符号。若是 #,则代表这个格子是堵塞的;若是 .,则代表这个格子是空的。
现在,我们在其中任意数量的空格子中倒水。
已知:假设我们在空格子 \((x, y)\) 中倒水,而 \((x, y)\) 与空格子 \((i, j)\) 可以通过一条仅经过空格子与带水格子的路径相连,且对于此路径中所有格子的高度都不大于 \((x, y)\) 的高度(即 \(n - x + 1\)),则空格子 \((i, j)\) 中必须有水,即只要在空格子 \((x, y)\) 中加水,空格子 \((i, j)\) 无论是否加水都会有水(整条路径中也都会有水,通俗一下解释就是水往低处流,但无法往更高处流)。(连通器原理)
我们希望求出:网格有多少种不同的情况(空白也算)。
解题思路
暴力 BFS/DFS 明显是不可行的。我们考虑其他的算法。
这里,我们先来看一下部分样例,来找一下灵感。
#.#...#.#
这里,我们发现,空白格子被分成了三块:.、... 以及另一个 .。
而假如我们只研究这行,会有多少种情况呢?
很明显,答案数量就是 $\mathrm C_3^0 + \mathrm C_3^1 + \mathrm C_3^2 + \mathrm C_3^3 = 1 + 3 + 3 + 1 = 8 = 2 ^ 3 $。这是互不干扰的情况,但是要是出现联通的情况怎么办呢?
联通与合并,遂考虑并查集。
比较容易看出来,有这些特点:
- 相邻空白格可以合并。
- 两个空白格上下相邻的情况,上面的格子可以合并下面的格子。
于是,我们可以把这个网格看成一个树形结构;再通过样例来理解一下。
#...#...#
#.#...#.#
这样,所有空白格子就成了一个连通块。我们把下面三个连通块看成三个子节点,上面的就看作父节点。这里我们开始考虑 DP 求解。父节点继承子节点的情况(类似树形 DP),根据乘法原理,转移方程即为 \(dp_i = \prod dp_j + 1\),其中,\(j\) 均为 \(i\) 的子节点。
这样最终的答案是什么呢?因为经过了并查集的操作,所以对于每个连通块,它的根节点所对的 \(dp\) 值,即为这个连通块的情况数。根据乘法原理,最后的答案就是所有连通块根节点 \(dp\) 值的乘积。
总结一下,这题难就难在首先,要意识到暴力不可行;其次,要想到连通块与合并,与并查集有关;最后,要抽象出树形结构,推出 DP 状态转移方程。还是很有意思的题目。不要像我模拟赛的时候打个暴力就走
代码部分
#include <bits/stdc++.h>
#define calc(x, y) ((x - 1) * m + y) //用来计算每个格子的唯一编号
const int MOD = 1e9 + 7;
using namespace std;
int n, m;
int a[1010][1010], fa[1000010];
long long dp[1000010];
int find(int x)//查询根节点(并查集操作)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int main()
{
cin >> n >> m;
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= m;j ++)
{
char c;
cin >> c;
a[i][j] = (c == '.' ? 1 : 0);//转化为数字,方便操作
fa[calc(i, j)] = calc(i, j);//并查集初始化
dp[calc(i, j)] = 1;//dp 值初始化
}
for(int i = n;i >= 1;i --)//从下到上操作(因为水不能往高处流)
{
for(int j = 3;j < m;j ++)//合并相邻的连通块
if(a[i][j] && a[i][j - 1])//都是空白格子
fa[find(calc(i, j))] = find(calc(i, j - 1));//并查集合并操作
for(int j = 2;j < m;j ++)//子节点找父节点
if(a[i][j] && a[i + 1][j])//都是空白格子
{
int fx = find(calc(i, j)), fy = find(calc(i + 1, j));//各自查询根节点
if(fx == fy)
continue;
fa[fy] = fx;//并查集合并操作
dp[fx] = dp[fx] * dp[fy] % MOD;//DP 值转移至根节点
}
for(int j = 2;j < m;j ++)
if(a[i][j] && find(calc(i, j)) == calc(i, j))//作为根节点,要加上全都选择的情况
dp[calc(i, j)] ++;
}
long long ans = 1;
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= m;j ++)
if(a[i][j] && find(calc(i, j)) == calc(i, j))//作为连通块的根节点
ans = ans * dp[calc(i, j)] % MOD;
cout << ans << endl;
return 0;
}
浙公网安备 33010602011771号