把“水往低处流”看成有向图的可达性,每个蓄水厂一次 BFS 求出它能灌溉的干旱区城市;可行时这个集合恰好是一段连续区间,于是问题化为区间覆盖贪心。
OJ: luogu
题目 ID: P1514
难度:提高
标签:BFS图论网格贪心区间覆盖可达性
创建: 2026-10-02 09:05
更新: 2026-10-03 10:50
形式化题目
有一个
只有第
- 它自己建了蓄水厂;或者
- 存在一个与它共边相邻、海拔严格更高、并且已经有水的格子。
要求第
把每个格子看成结点,若
于是:
格子
有水 存在第 行的某个格子 ,使得 在有向图上可以到达 。
问题变成:最少选第 1 行的几个结点,使它们能到达第
暴力解法
思路
最直接的做法就是把“建哪些蓄水厂”这件事全部枚举一遍。
第
choose[j] = 1:在第 1 行第列建蓄水厂; choose[j] = 0:不建。
dfs_choose(dep) 只负责填写第 dep 座城市的决策,填满 choose[1..M] 以后,在叶子节点上做一次统一的检查:把所有 choose[j] == 1 的格子当作起点,一起做一次洪水填充(只往海拔更低的相邻格子走),看看第
- 全部通水,就用建站数量去更新答案的最小值;
- 一遍枚举下来一个合法方案都没有,就说明无解。此时把“每座城市都建站”这个最强方案拿出来统计第
行还剩下几座通不了水的城市。
“先生成完整的 choose[],再在叶子节点统一检查”把枚举和判定分得很干净:
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-03 10:35
* update_at: 2026-10-03 10:35
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 使用 01 序列 / 选择序列递归枚举所有可能:
// 第一行一共有 m 座城市,每座城市的决策只有两种——建蓄水厂或不建。
// choose[j] = 1 表示在第 1 行第 j 列建蓄水厂,0 表示不建。
// dfs_choose(dep) 只负责填写第 dep 座的决策,填满 choose[1..m] 后
// 在叶子节点统一做一次多源洪水填充,检查干旱区是否全部通水。
// 枚举 2^m 种方案,只有 m 很小时可用,所以只适合小数据对拍。
#include <cstring>
#include <iostream>
using namespace std;
const int MAXN = 64; // 允许小规模生成器造到 n = 60;对拍时 m 会控制在 14 以内
int n, m;
int h[MAXN][MAXN]; // 海拔高度
int choose[MAXN]; // choose[j]:第 1 行第 j 列是否建蓄水厂
int seen[MAXN][MAXN]; // 洪水填充的访问标记
int able[MAXN]; // 干旱区第 j 列是否通水
int ans; // 最少蓄水厂数量
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
// 从 (x, y) 出发把能流到的城市全部标记;到第 n 行就记录通水。
void flood(int x, int y) {
seen[x][y] = 1;
if (x == n) able[y] = 1;
for (int d = 0; d < 4; d++) {
int nx = x + dx[d];
int ny = y + dy[d];
if (nx < 1 || nx > n || ny < 1 || ny > m) continue;
if (seen[nx][ny]) continue;
if (h[nx][ny] >= h[x][y]) continue; // 水往低处流
flood(nx, ny);
}
}
// 检查当前 choose[] 是否能让干旱区全部通水。
// 能通水时返回蓄水厂数量,不能通水时返回 -1。
int check() {
int cnt = 0;
for (int j = 1; j <= m; j++) {
cnt += choose[j];
}
if (cnt == 0) return -1; // 一个蓄水厂都没有,不可能通水
memset(seen, 0, sizeof(seen));
memset(able, 0, sizeof(able));
for (int j = 1; j <= m; j++) {
if (choose[j] == 1) flood(1, j);
}
for (int j = 1; j <= m; j++) {
if (able[j] == 0) return -1;
}
return cnt;
}
// 每一层决定第 dep 座城市建不建蓄水厂。
void dfs_choose(int dep) {
if (dep == m + 1) {
int cnt = check();
if (cnt != -1 && cnt < ans) ans = cnt;
return;
}
for (int v = 0; v <= 1; v++) {
choose[dep] = v;
dfs_choose(dep + 1);
}
}
void read_input() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> h[i][j];
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
ans = MAXN * MAXN; // 一个肯定取不到的初值
dfs_choose(1);
if (ans < MAXN * MAXN) {
cout << 1 << "\n" << ans << "\n";
return 0;
}
// 无解:把全部城市都建成蓄水厂,数一数干旱区里还有多少座通不了水。
for (int j = 1; j <= m; j++) choose[j] = 1;
memset(seen, 0, sizeof(seen));
memset(able, 0, sizeof(able));
for (int j = 1; j <= m; j++) flood(1, j);
int bad = 0;
for (int j = 1; j <= m; j++) {
if (able[j] == 0) bad++;
}
cout << 0 << "\n" << bad << "\n";
return 0;
}复杂度
枚举
瓶颈
瓶颈完全在
真正的浪费在于:枚举的所有方案里,同一个蓄水厂能灌溉的城市是不变的,但暴力对每种方案都重新算了一遍。只要先独立算出“在每个位置建站能灌溉哪些城市”,方案之间的共享信息就出来了。
正解
思路
第一步:每个蓄水厂一次 BFS
固定在第 1 行第
对
第二步:判断能不能做到
“能满足要求”
所以把所有
第三步:可行时,每个 是一段连续区间
接下来要解决的是“用最少的
引理:若整体可行,那么对每个源头
, 一定是第 行上一段连续的区间,即 的形式。
为什么? 反设源头
- 取一条从
到 的下坡路径 ,和一条从 到 的下坡路径 ; - 因为整体可行,
被某个源头 覆盖,存在一条从 到 的下坡路径 ; 不能碰到 或 上的任何一个格子。否则设公共格为 ,则 能到 (因为 在 或 上),而 又能沿 的后半段到 ,于是 能到 ,与反设矛盾; - 但把
看成一条从上边界( )通到下边界( )的路径,它像一堵竖墙把棋盘分成左右两半。若 ,则起点 在墙右侧、终点 在墙左侧, 必须穿墙;若 ,则 必须穿墙;若 ,那么 本身就在 上,直接矛盾。三种情况都会让某条路径与 相交。
矛盾说明
这个引理只在可行的前提下成立。不可行时集合完全可以断开,例如
2 2 2 4 3
1 4 1 4 2第 1 行第 4 列(海拔 4)能流到第 2 行的第 3 列和第 5 列,却到不了第 4 列:第 2 行第 4 列海拔也是 4,四周没有严格更高的邻居,任何源头都到不了它,所以整张图本来就不可行。算法会先在第二步拦下这种数据,根本不会用到这个引理。
第四步:区间覆盖贪心
可行性确认后,
标准贪心:把所有非空区间按左端点升序排序。设当前还差
- 在所有满足
(能接上当前缺口)的区间里,选右端点 最大的那个; - 这个区间必须选——位置
只能被左端点 的区间覆盖;而在这些区间里,右端点越远,一次能盖住的城市越多,显然最划算; - 令
( 是选中的右端点),重复,直到 。
为什么步骤 1 不能简化成“取左端点最小的可接区间”? 因为右端点才是决定“这一刀能砍多远”的量。例如还差
样例 2 的完整推演
样例 2 的网格是
行1: 8 4 5 6 4 4
行2: 7 3 4 3 3 3
行3: 3 2 2 1 1 2对第 1 行每一列做一次 BFS,得到 6 个覆盖集合:
| 源列 |
行 1 海拔 | 能灌溉的干旱区列 |
区间 |
|---|---|---|---|
| 1 | 8 | ||
| 2 | 4 | ||
| 3 | 5 | ||
| 4 | 6 | ||
| 5 | 4 | ||
| 6 | 4 |
六个集合的并集是
| 步骤 | 待覆盖位置 |
能接上的区间 | 选中(源头) | |
|---|---|---|---|---|
| 1 | ||||
| 2 | ||||
| 3 |
答案是
代码
flood(int sy) 对第 1 行第 sy 列做一次 BFS,同时记录它覆盖到的最左、最右干旱区列;主函数先判可行性,再跑区间覆盖贪心。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-03 10:35
* update_at: 2026-10-03 10:35
*/
// P1514 [NOIP 2010 提高组] 引水入城
// 思路:
// 1. 水只能从高处流向低处,把一个城市有水的效果看成“从第 1 行某座城市出发,
// 沿着只往低处走的有向边能到达哪些城市”。
// 2. 所以第 1 行每座城市都当作一次 BFS 的源,算出它能灌溉的干旱区城市。
// 干旱区里任何一座都没被覆盖,方案就不存在,直接输出 0 和不可能通水的城市数。
// 3. 若所有干旱区城市都能被覆盖,则每个蓄水厂能覆盖的干旱区城市恰好是
// 连续的一段(关键性质,见题解证明),于是问题变成“用最少的区间覆盖 [1, m]”,
// 按左端点排序后贪心即可。
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
const int MAXN = 505;
const int MAXC = MAXN * MAXN;
int n, m;
int h[MAXN][MAXN]; // h[i][j]:第 i 行第 j 列城市的海拔高度
int seen[MAXN][MAXN]; // 当前这次 BFS 的访问标记:值等于 stamp 表示本次访问过
int stamp; // BFS 时间戳,递增后就不用每次清空 seen 数组
int able[MAXN]; // able[j]:干旱区第 j 列是否被某个蓄水厂覆盖
int segL[MAXN]; // 第 1 行第 j 列建蓄水厂时,覆盖的干旱区列区间左端点
int segR[MAXN]; // 右端点;区间为空时 segL = m + 1、segR = 0
int qx[MAXC], qy[MAXC]; // 手写 BFS 队列,存城市坐标
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
// 从第 1 行第 sy 列的城市建一个蓄水厂出发做 BFS,
// 求出它实际上能灌溉到的干旱区城市列区间 [segL[sy], segR[sy]]。
// 能通水的干旱区城市同时标进 able[]。
void flood(int sy) {
stamp++;
int head = 0, tail = 0;
qx[tail] = 1;
qy[tail] = sy;
tail++;
seen[1][sy] = stamp;
int left = m + 1; // 覆盖到的最右小列
int right = 0; // 覆盖到的最右列
while (head < tail) {
int x = qx[head];
int y = qy[head];
head++;
if (x == n) { // 走到第 n 行,说明这座干旱区城市可以通水
if (y < left) left = y;
if (y > right) right = y;
able[y] = 1;
}
for (int d = 0; d < 4; d++) {
int nx = x + dx[d];
int ny = y + dy[d];
if (nx < 1 || nx > n || ny < 1 || ny > m) continue;
if (seen[nx][ny] == stamp) continue;
if (h[nx][ny] >= h[x][y]) continue; // 水只能由高处流向低处
seen[nx][ny] = stamp;
qx[tail] = nx;
qy[tail] = ny;
tail++;
}
}
segL[sy] = left;
segR[sy] = right;
}
void read_input() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> h[i][j];
}
}
}
void solve() {
for (int j = 1; j <= m; j++) {
flood(j);
}
// 干旱区中一座城市如果没有任何蓄水厂能覆盖,方案就不存在。
int bad = 0;
for (int j = 1; j <= m; j++) {
if (able[j] == 0) bad++;
}
if (bad > 0) {
cout << 0 << "\n" << bad << "\n";
return;
}
// 可行时每个蓄水厂覆盖的干旱区城市是一段连续区间,
// 收集所有非空区间,按左端点排序。
vector<pair<int, int> > seg;
for (int j = 1; j <= m; j++) {
if (segL[j] <= segR[j]) {
seg.push_back(make_pair(segL[j], segR[j]));
}
}
sort(seg.begin(), seg.end());
// 贪心:下一个待覆盖的城市是 pos,在左端点不超过 pos 的区间里挑右端点最远的。
int ans = 0;
int pos = 1;
int i = 0;
while (pos <= m) {
int far = pos - 1;
while (i < (int)seg.size() && seg[i].first <= pos) {
if (seg[i].second > far) far = seg[i].second;
i++;
}
ans++;
pos = far + 1;
}
cout << 1 << "\n" << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
对
总结
这道题的解法是一条清晰的“先建图、再降维”的链路:
- 建模:“水往低处流”
相邻且更矮的方向连边 一个 DAG 上的可达性问题。于是“某座城市有没有水”变成“它能不能被某个源头到达”。 - 去重:暴力枚举方案时,同一个蓄水厂能灌溉的城市被反复重算。改成对每个源头独立做一次 BFS,
次计算被压缩成 次。 - 降维:一般的“最少集合覆盖”是 NP-hard,但可行时每个
都是数轴上的连续区间,于是退化成经典的区间覆盖贪心。 - 贪心:按左端点排序,每次在能接上当前缺口的区间里选右端点最远的一个。
其中第 3 步是全题的关键:它不仅给出算法,也解释了为什么“不可行”和“可行”必须分成两个分支——连续区间性质只在可行时成立。而判断可行性本身只用一次简单的并集统计,代价极低。

