1. 题目解析与算法思路
这道题目描述了一个N×M的网格土地,每个格子有各自的高度。降雨后,水会积聚在低洼处,我们需要计算整个土地最多能积存多少立方英寸的水。这实际上是一个经典的"接雨水"问题的二维扩展版本。
1.1 问题建模
我们可以将这个问题建模为一个二维的高度场积水问题。想象一下,当水从外部流入时,它会从四周最低的点开始渗透。只有当四周的高度都高于某个点时,这个点才能积水。
关键观察点:
- 水会从土地边缘流出,所以边缘的格子无法积水
- 一个内部格子能积水的高度取决于它到边缘的路径上的最低高度(即所谓的"木桶效应")
- 积水量等于每个格子能积水的高度之和
1.2 算法选择
常见的解决思路有几种:
- 优先队列+BFS(类似Dijkstra算法)
- 逐层DFS/BFS模拟水位上升
- 动态规划计算每个格子的边界最小值
这里我们选择第二种方法——逐层DFS模拟水位上升,因为:
- 直观易懂,容易实现
- 对于N,M≤100的数据规模完全足够
- 可以很好地利用题目中高度范围有限(1-10000)的特性
2. 核心算法实现详解
2.1 预处理高度信息
cpp复制int h[105][105],vis[105][105];
int t[10005];
int n,m,mx,mn;
// 读取输入并预处理
read(n);read(m);
mn = 10005;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
read(h[i][j]);
mx = max(mx,h[i][j]);
mn = min(mn,h[i][j]);
t[h[i][j]]++; // 统计每个高度出现的次数
}
}
// 计算高度大于等于i的格子数量
for(int i=mx;i>=mn;i--){
t[i] += t[i+1];
}
这段代码做了几件重要的事情:
- 读取输入数据并存储到h数组中
- 记录全局最大高度mx和最小高度mn
- 统计每个高度出现的次数到t数组
- 通过后缀和技巧,快速计算高度≥i的格子数量
提示:使用后缀和可以让我们在O(1)时间内知道有多少格子的高度≥i,这在后续计算中非常有用。
2.2 水位模拟与DFS
cpp复制void dfs(int x,int y){
if(x<0||y<0||x>n+1||y>m+1)return; // 超出边界
if(vis[x][y])return; // 已访问过
if(h[x][y]>=H)return; // 当前格子高度≥水位高度
vis[x][y] = 1; // 标记为已访问
if(x>0&&x<=n&&y>0&&y<=m)sum--; // 如果是内部格子,减少可积水格子数
// 向四个方向扩散
dfs(x-1,y);
dfs(x+1,y);
dfs(x,y-1);
dfs(x,y+1);
}
DFS函数的作用是标记所有在当前水位H下会被水淹没的格子。关键点:
- 从虚拟的边界(0,0)开始DFS
- 只访问高度<H的格子
- 对于每个被淹没的内部格子,sum减1(sum初始为高度<H的格子总数)
2.3 主循环计算积水量
cpp复制for(H=mn+1;H<=mx;H++){ // 从最低到最高水位逐层检查
memset(vis,0,sizeof(vis));
sum = n*m - t[H]; // 高度<H的格子数量
dfs(0,0); // 从虚拟边界开始DFS
if(!sum)break; // 没有可积水格子了
ans += sum; // 累计当前水位的积水量
}
主循环的逻辑:
- 从最低水位mn+1开始,逐层提高到mx
- 对于每个水位H:
- 计算高度<H的格子数量sum
- 通过DFS标记所有会被淹没的格子(即水能流到的格子)
- 剩余的sum就是真正能积水的格子数量
- 将每层的积水量累加到ans中
3. 算法正确性分析
3.1 为什么这个方法有效?
这个算法的核心思想是"从外向内"模拟水位上升的过程:
- 水总是从边缘开始渗透
- 只有当四周都被更高地形包围时,水才会被"困住"
- 通过逐层提高水位,我们可以确保不漏掉任何可能的积水区域
3.2 时间复杂度分析
- 预处理:O(NM)
- 主循环:O(H),H是高度范围(mx-mn)
- 每次DFS:O(NM)(因为每个格子最多被访问一次)
总时间复杂度:O(NMH),对于N,M≤100和H≤10000,最坏情况下是1e6次操作,完全在合理范围内。
4. 优化与改进思路
4.1 可能的优化方向
-
使用优先队列替代逐层检查:
- 将边缘格子放入优先队列(最小堆)
- 每次取出最小高度的格子,处理其邻居
- 可以避免重复计算,时间复杂度降至O(NM log(NM))
-
记忆化搜索:
- 记录每个格子的"边界最小值"
- 可以减少重复计算
4.2 当前实现的优缺点
优点:
- 实现简单直观
- 对于题目给定的数据规模足够高效
- 容易理解和调试
缺点:
- 对于极端高度差大的情况效率较低
- 重复计算了部分信息
5. 常见问题与调试技巧
5.1 边界处理问题
在实现DFS时,特别要注意边界条件的处理。常见的错误包括:
- 数组越界访问
- 忘记标记已访问的格子导致无限递归
- 错误计算可积水格子数量
调试技巧:
- 打印中间结果,特别是vis数组
- 对小规模测试用例手动模拟算法过程
5.2 性能优化问题
当N,M接近100时,算法可能会变慢。可以考虑:
- 使用更高效的输入输出方法(如本题中的快速读取)
- 减少不必要的内存访问
- 提前终止不必要的计算
6. 完整代码实现与注释
cpp复制#include<bits/stdc++.h>
using namespace std;
int h[105][105]; // 存储每个格子的高度
int vis[105][105]; // 标记数组,记录是否被访问过
int t[10005]; // 高度统计数组
int n, m, mx, mn; // 网格大小,最大最小高度
int ans, sum; // 总积水量,临时计数器
int H; // 当前水位高度
// 快速读取整数
inline void read(int &res){
char c;
int f = 1;
res = 0;
c = getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')res=(res<<1)+(res<<3)+c-48,c=getchar();
res*=f;
}
// DFS函数,标记所有会被淹没的格子
void dfs(int x, int y){
// 边界检查
if(x<0||y<0||x>n+1||y>m+1) return;
if(vis[x][y]) return;
if(h[x][y] >= H) return;
vis[x][y] = 1; // 标记为已访问
// 如果是内部格子,减少可积水格子数
if(x>0 && x<=n && y>0 && y<=m) sum--;
// 向四个方向扩散
dfs(x-1, y);
dfs(x+1, y);
dfs(x, y-1);
dfs(x, y+1);
}
int main(){
// 读取输入
read(n); read(m);
mn = 10005;
// 预处理高度信息
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
read(h[i][j]);
mx = max(mx, h[i][j]);
mn = min(mn, h[i][j]);
t[h[i][j]]++; // 统计高度分布
}
}
// 计算高度≥i的格子数量(后缀和)
for(int i=mx; i>=mn; i--){
t[i] += t[i+1];
}
// 主循环:逐层提高水位
for(H=mn+1; H<=mx; H++){
memset(vis, 0, sizeof(vis));
sum = n*m - t[H]; // 高度<H的格子数量
dfs(0, 0); // 从虚拟边界开始DFS
if(!sum) break; // 没有可积水格子了
ans += sum; // 累计当前水位的积水量
}
cout << ans << endl;
return 0;
}
7. 测试用例分析
让我们分析题目提供的样例输入:
输入:
code复制3 6
3 3 4 4 4 2
3 1 3 2 1 4
7 3 1 6 4 1
可视化地形:
code复制3 3 4 4 4 2
3 1 3 2 1 4
7 3 1 6 4 1
算法执行过程:
- 最小高度mn=1,最大高度mx=7
- 水位从2开始:
- 高度<2的格子:(2,2)=1, (2,5)=1, (3,3)=1, (3,6)=1 → sum=4
- DFS后,这些格子都能被水渗透到 → sum=0
- 水位3:
- 高度<3的格子:除了上面4个,还有(1,6)=2, (2,4)=2 → sum=6
- DFS后,(2,2),(2,5),(3,3),(3,6)已被标记,但(1,6)和(2,4)无法被渗透
- 所以积水量=2((1,6)和(2,4))
- 水位4:
- 类似过程,积水量=3
- 更高水位没有新增积水
- 总积水量=5(水位3的2 + 水位4的3)
8. 算法扩展与应用
这个算法可以扩展到许多实际问题中:
- 地形积水分析(地理信息系统)
- 图像处理中的区域填充
- 物理模拟中的流体流动
- 三维空间中的体积计算
对于更大规模的数据,可以考虑以下优化:
- 并行计算:不同水位可以并行处理
- 空间分区:将地图划分为多个区域分别处理
- 近似算法:对于精度要求不高的情况,可以使用采样等方法加速
在实际编程竞赛中,掌握这种"从外向内"的思考方式非常重要。它不仅适用于积水问题,还可以解决许多类似的边界扩散问题,如:
- 迷宫求解
- 图像连通区域分析
- 物理模拟中的热传导等
理解了这个算法的核心思想后,可以尝试解决LeetCode上的"接雨水"问题(42. Trapping Rain Water),那是这个问题的一维简化版本。