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}\))
-
因為如果\(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\),得到\(f(k,l)\)的最小值。
- 計算出第\(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)\))
-
算一下上面這個狀態轉移方程的時間復雜度:
- 首先,\(i\)和\(j\)必須枚舉,所以是\(O(nm)\)的。
- 其次,\(k\)和\(l\)也是要枚舉的,所以又是一個\(O(nm)\)。
- 最后,每次枚舉\(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;
}
又是一年過去了,這里就祝大家\(\mathrm{CSP\ 2019\ J/S}\)認證rp++!
(話說,老子打完這次也要隱退了呢。。。