Files
python/TangDou/AcWing/FloodFill/1106.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

448 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## [$AcWing$ $1106$. 山峰和山谷](https://www.acwing.com/problem/content/1108/)
### 一、题目描述
$FGD$小朋友特别喜欢爬山,在爬山的时候他就在研究山峰和山谷。
为了能够对旅程有一个安排,他想知道山峰和山谷的数量。
给定一个地图,为$FGD$想要旅行的区域,地图被分为 $n×n$ 的网格,每个格子 $(i,j)$ 的高度 $w(i,j)$ 是给定的。
若两个格子有公共顶点,那么它们就是相邻的格子,如与 $(i,j)$ 相邻的格子有
$(i−1,j−1),(i−1,j),(i−1,j+1),(i,j−1),(i,j+1),(i+1,j−1),(i+1,j),(i+1,j+1)$。
我们定义一个格子的集合 $S$ 为山峰(山谷)当且仅当:
1. $S$ 的所有格子都有相同的高度。
2. $S$ 的所有格子都连通。
3. 对于 $s$ 属于 $S$,与 $s$ 相邻的 $s′$ 不属于 $S$,都有 $w_s>w_{s′}$(山峰),或者 $w_s<w_{s′}$(山谷)。
4. 如果周围不存在相邻区域,则同时将其视为山峰和山谷。
你的任务是,对于给定的地图,求出山峰和山谷的数量,如果所有格子都有相同的高度,那么整个地图即是山峰,又是山谷。
### 二、题意理解
输入样例1:
```c++
5
8 8 8 7 7
7 7 8 8 7
7 7 7 7 7
7 8 8 7 8
7 8 8 8 8
```
<center><img src='https://cdn.acwing.com/media/article/image/2019/10/16/19_0250799aef-1.png'></center>
输入样例2:
```c++
5
5 7 8 3 1
5 5 7 6 6
6 6 6 2 8
5 7 2 5 8
7 1 0 1 7
```
<center><img src='https://cdn.acwing.com/media/article/image/2019/10/16/19_08db5e60ef-2.png'></center>
解释一下这个用例:
数字$5$,需要把周围和自己一样的数字连接在一起$(Flood~Fill)$,然后看看周围是不是存在比自己 **高** 的,是不是存在比自己 **矮** 的。
* 如果周围没有比自己高的,自己就是山峰
* 如果周围没有比自己矮的,自己就是山谷
### 三、预备知识
* 周围八个位置遍历
八个位置一般不采用四个位置的方法,即$dx[4]+dy[4]$的形式,而是采用简单粗暴的九宫格遍历二层循环的办法。
* 函数的多返回值
$C++$的多返回值,一般采用传递$\&$地址符参数的方法,让函数内修改的结果返回到调用者手中。
### 四、$bfs$实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = N * N;
typedef pair<int, int> PII;
#define x first
#define y second
int n;
int h[N][N];
PII q[M];
bool st[N][N];
/*
sx,sy:出发的位置
has_higher,has_lower:是不是周围发现了比自己高的,比自己矮的
*/
void bfs(int sx, int sy, bool &has_higher, bool &has_lower) {
// 声明队列
int hh = 0, tt = -1;
// 添加出发点
q[++tt] = {sx, sy};
st[sx][sy] = true;
while (hh <= tt) {
auto t = q[hh++];
// 利用双重循环遍历周围8连通块
for (int i = t.x - 1; i <= t.x + 1; i++)
for (int j = t.y - 1; j <= t.y + 1; j++) {
if (i == 0 || i > n || j == 0 || j > n) continue; // 出地图不行
// 下一个目标地点的高度与自己不同,需要进行标识
if (h[i][j] != h[t.x][t.y]) {
if (h[i][j] > h[t.x][t.y])
has_higher = true;
else
has_lower = true;
} else if (!st[i][j]) { // 与自己相同,并且没有走过
q[++tt] = {i, j}; // 入队列
st[i][j] = true;
}
}
}
}
int main() {
cin >> n;
// 地图
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> h[i][j];
// 山峰个数,山谷个数
int peak = 0, valley = 0;
// Flood Fill
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
if (!st[i][j]) { // 发现新的连通块
bool has_higher = false, has_lower = false;
// bfs遍历连通块,标识并且找出这一块是否存在比它高的,比它矮的,使用引用返回多个值
bfs(i, j, has_higher, has_lower);
if (!has_higher) peak++; // 没有比自己高的,山峰
if (!has_lower) valley++; // 没有比自己矮的,山谷
// 由于三种情况,山峰,山谷,即不是山峰也不是山谷,所以不能用else
}
}
printf("%d %d\n", peak, valley);
return 0;
}
```
### 五、$dfs$+引用参数
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
// 通过了 20/22个数据
int n;
bool st[N][N];
int h[N][N];
/*
# 不同操作系统默认栈的大小
Linux默认栈空间的大小为8MB,通过命令ulimit -s来设置
在Windows下,栈的大小是2MB
# 原因分析
每进行一次递归,都会在栈上多加一层,所以递归太深的话会出现数据溢出的错误。函数调用层次过深,每调用一次,函数的参数、
局部变量等信息就压一次栈。
n=1000 n*n=1e3*1e3=1e6
sx,sy 每个int 4byte,所以共8 byte
bool has_higher,has_lower 各占1个byte ,所以共2byte
1e6*10=1e7 byte = 1e7/1024 kb=9,765.625 kb = 9.53mb
如果不加上 has_higher,has_lower,就是
1e6*8= 8e6/1024 kb=7,812.5kb = 7.6mb
因AcWing的评测机是GCC搭建在Linux环境中,所以栈的空间默认是8MB(我猜的,不对Y总别骂我~),也就是我们的运气好,采用全局的has_higher,
has_lower刚刚好通过这组测试数据,如果再多一点,一样是会挂掉的,这个是递归与栈的本质造成,这时只能采用bfs进行Flood Fill
# 写给AcWing
一般来说,评测时的栈空间限制等于内存限制。但系统默认的栈空间往往较小,有时会出现官方评测时正常运行,而本地测试时爆栈的情况。这时候就需要对栈空间进行更改。
现在看来AcWing的栈空间是默认的8MB,而不是CCF官方的栈空间限制等于内存限制,不知道y总是出于什么考虑。
参考链接:https://studyingfather.blog.luogu.org/noi-technical-faq
# 解决办法:
* 如果递归的层次较多,尽量避免dfs函数的参数个数,防止递归太深导致MLE出现
* 避开dfs,采用bfs即可解决,此时内存是在堆上分配的,可以使用3GB或以上
*/
void dfs(int sx, int sy, bool &has_higher, bool &has_lower) {
st[sx][sy] = true;
for (int x = sx - 1; x <= sx + 1; x++) {
for (int y = sy - 1; y <= sy + 1; y++) {
if (x <= 0 || x > n || y <= 0 || y > n) continue;
if (h[sx][sy] != h[x][y]) { // 高度不相等
if (h[sx][sy] < h[x][y]) has_higher = true;
if (h[sx][sy] > h[x][y]) has_lower = true;
} else { // 高度相等
if (st[x][y]) continue;
st[x][y] = true;
dfs(x, y, has_higher, has_lower);
}
}
}
}
int vally, peak;
int main() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> h[i][j];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (!st[i][j]) {
bool has_higher = false, has_lower = false;
dfs(i, j, has_higher, has_lower);
if (has_higher && has_lower) continue;
if (has_higher) vally++;
if (has_lower) peak++;
}
}
}
// 对于不存在山峰+山谷的一马平地,山峰山谷都输出1
if (peak == 0 && vally == 0) peak = 1, vally = 1;
printf("%d %d\n", peak, vally);
return 0;
}
```
### 六、$dfs$+全局变量
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n;
bool f[N][N];
int h[N][N];
//  将两个需要返回的参数,设置为全局变量,则可以正常通过此题。
// 将两个需要返回的参数,设置为带地址符的变量,则MLE
bool has_higher, has_lower;
// 657 ms
void dfs(int sx, int sy) {
f[sx][sy] = true;
for (int x = sx - 1; x <= sx + 1; x++) {
for (int y = sy - 1; y <= sy + 1; y++) {
if (x <= 0 || x > n || y <= 0 || y > n) continue;
if (h[sx][sy] != h[x][y]) { // 高度不相等
if (h[sx][sy] < h[x][y]) has_higher = true;
if (h[sx][sy] > h[x][y]) has_lower = true;
} else { // 高度相等
if (f[x][y]) continue;
dfs(x, y);
}
}
}
}
int vally, peak;
int main() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> h[i][j];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (!f[i][j]) {
has_higher = has_lower = false;
dfs(i, j);
if (has_higher && has_lower) continue;
if (has_higher) vally++;
if (has_lower) peak++;
}
}
}
// 对于不存在山峰+山谷的一马平地,山峰山谷都输出1
if (peak == 0 && vally == 0) peak = 1, vally = 1;
printf("%d %d\n", peak, vally);
return 0;
}
```
### 七、并查集解法
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
const int M = N * N;
int n;
int h[N][N];
int st[M][2]; // 第一维:并查集编号,第二维:0:附近的最小值,1:附近的最大值
// 1692 ms
// 8个方向
int dx[] = {0, 0, -1, 1, -1, 1, -1, 1}; // 上下左右
int dy[] = {1, -1, 0, 0, 1, 1, -1, -1}; // 左下,右下,左上,右上
int p[M];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 根据坐标获取并查集的编号
void getXy(int num, int &x, int &y) {
x = (num - 1) / n + 1;
y = (num - 1) % n + 1;
}
// 根据并查集的编号获取坐标
int getNum(int x, int y) {
return (x - 1) * n + y;
}
int valley, peak;
int main() {
// 初始化并查集
// i为每个格子在并查集中的编号,编号策略为 (i-1)*n+j
for (int i = 0; i < M; i++) p[i] = i; // 每个人都是自己的祖先
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
cin >> h[i][j];
int num = getNum(i, j);
st[num][0] = st[num][1] = h[i][j]; // 初始化
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
for (int k = 0; k < 8; k++) {
int x = i + dx[k], y = j + dy[k];
if (x == 0 || y == 0 || x > n || y > n) continue;
// 编号
int a = getNum(i, j);
int b = getNum(x, y);
// 族长
int pa = find(a), pb = find(b);
// 记录我们家族周围最小的
if (h[i][j] > h[x][y]) st[pa][0] = min(st[pa][0], h[x][y]);
// 记录我们家族周围最大的
if (h[i][j] < h[x][y]) st[pa][1] = max(st[pa][1], h[x][y]);
if (h[i][j] == h[x][y]) {
if (pa != pb) { // 合并并查集
p[pa] = pb;
st[pb][0] = min(st[pb][0], st[pa][0]);
st[pb][1] = max(st[pb][1], st[pa][1]);
}
}
}
}
// 没有比自己高的,山峰
// 没有比自己矮的,山谷
for (int i = 1; i <= n * n; i++) {
if (p[i] == i) {
int x, y;
getXy(i, x, y);
if (st[i][0] == h[x][y]) valley++;
if (st[i][1] == h[x][y]) peak++;
}
}
printf("%d %d\n", peak, valley);
return 0;
}
```
### 八、并查集优化
因为并查集通过双重循环,从左到右,从上到下遍历,所以,可以通过双向记录周边最大最小的办法,让每个不同的区块之间互认,这样就只需要枚举
**$1$右** **$2$下** **$3$右下** **$4$左下** 即可。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
const int M = N * N;
int n;
int h[N][N];
int st[M][2]; // 第一维:并查集编号,第二维:0:附近的最小值,1:附近的最大值
// 1132 ms
int dx[] = {0, 1, 1, 1}; // 1右 2下 3右下 4左下
int dy[] = {1, 0, 1, -1};
int p[M];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 根据坐标获取并查集的编号
void getXy(int num, int &x, int &y) {
x = (num - 1) / n + 1;
y = (num - 1) % n + 1;
}
// 根据并查集的编号获取坐标
int getNum(int x, int y) {
return (x - 1) * n + y;
}
int valley, peak;
int main() {
cin >> n;
// 初始化并查集
// i为每个格子在并查集中的编号,编号策略为 (i-1)*n+j
for (int i = 0; i < M; i++) p[i] = i; // 每个人都是自己的祖先
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
cin >> h[i][j];
int num = getNum(i, j);
st[num][0] = st[num][1] = h[i][j]; // 初始化
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
for (int k = 0; k < 4; k++) {
int x = i + dx[k], y = j + dy[k];
if (x == 0 || y == 0 || x > n || y > n) continue;
// 编号
int a = getNum(i, j), b = getNum(x, y);
// 族长
int pa = find(a), pb = find(b);
// 记录我们家族周围最小的
if (h[i][j] > h[x][y]) {
st[pa][0] = min(st[pa][0], h[x][y]);
st[pb][1] = max(st[pb][1], h[i][j]);
}
// 记录我们家族周围最大的
if (h[i][j] < h[x][y]) {
st[pa][1] = max(st[pa][1], h[x][y]);
st[pb][0] = min(st[pb][0], h[i][j]);
}
if (h[i][j] == h[x][y]) {
if (pa != pb) { // 合并并查集
p[pa] = pb;
st[pb][0] = min(st[pb][0], st[pa][0]);
st[pb][1] = max(st[pb][1], st[pa][1]);
}
}
}
}
// 没有比自己高的,山峰
// 没有比自己矮的,山谷
for (int i = 1; i <= n * n; i++) {
if (p[i] == i) {
int x, y;
getXy(i, x, y);
if (st[i][0] == h[x][y]) valley++;
if (st[i][1] == h[x][y]) peak++;
}
}
printf("%d %d\n", peak, valley);
return 0;
}
```