zoukankan      html  css  js  c++  java
  • NOIP 2018【摆渡车】题解

    updated on 2019.10.25:

    1. 以前的程序真的丑……现在已经把码风改良了;

    2. 去掉了之前那些完全是行为艺术的优化。

      向那些被我的naive讲解和丑陋程序劝退的盆友们诚恳地道个歉(雾

      (因为笔者是(2020)届中考生,所以这应该是最后一次大改了,以后就不会有时间了……)


    建议大家在博客里食用:传送门


    要是PJ组再考这么难的DP,我就当官把CCF取缔了


    开个玩笑。

    此题正解:(mathrm{DP})+各种剪枝 or 优化


    一、引理

    • 对于每个乘客,能搭载ta的车的发车时间只有(m)种情况
    • 设这个乘客开始等候的时间是(t_i),则对应的(m)种情况是([t_i,t_i+m))

    证明

    1. 如果存在一种情况,其发车时间是(geqslant t_i+m)的,则由题意可知,发车时间可以提早若干轮(也就是减去若干个(m)到达([t_i,t_i+m))这个区间,这样做不会影响发车时间(geqslant t_i+m)的那趟车
    2. 如果(<m)的话,那这个乘客根本就坐不上这趟车,所以不需要考虑。

    二、基本思想

    • 首先,题目给定我们的这(n)个人开始等候的时间是乱的,所以我们要先按照开始等车的时间把这(n)个人排个序,然后再离散化(具体来说就是将等待时间相同的若干个人“合并”成一个人)

      在结构体中,用pos表示这一堆人的等待时间num表示这一堆人的人数。(具体过程看代码)

    • (f(i,j))表示用摆渡车已经载了前(i)个人,且搭载了第(i)个人(不一定只搭载第(i)个人)的那趟摆渡车的发车时间是((t_i+j))的最小等候时间和。((t_i)的意义与题意相同)

    • 这里要注意:(t_i+j)除了要满足(j<m)(对应上面的引理),同时还要满足(j<t_{i+1}-t_i)(即(t_i+j<t_{i+1})

      • 因为如果(jgeqslant t_{i+1}),那这趟车就可以把第(i+1)个人也搭上了,显然违反了(mathrm{DP})状态的定义

        (在代码中,我们用一个名为border(i)#define表示了这两个限制)

    • 对于每个(f(i,j)),枚举上一趟摆渡车的出发时间。

    等等!数据范围写着:

    [1 leqslant t_i leqslant 4 imes10^6 ]

    你跟我说枚举时间?你这最起码都(O(nt_i) sim mathrm{T}(2 imes10^9)) 的时间复杂度了,怎么(mathrm{AC})

    别着急啊,我还没说完呢。

    • 其实引理已经告诉我们,我们不需要把整个(t_i)枚举完

      由引理可得,对于前(i-1)个乘客,每个乘客能搭载的摆渡车的发车时间只有(m)种情况,所以我们只需要枚举这((i-1) imes m)种情况即可。其他情况都是废的,不需要去考虑。

      这样做的枚举量为(O(nm) sim mathrm{T}(5 imes 10^4)),相比之前直接枚举(t_i)的时间复杂度(mathrm{T}(4 imes 10^6))来讲,已经小很多了。

    • 接着,假设前一趟摆渡车已经载了前(k)个人,那么我们要做的就只有两件事:

      1. 再枚举一个(l),得到(f(k,l))的最小值。
      2. 计算出第(k+1)个人到第(i)个人等候当前这趟摆渡车的等候时间和。

    敲重点!敲重点!敲重点!

    • 这里,(l)的取值范围有三个条件:

      • 前两个条件和前面的border()一样,不再赘述。

      • 第三个条件是(lleqslant (t_i+j)-m-t_k)(即(t_k+lleqslant (t_i+j)-m)

        • 原因很简单,如果(t_k+l> (t_i+j)-m),那么两趟车之间相隔的时间肯定就(<m),显然不合题意。

          (所以这里还要再定义一个border2(i)=min( border(i),第三个条件 )

    • 在状态转移方程中的体现就是:

      [f(i,j)=min_limits{0 leqslant k < i,lleqslant mathrm{border2(k)}} {f(k,l)+col(k+1,i,t_i+j)} ]

      这当中,(col(k+1,i,t_i+j))表示(k+1)个乘客到第(i)个乘客等候发车时间为(t_i+j)的那趟摆渡车的时间和,直接用一个for循环累计即可。

      (当然,当(k=0)时,(f(k,l))恒为零,表示这趟车直接把前(i)个人全部载完,这时等式右侧就直接等于(col(1,i,t_i+j))

    • 算一下上面这个状态转移方程的时间复杂度:

      1. 首先,(i)(j)必须枚举,所以是(O(nm))的。
      2. 其次,(k)(l)也是要枚举的,所以又是一个(O(nm))
      3. 最后,每次枚举(i,j,k,l),都要计算一次(col)函数,而这个(col)函数的时间复杂度是(O(n))的。

      综上所述,这个状态转移方程的时间复杂度为(O(nm imes nm imes n)=O(n^3m^2))

      这时间复杂度……也太可观了吧

      所以我们需要优化!优化!优化!


    三、程序实现 or 剪枝

    • 我们来关注一下这个式子:$$col(k+1,i,t_i+j)$$
      对于每个(i,j),当(k)每增加(1)时,(col)的值就只会减掉((t_i+j-t_k) imes num_k)(num_k)就是上文中提到的,结构体中(k)堆人的人数)。

      所以我们可以在枚举每个(i)(j)时,就把(col(1,i,t_i+j))算出来(用一个变量(val)存起来)

      然后,(k)(1)开始枚举,每当(k)在循环一开始等于某个值(x)时,(val)就减去((t_i+j-t_x) imes num_x)

      状态转移方程就变为:$$f(i,j)=min_limits{0 leqslant k < i,l leqslant mathrm{border2(k)}} {f(k,l)+val}$$

      这样一抽出来,时间复杂度就变成了(O(nm(n+nm))=O(n^2m+n^2m^2))
      只保留最高次项后,时间复杂度就降为了(O(n^2m^2))这就是(60)分的做法!


    • 其实大家有没有想过,枚举(l)这个操作显得有些多余,可不可以省去呢?(毕竟只是求一个最小值而已,我求完一次就把这个最小值存起来不就行了吗?)

      没错,上面的想法是正确的!

    • 我们开多一个数组(mathrm{Min}(i,j)= min_limits{jleqslant mathrm{border}(i)} {f(i,j)})

      则之前的状态转移方程可以简化为:

      [f(i,j)=min_limits{0 leqslant k < i} {mathrm{Min}(k,mathrm{border2}(k))+val} ]

      (mathrm{Min})可以在求每个(f(k,l))的时候顺带维护。

      因为这里只枚举了(i,j,k),所以(mathrm{DP})的时间复杂度是(O(n^2m))!!!

    这个时间复杂度可以通过本题!!!


    四、考场代码

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    
    const int maxn=502,maxm=102;
    const int INF=0x7fffffff;
    
    #define border2(x) min( border(x),lpos-Mem[x].pos )	//第三个条件是用小于等于号连接的,所以不用-1
    #define border(x) min( m-1,Mem[x+1].pos-Mem[x].pos-1 )	//因为两个条件都是用小于号连接的,所以在循环中要-1
    
    int f[maxn][maxm];
    int Min[maxn][maxm];
    
    int a[maxn];
    
    struct Node{int pos,num;}Mem[maxn];int sz;
    
    int col(int l,int r,int pos)
    {
        int res=0;
        for(int i=l;i<=r;i++) res+=(pos-Mem[i].pos)*Mem[i].num;
    
        return res;
    }
    
    int main()
    {
        int n,m;
        scanf("%d%d",&n,&m);
    
        for(int i=1;i<=n;i++) scanf("%d",&a[i]);
        sort(a+1,a+n+1);
    
    	a[0]=-1;
    
        for(int i=1;i<=n;i++)
        {
            if( a[i]!=a[i-1] ) Mem[++sz].pos=a[i];
            Mem[sz].num++;
        }
    
        Mem[sz+1]=(Node){INF,0};
        n=sz;
    
        for(int i=1;i<=n;i++) for(int j=0;j<=m;j++) f[i][j]=Min[i][j]=INF;
    
        for(int i=1;i<=n;i++)
            for(int j=0;j<=border(i);j++)
            {
                int pos=Mem[i].pos+j,lpos=pos-m;
    
                int val=col(1,i,pos);
                f[i][j]=val;
    
                for(int k=1; k<i and Mem[k].pos<=lpos ;k++)
                {
                    val-=(pos-Mem[k].pos)*Mem[k].num;
                    f[i][j]=min( f[i][j],Min[k][border2(k)]+val );
                }
                
                Min[i][j]=f[i][j];
                if( j>0 ) Min[i][j]=min( Min[i][j],Min[i][j-1] );
            }
    
        printf("%d",Min[n][m-1]);return 0;
    }
    
  • 相关阅读:
    【1801日語听解4】第14回:6月9日
    【日語听解2】第14回:6月8日
    【日語視聴説2】第14回:6月8日
    【日本語新聞編集】第13回:6月5日
    【1801日語写作】第13回:6月4日
    【日本語新聞選読】第13回:6月2日
    Win10版本
    家庭或小规模无线使用方
    批处理bat复制命令Copy与Xcopy
    批处理bat删除命令
  • 原文地址:https://www.cnblogs.com/info---tion/p/11277515.html
Copyright © 2011-2022 走看看