• AcWing 1088 旅行问题


    \(AcWing\) \(1088\) 旅行问题

    题目传送门

    https://www.acwing.com/solution/content/12796/

    一、题意理解


    给定一个 上有 \(n\) 个节点,编号从 \(1∼n\),以及一辆小车车

    每一个 节点 \(i\) 有一个 权值 \(p_i\) 表示当车 到达该点 时,可以 获得 的 油量

    还有一个 权值 \(d_i\) 表示当车从 节点 \(i\)节点 \(i+1\) 所需要 消耗 的 油量

    现有一辆车想从环上 任意点 出发,顺时针逆时针 绕环一圈走回起点

    行驶的过程中,油量不能为 负数初始油量起点 处所能获得的 油量

    判断能否完成 环圈行驶

    二、暴力怎么做

    输入:
    5
    3 1
    1 2
    5 2
    0 1
    5 4
    

    用测试用例进行人脑模拟,以加快对题目的理解:

    步骤\(1\):进行破环成链:

    \(1->(1)->2->(2)->3->(2)->4->(1)->5 ->(4) ->1\)
    \(3~~~~~~~~~~~~~~~~~~~~~~1~~~~~~~~~~~~~~~~~~~~~~ 5~~~~~~~~~~~~~~~~~~~~~~0~~~~~~~~~~~~~~~~~~~~~~5\)

    第二行的数字:每个节点可以补充的油量,\(p[i-1]\)
    \(()\)中为在行走过程中消耗的油量,\(d[i-1]\)

    \(Q\):为什么是\(p[i-1],d[i-1]\),而不是\(p[i],d[i]\)呢?
    \(A\):以\(2\)号节点为例,到达了\(2\),还没有加上\(2\)号站点的油时,此时剩余油量为\(3-1=2,\)\(p[1]-d[1]=3-1=2\),也就是,到达本级节点时的油量,其实是和前一个、前前一个、...相关的,还与本级无关。

    利用前缀和的思想优化一下,反正一般这种增量、变量的都用前缀和优化,行不行都可以尝试一下~

    定义 \(s[i]=s[i-1]+p[i-1]-d[i-1]\)
    \(s[i]\)的含义是:从\(1\)号节点出发,到达\(i\)号节点,还未取得\(i\)号节点的油量前,油量剩余值。

    道理

    如果我们从\(1\)号点出发,事情还是比较简单的,就是研究一下在整条路线上,每个节点到达、还未加上此节点的油量前,是不是剩余油量全部都大于等于\(0\),如果出现某个节点到达时(未加上本节点的油),
    油量已经小于\(0\),就意味着不可行,因为中途没油是不可能跑到下一个节点的。

    这东西最终看\(s[n+1]\)是不行的,因为可能会出现中途没有了油,不合法的场景出现。如果我们简单的获取\(s[n+1]\),最终的结果可能是大于\(0\),比如我举个简单的栗子:

    2
    100  200
    1000 100
    

    很明显,这个样例是不行的,第一个节点有\(100\)个油,我们加上,走到了第二个节点时,需要消耗200个油,它中途就会没有了油,肯定是跑不到的。
    但如果我们看最终的结果 \(s[n+1]=s[2+1]=s[3]=s[2]+p[n]-d[n]=s[2]+p[2]-d[2]=s[1]+p[1]-d[1]+p[2]-d[2]=0+100-200+1000-100=800\)
    但这明显是不可能的结果!

    \(Q\):如果我们不是从\(1\)号节点出发呢?

    \(A\):因为破环成链后,其实就是一个线性的问题,依然可以使用前缀和进行处理。假设我们从i点出发,则就是在问我们,
    \(j ∈[i+1,i+2,i+3, ... ,i+n]\)\(n\)个点中,\(s[j]-s[i]\)是不是一直大于等于\(0\),如果有一个小于\(0\)的就是不合法

    这里有两种暴力计算的办法,分别是:

    //1、暴力计算方法I
      for (int i = 1; i <= n; i++) { //枚举每个出发点
          bool flag = true;
          for (int j = i + 1; j <= i + n; j++)
              if (s[j] - s[i] < 0) {
                  flag = false;
                  break;
              }
          if (flag) ans[i] = true;
      }
    
      // 2、暴力计算方法II
      for (int i = 1; i <= n; i++) { //枚举每个出发点
          LL Min = LLONG_MAX;
          for (int j = i + 1; j <= i + n; j++) Min = min(Min, s[j]); //记录在哪个点是存油量最少的情况
          // s[j]-s[i]>=0 则表示一直保持油量大于等于0
          if (Min >= s[i]) ans[i] = true;
      }
    

    其实这两种计算方法本质上是一样的,但第二种更聪明些,我们来学习一下第二种方法:

    我找出这些数中的最小值,如果最小值都比指定的数字小,那么整条路径就都比指定数字小!
    这句话是后面优化时采用单调队列的基础,因为这就明显指向了在区间内找出最小值!反倒是第一种思路朴实,无法再想办法优化了,老实人有罪~

    暴力大法

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    const int N = 2000010;
    int n;
    int p[N];
    int d[N];
    LL s[N];
    bool ans[N];
    
    int main() {
        scanf("%d", &n);
    
        for (int i = 1; i <= n; i++) {
            scanf("%d %d", &p[i], &d[i]);
            p[i + n] = p[i];
            d[i + n] = d[i];
        }
        // 顺时针
    
        // p[i-1]-d[i-1]:从i-1点出发,可以获取到p[i-1]的油,到i点,走了一段路程,消耗掉d[i-1]的油量,p[i-1]-d[i-1]:从i-1到i的油量增量(增量有可能是正的,也有可能是负的)
        // 设 s[i]=s[i-1]+ p[i-1] -d[i-1]
        // 则 s[i]理解为增量和
        // 假如我们从1号点出发,则i∈[2~n+1] 这些点的s[i]必须全部>=0,一旦出现<0的情况,就是中途没油了~
        for (int i = 1; i <= 2 * n; i++) s[i] = s[i - 1] + p[i - 1] - d[i - 1];
    
        // 2、暴力计算方法II
        for (int i = 1; i <= n; i++) { //枚举每个出发点
            LL Min = LLONG_MAX;
            for (int j = i + 1; j <= i + n; j++) Min = min(Min, s[j]); //记录在哪个点是存油量最少的情况
            // s[j]-s[i]>=0 则表示一直保持油量大于等于0
            if (Min >= s[i]) ans[i] = true;
        }
    
        //逆时针
        for (int i = 2 * n; i; i--) s[i] = s[i + 1] + p[i + 1] - d[i]; //这里需要注意一下画图理解下为什么是-d[i]
        for (int i = n + 1; i <= 2 * n; i++) {
            LL Min = LLONG_MAX;
            for (int j = i - 1; j >= i - n; j--) Min = min(Min, s[j]);
            if (Min >= s[i]) ans[i - n] = true;
        }
    
        //枚举输出
        for (int i = 1; i <= n; i++) puts(ans[i] ? "TAK" : "NIE");
        return 0;
    }
    
    

    三、单调队列优化

    假设我们从\(i\)号车站出发,那么一定要确保这段区间\(i+1 − > i + n\)\(s[i]\)大于等于\(0\),所以我们可以用单调递增队列来维护这长度为\(n\)的区间的所有前缀和,这样,我们每次达到\(n\)区间时,我们都可以通过队头元素来判断是否是大于等于\(0\)的。逆时针同理,逆着来看问题即可。详见\(AC\)代码。

    代码实现

    #include <bits/stdc++.h>
    
    using namespace std;
    typedef long long ll;
    const int N = 2000010; //破环成链,双倍长度
    int n, p[N], d[N];
    ll s[N];
    ll a[N];
    
    int q[N];
    bool ans[N];
    
    /*
    顺时针:s[i] 表示 1 ~ i 的剩余花费
    即 s[i] = s[i - 1] - d[i - 1] + p[i - 1]
    
    ans[i] = min(s[i + 1] ~ s[i + n]) - s[i] >= 0
    
    
    逆时针:s[i] 表示 i ~ n 的剩余花费
    即 s[i] = s[i + 1] - d[i] + p[i + 1]
    
    ans[i - n] = min(s[i - n] ~ s[i - 1]) - s[i] >= 0
    */
    int main() {
        scanf("%d", &n);
    
        for (int i = 1; i <= n; i++) {
            scanf("%d %d", &p[i], &d[i]); //此题目数据量 n<=1e6,数据量大,使用scanf进行读取
            p[i + n] = p[i];              //破环成链,复制数据
            d[i + n] = d[i];
        }
    
        /*s[i]的含义:到达i站点,但未加上i站点的油时的剩余油量。
          Q:为什么这样设计概念呢?
          A:(1) 从结果出发进行思考:
                还没加上此站点的油,那么剩余的油量如果大于等于零,说明可以成功到达i站。剩余的油量如果小于零,说明不可以成功到达i站。而我们想要求的就是是否能成功完成全程,
                也就是能成功到达每个站点,如此设计概念,就是从结果出发进行的设计。
           (2) 从边界出发进行思考:
                假设我们从1号站点出发去往2号站,那么s[2]设计成什么呢?
                A:加了1点的油,走了1~2的路,再加了2点油
                B:加了1点的油,走了1~2有路,还没加2点的油
                看明白了吧,我们肯定要选择B的设计思路啊!A是加了两个点的油,那不就把问题混杂在一起了吗?B才是干干净净的处理方法啊。
    
          Q: s[i]是否存在递推关系呢?
          A:肯定存在啊,这就是一个前缀和啊,因为p[i-1]-d[i-1]就是油的增量(当然,可能是负数),s[i-1]就是进入上一个加油站时的剩余油量,这四者之间的关系就是:
            s[i]=s[i-1]+p[i-1]-d[i-1]
    
          Q: 为什么下面的代码从i=1一直计算到n*2呢,计算到n不就行了吗?
          A: 因为破环成链啊,如果我们不把长度转化为两倍的长度,假设共n=5站,计为 1,2,3,4,5 那么从3号点出发,它应该可以回到3号点,应该走 3-4-5-1-2-3,对吧,这样用1~n去表示,
          表示不出来啊,所以我们认为6就是1,7就是2,8就3,也就是让3走到8,相当于回到了3啊。也就是i和i+n其实是同一个站点。思考一下边界情况,
          比如我们假设从1号站点出发:
          s[1],是什么意思呢?根据定义:s[1]表示到达了1号站点,还没有加上1号站点的油时的剩余油量。还没加上油,应该初始值是0.
          s[5],是什么意思呢?根据定义:s[5]表示到达了5号站点,还没有加上5号站点的油时的剩余油量。
          s[6],是什么意思呢?根据定义:s[6]表示到达了6号站点,还没有加上6号站点的油时的剩余油量。因为6其实就是1,也就是回到了1号站点。
    
          如果我们是要从5号站点出发呢?
          s[5],是什么意思呢?根据定义:s[5]表示到达了5号站点,还没有加上5号站点的油时的剩余油量。还没加上油,应该初始值是0.可是现在s[5]肯定是有值的啊,它不是0啊?怎么办?
          这个很好办,从哪个位置出发,就把谁减去这个值就行了,对比思考一下前缀和不就明白了吗?
        */
        for (int i = 1; i <= n * 2; i++) s[i] = s[i - 1] + p[i - 1] - d[i - 1];
    
        //顺时针
        /*Q:如果我们从1号点出发,怎么算是1号点是合法的,可以完成全程呢?
          A: 1号点出发,我们需要考查它的后续 s[2]-s[1],s[3]-s[1],s[4]-s[1],s[5]-s[1],s[6]-s[1]是否最小值都大于等于零,如果是这样,就是合法的起点。
              2号点出发,我们需要考查它的后续 s[3]-s[2],s[4]-s[2],s[5]-s[2],s[6]-s[2],s[7]-s[2]是否最小值都大于等于零,如果是这样,就是合法的起点。
              ...
              上面这样写不是很聪明的写法,可以转为:
              1号点出发,我们需要考查它的后续 min(s[2],s[3],s[4],s[5],s[6]) >=s[1],如果是这样,就是合法的起点。
              2号点出发,我们需要考查它的后续 min(s[3],s[4],s[5],s[6],s[7]) >=s[2],如果是这样,就是合法的起点。
              ...
              区间长度为5,一直要求其区间内的最小值,用单调队列可以优化啊~
              
              在分析考查s[1]时,我们需要知道区间[2~6]之内的最小值,如果我们正序遍布进行DP数据填充的话,1是第1个填充的,它还不知道区间[2~6]之间的最小值是多少,那怎么DP啊?
              !!!正难则反!!!,此题告诉我们,当我们发现DP依赖的数据无法正序准备好的时候,我们可以考虑反着来试试:
              如果我们从最后一个2*n开始倒序遍历,就会准备好一个滑动窗口,内容就是当前遍历到节点的后序最长范围n之内的s[j]最小值,当前枚举到的位置i,可以以O(1)的时间直接调用q[hh]
              就可以获得它后面最长n个长度范围内的s[j]最小值所在的位置!这样滑动窗口就用的上了!
    
              经验总结:依赖的滑动窗口在右侧,倒序枚举计算
        */
        int hh = 0, tt = 0;
        q[0] = n * 2 + 1; //倒序枚举,哨兵节点是最后一个节点ID+1
        //每个节点,都需要考查距离自己i+1~i+n内的后续范围内的s[j]最小值,单调队列在i的右侧,需要倒序遍历
        for (int i = n * 2; i; i--) {
            /*
             Q: 为什么是q[hh] > i + n ?
             你看我代码的注释,就讨论顺时针吧,逆时针类似。
             讨论从 i 开始,绕一圈等于从 i 走到它倍长的那个点 i+n,所以是找 i+1 到 i+n 的最小值,
             [i+1,i+n] 这个区间长度是 n,算完 i 的答案,才会加入 s[i] 到单调队列中
             */
            while (hh <= tt && q[hh] > i + n) hh++; //保持窗口的n个最长数据
    
            /*这句是本题的题意相关,与单调队列框架无关
            只关心1~n之间的信息(n+1..2*n都是铺垫)
            如果[1~n]中的某个位置的右侧n个数据范围内的最小石油剩余量,大于以i为出发点时的石油剩余量s[i],则全部节点可以到达!
            */
            if (i <= n && s[q[hh]] >= s[i]) ans[i] = true;
    
            //准备i入队列,保留 活的小+比较小的值
            while (hh <= tt && s[q[tt]] >= s[i]) tt--;
            q[++tt] = i;
        }
    
        //后缀和
        /**
         s[i] 表示 i ~ n 的剩余油量
         */
        for (int i = 2 * n; i; i--) s[i] = s[i + 1] + p[i + 1] - d[i]; //注意一下这个后缀和的计算方法
        hh = 0, tt = 0;
        q[0] = 0; //哨兵
        //每个节点,都需要考查距离自己i+1~i+n内的前序范围,单调队列在i的左侧,需要正序遍历
        for (int i = 1; i <= 2 * n; i++) {
            while (hh <= tt && q[hh] < i - n) hh++; //保持窗口的n个最长数据
    
            /*这句是本题的题意相关,与单调队列框架无关
            只关心n+1~2*n之间的信息(1..n都是铺垫)
            如果[n+1~2*n]中的某个位置的左侧n个数据范围内的最小石油剩余量,大于以i为出发点时的石油剩余
            量s[i],则全部节点可以到达!
            */
            if (i > n && s[q[hh]] >= s[i]) ans[i - n] = true;
    
            //准备i入队列,保留 活的小+比较小的值
            while (hh <= tt && s[q[tt]] >= s[i]) tt--;
            q[++tt] = i;
        }
    
        //输出
        for (int i = 1; i <= n; i++) puts(ans[i] ? "TAK" : "NIE");
        return 0;
    }
    
  • 相关阅读:
    Linux系统 虚拟化篇之KVM
    Linux系统 Top命令
    Linux系统 日常优化
    模拟浏览器多文件上传
    51nod 1315 合法整数集 (位操作理解、模拟、进制转换)
    51nod 1138 连续整数的和(等差数列)
    51nod 1042 数字0-9的数量 (数位dp、dfs、前导0)
    51nod 1136 欧拉函数(数论,用定义解题)
    51nod 1106 质数检测(数论)
    51nod 1006 最长公共子序列Lcs(dp+string,无标记数组实现)
  • 原文地址:https://www.cnblogs.com/littlehb/p/15812821.html
Copyright © 2020-2023  润新知