P1514 [NOIP 2010 提高组] 引水入城 笔记

前言

本蒟蒻刷题时看到 dalao 的题解,思考 114514 秒后才得出答案,因此写下如下见解,希望能帮到大家。

原题解链接

  • 使用AI优化,原文难看的要死

题目简介

MC版

你有一个空间,可以在最上面一行放水,水会顺着方块往低处流。

问:下面的地能不能全被水铺满?

如果不能,那就没辙了,顺便数数还有几块地是干的。

如果能,由于你是恒大集团的一员,要尽可能地节省经费,求最少放几个水源。

每个格子上的数字,就是那地方堆了几个方块(高度)。

普通版

给定一个 \(n \times m\) 的矩形,每个格子有一个高度。

你可以在第一行的任意多个格子放水,水会从高处往低处流(只能流到相邻且高度严格更低的格子)。

问:

  1. 能否让最后一行的每个格子都有水流到?如果不能,输出无解,并给出最后一行有多少格子没有水;
  2. 如果能,最少需要在第一行放几个水源,才能使最后一行全部有水。

正文

无解的情况好判断:从第一行每个格子往里灌,搜一遍标记能到的格子,最后一行要是还有没被标记的,那就是无解,答案就是没标记的格子数。这部分没什么好说的。

剩下是有解的情况。有解的时候,一条水流到最后一行的覆盖一定是一段连续的区间,不会中间断一截。为什么呢?

假设一条水流分叉了,中间空出一块蓝色区域。既然题目要求最后一行全都有水,那这块蓝色区域迟早也得被水填上吧?那填它的那滴水是从哪来的?它总得从上面流下来,而流下来的路上,必然要跟原来那条水流碰头。一碰头,两条水流在最后一行就连成一片了,中间那截空洞根本留不下来。

  • 注:此为原题解内的图片,借用致歉,侵私删。

既然蓝色区域有水流过去,就肯定会有一条水流流进蓝色区域。

为什么?上面不解释了吗你还问

  • 注:此为原题解内的图片,借用致歉,侵私删。

那为什么不能是下面这种情况呢?

  • 注:此为自己手绘,画风不一样致歉。

因为这样就跟上面的水流矛盾了。

为什么?上面的水流就应该是如图,所以举一反三,压根不存在如上的水流。

说白了,有解的情况下不可能留一块没水过去的蓝色区域,因为题目要求最后一行必须全部有水。所以水流只能是下面这样:

  • 注:此为原题解内的图片,借用致歉,侵私删。

想通这一点,问题就变成:每个第一行的格子,灌下去之后能覆盖最后一行的一段区间 \(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)\) 开始,不断寻找左端点落在上一个右端点之内、且右端点最远的区间,统计所需区间数即为答案。

cpp 复制代码
#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是<=并非<