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}\)

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

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

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

等等!数据范围写着:

\[1 \leqslant t_i \leqslant 4\times10^6 \]

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

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

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

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

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

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

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

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

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

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

    • 第三个条件是\(l\leqslant (t_i+j)-m-t_k\)(即\(t_k+l\leqslant (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,l\leqslant \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\times nm\times n)=O(n^3m^2)\)

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

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


三、程序实现 or 剪枝

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

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

    然后,\(k\)\(1\)开始枚举,每当\(k\)在循环一开始等于某个值\(x\)时,\(val\)就减去\((t_i+j-t_x)\times 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{j\leqslant \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;
}
posted @ 2019-07-31 16:56  info___tion  阅读(1476)  评论(0编辑  收藏  举报