之前的程序真的丑……如今已经把码风改良了;html
去掉了以前那些彻底是行为艺术的屑优化。数组
向那些被个人naive讲解和丑陋程序劝退的盆友们诚恳地道个歉(雾函数
(由于笔者是\(2020\)届中考生,因此这应该是最后一次大改了,之后就不会有时间了……)优化
建议你们在博客里食用:传送门spa
要是PJ组再考这么难的DP,我就当官把CCF取缔了code
开个玩笑。htm
首先,题目给定咱们的这\(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\)我的,那么咱们要作的就只有两件事:
这里,\(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)\))
算一下上面这个状态转移方程的时间复杂度:
综上所述,这个状态转移方程的时间复杂度为\(O(nm\times nm\times n)=O(n^3m^2)\)。
这时间复杂度……也太可观了吧
因此咱们须要优化!优化!优化!
咱们来关注一下这个式子:\[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; }
又是一年过去了,这里就祝你们\(\mathrm{CSP\ 2019\ J/S}\)认证rp++!
(话说,老子打完此次也要隐退了呢。。。