博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
NOIP 2018【摆渡车】题解
阅读量:4702 次
发布时间:2019-06-09

本文共 5034 字,大约阅读时间需要 16 分钟。

updated on 2018.11.17:

1. 之前所说的最后一项优化经检验发现有\(bug\),已经去掉;

2. 对文中的一些错误进行了修改,同时对文章的行文作了一些调整,便于大家理解。


建议大家在博客里食用:


要是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+m\)的那趟车,不会有后效性

  2. 如果\(<m\)的话,那这个乘客根本就坐不上这趟车,所以不需要考虑。


二、基本思想

  • 首先,题目给定我们的这\(n\)个人开始等候的时间是乱的,所以我们要先按照开始等车的时间把这\(n\)个人排个序

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

    这里要注意:\(t_i+j\)同时还需要满足\(t_i+j<t_{i+1}\)

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

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

等等!数据范围写着:

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

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

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

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

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

    这样做的枚举量为\(O(nm) \sim O(5 \times 10^4)\),相比之前直接枚举\(t_i\)的时间复杂度\(O(4 \times 10^6)\)来讲,已经是飞跃了。

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

    1. 再枚举一个\(l\),得到\(f[k][l]\)的最小值。
    2. 计算出第\(k+1\)个人到第\(i\)个人等候当前这趟摆渡车的等候时间和。
  • 在状态转移方程中的体现就是:

    \[f[i][j]=\min_{0 \leqslant k < i,0 \leqslant l < m} \{f[k][l]+col(k+1,i,t_i+j)\}\]

    这当中,\(col(k+1,i,t_i+j)\)表示\(k+1\)个乘客到第\(i\)个乘客等候发车时间为\(t_i+j\)的那趟摆渡车的时间和。

    这里也有一个地方要注意:在枚举\(k\)\(l\)时,要满足\(t_k+l<t_{k+1}\)

    理由和上面一样,是因为这样做违反了\(\mathrm{DP}\)中状态的定义

    其实上面的这个状态转移方程并不严谨,不过本人会慢慢把它修改得严谨起来(其实下文中的修改过程就是本人在考场上的思路)。

    但这个方程很重要,以后的所有优化全都源自于这个方程,为了节省篇幅,本人不会重述这个方程,所以请大家记住这个转移方程。

  • 先抛开正确性,我们可以来计算一下上面这个状态转移方程的时间复杂度。

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

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

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

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


三、程序实现 or 剪枝

  • 我们可以发现,这\(n\)个人中有一些人开始等候的时间是相同的

    对于这些人,我们可以进行去重(开一个结构体,把相同时间的人压进一个结构体里面,结构体里则用一个变量表示这些人开始等候的时间,用另一个变量表示这些重复的人的人数)

    虽然这样做时间复杂度会多增加了\(O(n\log n)\),但这样可以在\(\mathrm{DP}\)减少很多繁琐的特判


  • 我们来关注一下这个式子:\[col(k+1,i,t_i+j)\]

    对于每个\(i,j\),当\(k\)每增加一时,\(col\)的值就只会减掉\((t_i+j-t_k)*size_k\)\(size_k\)表示结构体中第\(k\)个元素的重复的人的数量)。

    所以我们可以在枚举每个\(i\)\(j\)时,就把\(col(k+1,i,t_i+j)\)算出来(用一个变量\(val\)存起来),然后,每当\(k\)增加\(1\)\(val\)就减去\((t_i+j-t_k)*size_k\)

    状态转移方程就变为:\[f[i][j]=\min_{0 \leqslant k < i,0 \leqslant l < m} \{f[k][l]+val\}\]

    这样一抽出来,时间复杂度就变成了\(O(nm(n+nm))=O(n^2m+n^2m^2)\)

    只保留最高次项后,时间复杂度就降为了\(O(n^2m^2)\)!!!又是一个飞跃!!!

注意!接下来的内容全都是难点,请大家做好心理准备!


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

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

    \[Min[i]= \min_{0 \leqslant j < m,t_i+j<t_{i+1}} \{f[i][j]\}\]

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

    \[f[i][j]=\min_{0 \leqslant k < i} \{Min[k]+val\}\]

    \(Min[k]\)可以在求每个\(f[k][l]\)的时候顺带维护

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

    ## 这个时间复杂度足以通过本题了!!!

    但是,这样做真的是对的吗?

    假设有这样一个例子:

    T3.png

    (数轴上的两个点表示的是乘客开始等候的时间,圈3和圈4表示两个乘客在(结构体)数组中的编号)

    这时,我们直接用\(Min[3]\)来为\(f[4]\)\(\mathrm{DP}\)就不行了。

    假设我们要求\(f[4][0]\)的值(也就是说最后一趟车的发车时间和第4个乘客开始等候的时间相同的情况),当我们枚举到\(k=3\)的情况时,我们只能取\(0 \leqslant l \leqslant 1\)的情况。(如果\(l>1\)的话,前一趟车的时间就有可能和当前这趟车的时间重叠了,违反了\(\mathrm{DP}\)中状态的定义!)

    也就是说,不能直接用\(Min\)数组来\(\mathrm{DP}\)!(因为\(Min[3]\)包含了\(f[3][0]\)\(f[3][4]\)的最小值,直接用会出错!

    那应该怎么办?(我在考场上最绝望的时刻就是思考这个问题的过程)

    后来一想:可不可以直接特判呢?

    ## 事实证明,可以!

    首先,对于每个\(i,j\),我们都为其开一个变量\(lt=t_i+j-m\)作为一个“防护墙”

    对于每个\(k\),如果\(t_k+m>lt\),那么\(Min[k]\)就不适用于\(f[i][j]\)这个状态(因为对于这个\(k\),有些\(t_k+l\)加上\(m\)以后会和\(t_i+j\)重叠

    对于这些\(k\),我们就重新帮它枚举一个\(l\)(满足\(t_k+l \leqslant lt\)不然超过了会重叠)去\(\mathrm{DP}\)

    由此也可以得出,如果\(t_k>lt\),就可以直接退出当前循环了。(因为你没有\(l\)再去枚举了)

    这样一来,状态转移方程就开始有些恶心了:

    \[f[i][j]=\begin{cases} \min\ \{Min[k]+val\} & t_k+m \leqslant lt \\ \min\limits_{t_k+l \leqslant lt} \{ f[k][l]+val \} & t_k+m>lt \end{cases}\]

    这样优化后的时间复杂度是多少呢?

    首先,无论如何,\(i(n)\)\(j(m)\)是一定要枚举的,所以时间复杂度至少会有\(O(nm)\)

    然后,我们可以把两个方程分开考虑:

    1. 第一个方程枚举了\(k(n)\),时间复杂度为\(O(n)\)

    2. 因为\(k\)\(l\)总共只会计算\(m\)次(因为只有当\(lt-t_k<m\)时才会进入第二种情况。而\(l\)每次至少增长\(1\),最慢也就是\(O(m)\)而已)

      所以第二条方程的时间复杂度为\(O(m)\)

    加起来就是\(O(nm(n+m))\),因为\(n>m\),所以可以简化成\(O(n^2m)\)这个时间复杂度可以通过本题!

    至此,我们终于得出了此题的可行解法!


四、考场代码

我的代码在结构体边界上有一些预处理,这个重在意会即可相信大家应该看得懂

#include
#include
using namespace std;const int maxn=502,maxm=102;const int INF=0x7fffffff;int f[maxn][maxm];int Min[maxn];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 argc, char const *argv[]){ 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); Mem[0].pos=-m*2-2; 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].pos=Mem[sz].pos+m+2; for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) f[i][j]=INF; Min[i]=INF; } Min[0]=0; for(int i=1;i<=sz;i++) for(int j=0;j

\(In\ a\ word\),祝大家\(\mathrm{NOIP\ rp}\)++!

转载于:https://www.cnblogs.com/info---tion/p/11277515.html

你可能感兴趣的文章
CF1168B Good Triple
查看>>
CF1208E Let Them Slide
查看>>
AT2000 Leftmost Ball
查看>>
CF1086E Beautiful Matrix
查看>>
在单位上班的25条建议(建议收藏)
查看>>
web前端--http协议
查看>>
欧拉定理证明&阶乘的逆元
查看>>
Prime Game Gym - 101981J(网络流/二分图)
查看>>
Teamwork Gym - 101492E (dp)
查看>>
No Link, Cut Tree! Gym - 101484F(dp)
查看>>
Coprimes Gym - 101492C(bitset)
查看>>
Partial Tree UVALive - 7190(完全背包)
查看>>
『深度应用』NLP机器翻译深度学习实战课程·零(基础概念)
查看>>
『开发技术』Windows极简安装使用face_recognition实现人脸识别
查看>>
『深度应用』NLP命名实体识别(NER)开源实战教程
查看>>
『开发技术』GPU训练加速原理(附KerasGPU训练技巧)
查看>>
『深度应用』NLP机器翻译深度学习实战课程·壹(RNN base)
查看>>
『深度应用』一小时教你上手MaskRCNN·Keras开源实战(Windows&Linux)
查看>>
『王霸之路』从0.1到2.0一文看尽TensorFlow奋斗史
查看>>
系统测试中需要注意的点
查看>>