题解:洛谷 P2919 Guarding the Farm

【题目来源】

洛谷:P2919 [USACO08NOV] Guarding the Farm S - 洛谷

【题目描述】

农场有许多小山丘,约翰农夫希望在这些小山丘上放置守卫,以确保他珍贵的奶牛的安全。

他想知道如果他希望在每个小山丘的顶部放置一个守卫,他需要多少守卫。他有一张地图,这张地图是一个整数矩阵;矩阵有 \(N\) 行(\(1 < N \leq 700\))和 \(M\) 列(\(1 < M \leq 700\))。矩阵中的每个元素表示一个高度 \(H_{ij}\)\(0 \leq H_{ij} \leq 10,000\))。请帮助他确定地图上有多少个山顶。

一个山顶是由一个或多个相邻且具有相同值的矩阵元素组成,这些元素被地图的边缘或具有较低(较小)高度的元素完全包围。如果两个不同的元素的 \(X\) 坐标差的绝对值不大于 1,且 \(Y\) 坐标差的绝对值也不大于 1,则它们是相邻的。

【输入】

* 第 1 行:两个用空格分隔的整数:\(N\)\(M\)

* 第 2 行到第 \(N+1\) 行:第 \(i+1\) 行描述矩阵的第 \(i\) 行,包含 \(M\) 个用空格分隔的整数:\(H_{ij}\)

【输出】

* 第 1 行:一个整数,表示山顶的数量

【输入样例】

8 7
4 3 2 2 1 0 1
3 3 3 2 1 0 1
2 2 2 2 1 0 0
2 1 1 1 1 0 0
1 1 0 0 0 1 0
0 0 0 1 1 1 0
0 1 2 2 1 1 0
0 1 1 1 2 1 0

【输出样例】

3

【核心思想】

  1. 问题分析:给定 \(N \times M\) 的高度矩阵,求"山顶"数量。山顶定义:由相邻(八连通)且高度相同的格子组成的连通块,且该连通块被边缘或更低高度的格子完全包围(即连通块内所有格子的八邻域中没有更高高度的格子)。这是一个BFS 找连通块 + 极值判断问题,关键在于对每个等高连通块判断其是否为局部最大值区域。

  2. 算法选择

    • BFS 找连通块:遍历每个未访问的格子,BFS 扩展八连通方向中高度相同的格子,标记访问
    • 极值判断:在 BFS 过程中,检查连通块内每个格子的八邻域,若存在更高高度的格子,则该连通块不是山顶
  3. 关键步骤

    • 读入数据\(N, M\) 和高度矩阵 \(h[1..N][1..M]\)
    • 遍历所有格子\(i\)\(1\)\(N\)\(j\)\(1\)\(M\)):
      • vis[i][j] 已访问,跳过
      • bfs(i, j):找到以 \((i,j)\) 为起点的等高连通块
        • val = h[i][j],记录当前连通块高度
        • flag = true,标记是否为山顶
        • 队列扩展八邻域:
          • h[nx][ny] > valflag = false(存在更高点,不是山顶)
          • h[nx][ny] == val && !vis[nx][ny]:加入队列,标记访问
        • 返回 flag
      • bfs 返回 trueans++
    • 输出 ans
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \cdot M)\),每个格子最多被访问一次,八邻域检查常数时间
    • 空间复杂度:\(O(N \cdot M)\),高度矩阵和访问标记
  5. BFS 连通块 + 极值判断的核心思想

    • 等高连通块:山顶不是单个格子,而是由相邻等高格子组成的区域,BFS 八连通扩展找到完整区域
    • 局部最大值的区域化:传统"山顶"是单个最高点,本题扩展为"等高区域",要求整个区域的外围(八邻域)没有更高点
    • 即时否定策略:BFS 过程中一旦发现某个连通块格子的邻域有更高点,立即置 flag = false,无需等待 BFS 结束
    • 访问标记避免重复:每个格子只属于一个连通块,访问后不再处理,保证线性时间复杂度
    • 适用于"网格连通块 + 局部极值判断"问题,核心在于将单点极值扩展为区域极值,用 BFS 统一处理

【算法标签】

普及+ #BFS-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;  // 定义坐标对类型
const int N = 705;  // 定义地图最大尺寸
int n, m;  // n: 行数,m: 列数
int h[N][N];  // 存储每个点的高度
bool vis[N][N];  // 标记每个点是否被访问过

// 八个方向的偏移量
int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};

// 广度优先搜索函数,判断(x,y)是否是山丘的最高点
bool bfs(int x, int y)
{
    int val = h[x][y];  // 记录当前连通块的高度值
    bool flag = true;  // 标记当前连通块是否满足山丘条件

    queue<PII> q;  // 定义队列用于BFS
    q.push({x, y});  // 将起点加入队列
    vis[x][y] = true;  // 标记起点已访问

    while (!q.empty())  // 当队列不为空时循环
    {
        int xx = q.front().first, yy = q.front().second;  // 取出队首坐标
        q.pop();  // 弹出队首
        for (int i = 0; i < 8; i++)  // 遍历八个方向
        {
            int nx = xx + dx[i], ny = yy + dy[i];  // 计算相邻坐标
            if (nx < 1 || nx > n || ny < 1 || ny > m)  // 检查边界
                continue;
            if (h[nx][ny] > val)  // 如果相邻点高度大于当前连通块高度
                flag = false;  // 不满足山丘条件

            if (h[nx][ny] == val && !vis[nx][ny])  // 如果相邻点高度相同且未访问
            {
                q.push({nx, ny});  // 加入队列
                vis[nx][ny] = true;  // 标记已访问
            }
        }
    }
    return flag;  // 返回当前连通块是否满足山丘条件
}

int main()
{
    cin >> n >> m;  // 输入行数和列数
    for (int i = 1; i <= n; i++)  // 读入高度图
        for (int j = 1; j <= m; j++)
            cin >> h[i][j];

    int ans = 0;  // 山丘数量
    for (int i = 1; i <= n; i++)  // 遍历整个地图
        for (int j = 1; j <= m; j++)
        {
            if (!vis[i][j])  // 如果当前点未被访问
            {
                if (bfs(i, j))  // 执行BFS,判断是否是山丘
                    ans++;  // 如果是山丘,计数加1
            }
        }
    cout << ans << endl;  // 输出山丘数量

    return 0;
}

【运行结果】

8 7
4 3 2 2 1 0 1
3 3 3 2 1 0 1
2 2 2 2 1 0 0
2 1 1 1 1 0 0
1 1 0 0 0 1 0
0 0 0 1 1 1 0
0 1 2 2 1 1 0
0 1 1 1 2 1 0
3
posted @ 2026-08-10 12:29  团爸讲算法  阅读(9)  评论(0)    收藏  举报