P1514 [NOIP 2010 提高组] 引水入城 笔记
前言
本蒟蒻刷题时看到 dalao 的题解,思考 114514 秒后才得出答案,因此写下如下见解,希望能帮到大家。
- 使用AI优化,原文难看的要死
题目简介
MC版
你有一个空间,可以在最上面一行放水,水会顺着方块往低处流。
问:下面的地能不能全被水铺满?
如果不能,那就没辙了,顺便数数还有几块地是干的。
如果能,由于你是恒大集团的一员,要尽可能地节省经费,求最少放几个水源。
每个格子上的数字,就是那地方堆了几个方块(高度)。
普通版
给定一个 \(n \times m\) 的矩形,每个格子有一个高度。
你可以在第一行的任意多个格子放水,水会从高处往低处流(只能流到相邻且高度严格更低的格子)。
问:
- 能否让最后一行的每个格子都有水流到?如果不能,输出无解,并给出最后一行有多少格子没有水;
- 如果能,最少需要在第一行放几个水源,才能使最后一行全部有水。
正文
无解的情况好判断:从第一行每个格子往里灌,搜一遍标记能到的格子,最后一行要是还有没被标记的,那就是无解,答案就是没标记的格子数。这部分没什么好说的。
剩下是有解的情况。有解的时候,一条水流到最后一行的覆盖一定是一段连续的区间,不会中间断一截。为什么呢?
假设一条水流分叉了,中间空出一块蓝色区域。既然题目要求最后一行全都有水,那这块蓝色区域迟早也得被水填上吧?那填它的那滴水是从哪来的?它总得从上面流下来,而流下来的路上,必然要跟原来那条水流碰头。一碰头,两条水流在最后一行就连成一片了,中间那截空洞根本留不下来。

- 注:此为原题解内的图片,借用致歉,侵私删。
既然蓝色区域有水流过去,就肯定会有一条水流流进蓝色区域。
为什么?
上面不解释了吗你还问

- 注:此为原题解内的图片,借用致歉,侵私删。
那为什么不能是下面这种情况呢?

- 注:此为自己手绘,画风不一样致歉。
因为这样就跟上面的水流矛盾了。
为什么?上面的水流就应该是如图,所以举一反三,压根不存在如上的水流。
说白了,有解的情况下不可能留一块没水过去的蓝色区域,因为题目要求最后一行必须全部有水。所以水流只能是下面这样:

- 注:此为原题解内的图片,借用致歉,侵私删。
想通这一点,问题就变成:每个第一行的格子,灌下去之后能覆盖最后一行的一段区间 \([l_i, r_i]\),现在要用最少的区间把 \([1, m]\) 整段盖满。
这就是个很经典的区间覆盖贪心。从 left = 1 开始,在所有左端点不超过 left 的区间里,挑右端点最远的那个,把覆盖推到 right,然后 left = right + 1,接着往下找,直到盖完。为什么这么挑一定最优?因为当前这个 left 总得有人盖,而能盖到它的区间里,右端点伸得越远,后面就越省事,所以选最远的不会亏。
举个例子,最后一行 5 格,三个起点的区间是 \([1,2]\)、\([2,4]\)、\([4,5]\)。从 left = 1 开始,能盖住 1 的只有 \([1,2]\),推到 2;left = 3,能盖住 3 的是 \([2,4]\),推到 4;left = 5,用 \([4,5]\) 收尾。一共 3 个起点,刚刚好。
实现上就是 DFS 从第一行每个点往下搜,一边标记 vis,一边用 l[x][y]、r[x][y] 记下这个点能到最后一行的最左、最右位置,最后一行的格子初始化为 l = r = j。有个容易写错的地方:某个格子就算已经访问过了,l、r 也还是要更新的,因为从别的路过来可能拿到更优的端点,只是不用再往下递归了。最后先判无解,有解再按上面的贪心数区间就行。
实现
在最后一行贪心:从 \((n,1)\) 开始,不断寻找左端点落在上一个右端点之内、且右端点最远的区间,统计所需区间数即为答案。
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int n,m;
int dx[]={-1,0,1,0};
int dy[]={0,1,0,-1};//写BFS的时候你应该用过的方向数组
bool vis[N][N];
int a[N][N],l[N][N],r[N][N];//a是输入的海拔,l r是区间
void dfs(int x,int y)
{
vis[x][y]=1;
for(int _=0;_<4;_++)//4是上下左右四个方向
{
int xx=x+dx[_],yy=y+dy[_];//计算现在的点的上下左右
if(xx<1||xx>n||yy<1||yy>m||a[x][y]<=a[xx][yy])continue;
if(!vis[xx][yy])dfs(xx,yy);
l[x][y]=min(l[x][y],l[xx][yy]);
r[x][y]=max(r[x][y],r[xx][yy]);//更新区间
}
}
int main()
{
cin>>n>>m;
memset(l,21e6,sizeof(l));
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)
{
cin>>a[i][j];
if(i==n)l[i][j]=r[i][j]=j;//初始化区间(开局每个点的区间就是它自己)
}
for(int i=1;i<=m;i++)if(!vis[1][i])dfs(1,i);//从最底层开始向上dfs
bool b=1;
int ans=0;//最底层还没有水的节点的数量
for(int i=1;i<=m;i++)if(!vis[n][i]){b=0;ans++;}//最底层有点没有被访问到,说明有点无法流入水中
if(!b)
{
cout<<"0\n"<<ans;//遗憾退场
return 0;
}
int ll=1,rr=r[1][1];//ll是最左边的点,rr是最右边的点
while(ll<=m)
{
for(int i=1;i<=m;i++)if(l[1][i]<=ll)rr=max(rr,r[1][i]);//如果这个点的左边界小于等于ll,那么就可以更新rr
ll=rr+1;//更新ll
ans++;
}
cout<<"1\n"<<ans;//成功
return 0;//好习惯
}
//自己写了半天不是AI
⚠️注意⚠️
如果你TLE:看看你的dfs调用是不是写反了
如果你ans虚高:注意一下累加ans的代码
如果你只获得了60分:在处理区间时的while是<=并非<

浙公网安备 33010602011771号