NOIP 2018【摆渡车】题解

updated on 2019.10.25:

  1. 之前的程序真的丑……如今已经把码风改良了;html

  2. 去掉了以前那些彻底是行为艺术的优化。数组

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

    (由于笔者是\(2020\)届中考生,因此这应该是最后一次大改了,之后就不会有时间了……)优化


建议你们在博客里食用:传送门spa


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


开个玩笑。htm

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


1、引理

  • 对于每一个乘客,能搭载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\)的话,那这个乘客根本就坐不上这趟车,因此不须要考虑。

2、基本思想

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

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

  • \(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)\)

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

    因此咱们须要优化!优化!优化!


3、程序实现 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)\)!!!

这个时间复杂度能够经过本题!!!


4、考场代码

#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;
}

又是一年过去了,这里就祝你们\(\mathrm{CSP\ 2019\ J/S}\)认证rp++!

(话说,老子打完此次也要隐退了呢。。。

相关文章
相关标签/搜索