updated on 2019.10.25:
-
以前的程序真的丑……现在已经把码风改良了;
-
去掉了之前那些完全是行为艺术的
屑优化。向那些被我的naive讲解和丑陋程序劝退的盆友们诚恳地道个歉(雾(因为笔者是(2020)届中考生,所以这应该是最后一次大改了,以后就不会有时间了……)
建议大家在博客里食用:传送门
要是PJ组再考这么难的DP,我就当官把CCF取缔了
开个玩笑。
此题正解:(mathrm{DP})+各种剪枝 or 优化
一、引理
- 对于每个乘客,能搭载ta的车的发车时间只有(m)种情况;
- 设这个乘客开始等候的时间是(t_i),则对应的(m)种情况是([t_i,t_i+m))。
证明
- 如果存在一种情况,其发车时间是(geqslant t_i+m)的,则由题意可知,发车时间可以提早若干轮(也就是减去若干个(m))到达([t_i,t_i+m))这个区间,这样做不会影响发车时间(geqslant t_i+m)的那趟车。
- 如果(<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)个人,那么我们要做的就只有两件事:
- 再枚举一个(l),得到(f(k,l))的最小值。
- 计算出第(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)))
-
算一下上面这个状态转移方程的时间复杂度:
- 首先,(i)和(j)必须枚举,所以是(O(nm))的。
- 其次,(k)和(l)也是要枚举的,所以又是一个(O(nm))。
- 最后,每次枚举(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;
}