NOIP 2018【摆渡车】题解
updated on 2018.11.17:
1. 之前所說的最后一項(xiàng)優(yōu)化經(jīng)檢驗(yàn)發(fā)現(xiàn)有\(bug\),已經(jīng)去掉;
2. 對文中的一些錯(cuò)誤進(jìn)行了修改,同時(shí)對文章的行文作了一些調(diào)整,便于大家理解。
建議大家在博客里食用:傳送門
要是PJ組再考這么難的DP,我就當(dāng)官把CCF取締了
開個(gè)玩笑。
此題正解:\(\mathrm{DP}\)+各種剪枝 or 優(yōu)化
一、引理
對于每個(gè)乘客,能搭載ta的車的發(fā)車時(shí)間只有\(m\)種情況;
設(shè)這個(gè)乘客開始等候的時(shí)間是\(t_i\),則對應(yīng)的\(m\)種情況是\([t_i,t_i+m)\)(方括號 和 圓括號 表示左閉右開區(qū)間)。
證明
如果存在一種情況,其發(fā)車時(shí)間是\(\geqslant t_i+m\)的,則由題意可知,發(fā)車時(shí)間可以提早若干輪(也就是減去若干個(gè)\(m\))到達(dá)\([t_i,t_i+m)\)這個(gè)區(qū)間,這樣做不會影響發(fā)車時(shí)間\(\geqslant t+m\)的那趟車,不會有后效性。
如果\(<m\)的話,那這個(gè)乘客根本就坐不上這趟車,所以不需要考慮。
二、基本思想
首先,題目給定我們的這\(n\)個(gè)人開始等候的時(shí)間是亂的,所以我們要先按照開始等車的時(shí)間把這\(n\)個(gè)人排個(gè)序。
設(shè)\(f[i][j]\)表示用擺渡車已經(jīng)載了前\(i\)個(gè)人,且搭載了第\(i\)個(gè)人(不一定只搭載第\(i\)個(gè)人)的那趟擺渡車的發(fā)車時(shí)間是(\(t_i+j\))的最小等候時(shí)間和。(\(t_i\)的意義與題意相同)
這里要注意:\(t_i+j\)同時(shí)還需要滿足\(t_i+j<t_{i+1}\)
因?yàn)槿绻?span id="ozvdkddzhkzd" class="math inline">\(\geqslant t_{i+1}\),那這趟車就可以把第\(i+1\)個(gè)人也搭上了,違反了\(\mathrm{DP}\)狀態(tài)的定義。對于每個(gè)\(f[i][j]\),枚舉上一趟擺渡車的出發(fā)時(shí)間。
等等!數(shù)據(jù)范圍寫著:
\[1 \leqslant t_i \leqslant 4\times10^6 \]
你跟我說枚舉時(shí)間?你這最起碼都\(O(n*t_i) \sim O(2\times10^9)\) 的時(shí)間復(fù)雜度了,怎么\(AC\)?
別著急啊,我還沒說完呢。
其實(shí)引理已經(jīng)告訴我們,我們不需要把整個(gè)\(t_i\)枚舉完。
由引理可得,對于前\(i-1\)個(gè)乘客,每個(gè)乘客能搭載的擺渡車的發(fā)車時(shí)間只有\(m\)種情況,所以我們只需要枚舉這\((i-1)\times m\)種情況即可。其他情況都是廢的,不需要去考慮。
這樣做的枚舉量為\(O(nm) \sim O(5 \times 10^4)\),相比之前直接枚舉\(t_i\)的時(shí)間復(fù)雜度\(O(4 \times 10^6)\)來講,已經(jīng)是飛躍了。
接著,假設(shè)前一趟擺渡車已經(jīng)載了前\(k\)個(gè)人,那么我們要做的就只有兩件事:
- 再枚舉一個(gè)\(l\),得到\(f[k][l]\)的最小值。
- 計(jì)算出第\(k+1\)個(gè)人到第\(i\)個(gè)人等候當(dāng)前這趟擺渡車的等候時(shí)間和。
在狀態(tài)轉(zhuǎn)移方程中的體現(xiàn)就是:
\[f[i][j]=\min_{0 \leqslant k < i,0 \leqslant l < m} \{f[k][l]+col(k+1,i,t_i+j)\}\]
這當(dāng)中,\(col(k+1,i,t_i+j)\)表示第\(k+1\)個(gè)乘客到第\(i\)個(gè)乘客等候發(fā)車時(shí)間為\(t_i+j\)的那趟擺渡車的時(shí)間和。
這里也有一個(gè)地方要注意:在枚舉\(k\)和\(l\)時(shí),要滿足\(t_k+l<t_{k+1}\)。
理由和上面一樣,是因?yàn)檫@樣做違反了\(\mathrm{DP}\)中狀態(tài)的定義。其實(shí)上面的這個(gè)狀態(tài)轉(zhuǎn)移方程并不嚴(yán)謹(jǐn),不過本人會慢慢把它修改得嚴(yán)謹(jǐn)起來(其實(shí)下文中的修改過程就是本人在考場上的思路)。
但這個(gè)方程很重要,以后的所有優(yōu)化全都源自于這個(gè)方程,為了節(jié)省篇幅,本人不會重述這個(gè)方程,所以請大家記住這個(gè)轉(zhuǎn)移方程。
先拋開正確性,我們可以來計(jì)算一下上面這個(gè)狀態(tài)轉(zhuǎn)移方程的時(shí)間復(fù)雜度。
- 首先,\(i\)和\(j\)必須枚舉,所以是\(O(nm)\)的時(shí)間復(fù)雜度。
- 其次,\(k\)和\(l\)也是要枚舉的,所以又是一個(gè)\(O(nm)\)。
- 最后,每次枚舉\(i,j,k,l\),都要計(jì)算一次\(col\)函數(shù),而這個(gè)\(col\)函數(shù)的時(shí)間復(fù)雜度是\(O(n)\)的。
綜上所述,這個(gè)狀態(tài)轉(zhuǎn)移方程的時(shí)間復(fù)雜度為\(O(nm)*O(nm)*O(n)=O(n^3m^2)\)。
這時(shí)間復(fù)雜度……也太可觀了吧
所以我們需要優(yōu)化!優(yōu)化!優(yōu)化!
三、程序?qū)崿F(xiàn) or 剪枝
我們可以發(fā)現(xiàn),這\(n\)個(gè)人中有一些人開始等候的時(shí)間是相同的。
對于這些人,我們可以進(jìn)行去重(開一個(gè)結(jié)構(gòu)體,把相同時(shí)間的人壓進(jìn)一個(gè)結(jié)構(gòu)體里面,結(jié)構(gòu)體里則用一個(gè)變量表示這些人開始等候的時(shí)間,用另一個(gè)變量表示這些重復(fù)的人的人數(shù))。
雖然這樣做時(shí)間復(fù)雜度會多增加了\(O(n\log n)\),但這樣可以在\(\mathrm{DP}\)時(shí)減少很多繁瑣的特判。
我們來關(guān)注一下這個(gè)式子:\[col(k+1,i,t_i+j)\]
對于每個(gè)\(i,j\),當(dāng)\(k\)每增加一時(shí),\(col\)的值就只會減掉\((t_i+j-t_k)*size_k\)(\(size_k\)表示結(jié)構(gòu)體中第\(k\)個(gè)元素的重復(fù)的人的數(shù)量)。所以我們可以在枚舉每個(gè)\(i\)和\(j\)時(shí),就把\(col(k+1,i,t_i+j)\)算出來(用一個(gè)變量\(val\)存起來),然后,每當(dāng)\(k\)增加\(1\),\(val\)就減去\((t_i+j-t_k)*size_k\)。
狀態(tài)轉(zhuǎn)移方程就變?yōu)?#xff1a;\[f[i][j]=\min_{0 \leqslant k < i,0 \leqslant l < m} \{f[k][l]+val\}\]
這樣一抽出來,時(shí)間復(fù)雜度就變成了\(O(nm(n+nm))=O(n^2m+n^2m^2)\)
只保留最高次項(xiàng)后,時(shí)間復(fù)雜度就降為了\(O(n^2m^2)\)!!!又是一個(gè)飛躍!!!
注意!接下來的內(nèi)容全都是難點(diǎn),請大家做好心理準(zhǔn)備!
其實(shí)大家有沒有想過,枚舉\(l\)這個(gè)操作顯得有些多余,可不可以省去呢?(畢竟只是求一個(gè)最小值而已,我求完一次就把這個(gè)最小值存起來不就行了嗎?)
沒錯(cuò),上面的想法是正確的!
設(shè)
\[Min[i]= \min_{0 \leqslant j < m,t_i+j<t_{i+1}} \{f[i][j]\}\]則之前的狀態(tài)轉(zhuǎn)移方程可以簡化為:
\[f[i][j]=\min_{0 \leqslant k < i} \{Min[k]+val\}\]
\(Min[k]\)可以在求每個(gè)\(f[k][l]\)的時(shí)候順帶維護(hù)
因?yàn)檫@里只枚舉了\(i,j,k\),所以\(\mathrm{DP}\)的時(shí)間復(fù)雜度是\(O(n^2m)\)!!!
## 這個(gè)時(shí)間復(fù)雜度足以通過本題了!!!
但是,這樣做真的是對的嗎?
假設(shè)有這樣一個(gè)例子:
(數(shù)軸上的兩個(gè)點(diǎn)表示的是乘客開始等候的時(shí)間,圈3和圈4表示兩個(gè)乘客在(結(jié)構(gòu)體)數(shù)組中的編號)
這時(shí),我們直接用\(Min[3]\)來為\(f[4]\)做\(\mathrm{DP}\)就不行了。
假設(shè)我們要求\(f[4][0]\)的值(也就是說最后一趟車的發(fā)車時(shí)間和第4個(gè)乘客開始等候的時(shí)間相同的情況),當(dāng)我們枚舉到\(k=3\)的情況時(shí),我們只能取\(0 \leqslant l \leqslant 1\)的情況。(如果\(l>1\)的話,前一趟車的時(shí)間就有可能和當(dāng)前這趟車的時(shí)間重疊了,違反了\(\mathrm{DP}\)中狀態(tài)的定義!)
也就是說,不能直接用\(Min\)數(shù)組來\(\mathrm{DP}\)!(因?yàn)?span id="ozvdkddzhkzd" class="math inline">\(Min[3]\)包含了\(f[3][0]\)到\(f[3][4]\)的最小值,直接用會出錯(cuò)!)
那應(yīng)該怎么辦?(我在考場上最絕望的時(shí)刻就是思考這個(gè)問題的過程)
后來一想:可不可以直接特判呢?
## 事實(shí)證明,可以!
首先,對于每個(gè)\(i,j\),我們都為其開一個(gè)變量\(lt=t_i+j-m\)作為一個(gè)“防護(hù)墻”。
對于每個(gè)\(k\),如果\(t_k+m>lt\),那么\(Min[k]\)就不適用于\(f[i][j]\)這個(gè)狀態(tài)(因?yàn)閷τ谶@個(gè)\(k\),有些\(t_k+l\)加上\(m\)以后會和\(t_i+j\)重疊)
對于這些\(k\),我們就重新幫它枚舉一個(gè)\(l\)(滿足\(t_k+l \leqslant lt\),不然超過了會重疊)去\(\mathrm{DP}\)。
由此也可以得出,如果\(t_k>lt\),就可以直接退出當(dāng)前循環(huán)了。(因?yàn)槟銢]有\(l\)再去枚舉了)
這樣一來,狀態(tài)轉(zhuǎn)移方程就開始有些惡心了:
\[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}\]
這樣優(yōu)化后的時(shí)間復(fù)雜度是多少呢?
首先,無論如何,\(i(n)\)和\(j(m)\)是一定要枚舉的,所以時(shí)間復(fù)雜度至少會有\(O(nm)\)。
然后,我們可以把兩個(gè)方程分開考慮:
第一個(gè)方程枚舉了\(k(n)\),時(shí)間復(fù)雜度為\(O(n)\)
因?yàn)?span id="ozvdkddzhkzd" class="math inline">\(k\)和\(l\)總共只會計(jì)算\(m\)次(因?yàn)橹挥挟?dāng)\(lt-t_k<m\)時(shí)才會進(jìn)入第二種情況。而\(l\)每次至少增長\(1\),最慢也就是\(O(m)\)而已)
所以第二條方程的時(shí)間復(fù)雜度為\(O(m)\)。
加起來就是\(O(nm(n+m))\),因?yàn)?span id="ozvdkddzhkzd" class="math inline">\(n>m\),所以可以簡化成\(O(n^2m)\)!這個(gè)時(shí)間復(fù)雜度可以通過本題!
至此,我們終于得出了此題的可行解法!
四、考場代碼
我的代碼在結(jié)構(gòu)體邊界上有一些預(yù)處理,這個(gè)重在意會即可相信大家應(yīng)該看得懂
#include<stdio.h> #include<algorithm> 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<min(m,Mem[i+1].pos-Mem[i].pos);j++){int pos=Mem[i].pos+j,lpos=pos-m;int val=col(1,i,pos);f[i][j]=val;for(int k=0; k<i and Mem[k].pos<=lpos ;k++){val-=(pos-Mem[k].pos)*Mem[k].num;//如果Mem[k+1].pos-1<=lpos,那么Min[k]照樣能用(因?yàn)镸em[k+1].pos-1也是l的邊界之一,只要l小于兩個(gè)邊界中較小的那個(gè)值,就可以直接用Min[k])if( min( Mem[k].pos+m-1,Mem[k+1].pos-1 )<=lpos ) f[i][j]=min( f[i][j],Min[k]+val );else{for(int kk=0; Mem[k].pos+kk<Mem[k+1].pos and Mem[k].pos+kk<=lpos and kk<m;kk++)f[i][j]=min( f[i][j],f[k][kk]+val );}}Min[i]=min( Min[i],f[i][j] );}printf("%d",Min[sz]);return 0; }\(In\ a\ word\),祝大家\(\mathrm{NOIP\ rp}\)++!
轉(zhuǎn)載于:https://www.cnblogs.com/info---tion/p/11277515.html
總結(jié)
以上是生活随笔為你收集整理的NOIP 2018【摆渡车】题解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【2018NOIP普及组】T3 摆渡车
- 下一篇: 【NOIP2018普及组】【DP】摆渡车