擺渡車(題目和測試右轉 洛谷P5017)
做法:dp+各種優化(剪枝)
這道題考場上看了一臉懵逼...第一眼看這 tm 不是個一維dp嗎...結果按着這個朦朧的思路,刪刪改改約莫0.5h,終於過了小樣例,然后一測大樣例...GG了。冥思苦想了1h,最終放棄了 (感謝這白費的1.5h,迫使我T4打了個暴力,結果AC了...)
言歸正傳
考試完后,參考了各種題解,WA了N/A次,終於AC了這道毒瘤題,艱難的AC歷程...
(〇)引子
首先來闡明一下題意:在一條數軸上有n個點,然后要求將這條數軸分為若干段(左開右閉的區間),要求:每段的兩個端點之間的距離必須 >= m,求分段的最小總費用。
費用定義為:若一個點k在區間 ( j , i ] ,則這個點的費用為 i - k (可能一個位置上有多個點)
總費用定義為:所有點的費用和即為總費用
(一)如何拿分(暴力?)
講解完題意,接下來我們要研究一個很重要的問題:如何拿分?
先看 30% 的數據:
n≤20, m≤2, 0≤ti≤100
看着 ti 的大小很容易想到用一維數組 f[i] 表示在第 i 分鍾發車的最小費用。
然后,這TM不是個一維線性dp嗎?
我們可以枚舉一個 j ,即枚舉前一次是在哪里發車的,這樣我們便得到了狀態轉移方程:
這樣一來,時間復雜度就是 ,30%的數據是可以過的。
(二)如何拿中分(不高也不低的分)
看一下50%的數據:
n ≤ 500, m ≤ 100, 0 ≤ ti ≤ 10^4
nt^2的時間復雜完全過不了50%得數據,怎么辦呢
通過觀察(一)的狀態轉移方程,可以發現枚舉一個k實際上是沒必要的,可以用前綴和優化:
將等車人到達的時間用數組存起來,peo[ i ]表示在 0 到 i 分鍾內等車人的數量,cost[ i ]表示在 0 到 i 分鍾所有等車人到達時間的累積(即將 1 到 i 分鍾內到達的等車人的到達時間累加起來)
就可以得到狀態轉移方程
分析一下時間復雜度:因為只枚舉了 i 和 j ,所以時間復雜度降低為
50%的數據在 “少爺機” 上是穩穩的。
程序段:
1 for(int i=1; i<t+m; i++) 2 { 3 f[i] = i*peo[i] - cost[i]; 4 for(int j=0; j<=i-m; j++) 5 f[i] = min(f[i], f[j]+(peo[i]-peo[j])*i-(cost[i]-cost[j])); 6 }
(三)如何拿高分(不是滿分,考場上的高分)
自然是優化啦。問題來了,如何優化。
先看看70%的數據范圍:
n≤500, m≤10, 0≤ti≤4×10^6
很明顯,t^2的時間復雜的是肯定超的。而 i 是這個狀態轉移方程的狀態,必須要枚舉,只能從 j 下手,優化這個動態規划了。
j 如何優化呢?其實,仔細想一想,很容易發現,j 沒有必要從 0 枚舉到 i-m,換句話說,從0到 i-m 的這段區間內,有很多狀態是無用的,即這些狀態一定不能構成當前 f[i] 的最優解,真正有用的只有區間 ( i-m-m+1, i-m ]。如何證明呢?
先貼一幅圖:
觀察上圖可以發現:
當 j 在 ( R, i )中時,擺渡車回來的時間一定大於 i ,不滿足狀態 f[i] 的定義,舍去。
當 j 在 ( ?, L ]中時,一定可以多發一次車,使擺渡車回來的時間小於等於 i,這樣得到的花費一定小於等於只發一次車的情況。因此,也可以省去。
最后,我們發現,j 只能在區間 ( L, R ] 之間才能得到最小花費,即 j 只有 m-1 種情況。
於是,狀態轉移方程不變,j 的枚舉范圍改變,時間復雜度降為
70%的數據穩穩地過,甚至在 “少爺機” 上面的 score 可能 100 ( 最起碼分數在70以上 )。
程序段:
1 for(int i=1; i<t+m; i++) 2 { 3 f[i] = i*peo[i] - cost[i]; 4 for(int j=max(i-m-m+1,0); j<=i-m; j++) 5 f[i] = min(f[i], f[j]+(peo[i]-peo[j])*i-(cost[i]-cost[j])); 6 }
(四)70+太低了如何拿滿分
老樣子,再來看一下數據范圍(終極boss來了):
n≤500, m≤100, 0≤ti≤4×10^6
m的范圍變回 100 了,tm的時間復雜度好方,腫么辦!!!
別着急,先來分析一下數據范圍:
可以發現,ti 達到了4*10^6 ,然鵝 n 卻還是500,不難想象,如果將 ti 看做一條線段,上面有 4*10^6個整點,然后取其中的500個點標記,標記的點一定是非常離散的。
同樣的道理,在如此之大的 ti 中,一定有很多區間是沒有等車人的(無用的區間),但是仍然無用地枚舉了m次,時間復雜度大大增加。
這時候,我們只需要加一個小小的剪枝,判斷當前枚舉的 i 點到 i-m 中是否有人,如果沒有人的話,就沒有必要進行多一次的循環了(因為再發多一次車對答案沒有影響,畢竟沒人),只需要將 f[i] 賦值為 f[i-m],然后continue即可。
經過某神犇的證明,經過這次剪枝后,實際枚舉的點只有 nm 個,時間復雜度降為(當然還有一個常數 t 被我忽略了)
100%的數據跑得飛快……
程序段:
1 for(int i=1; i<t+m; i++) 2 { 3 if(i>=m and peo[i]-peo[i-m] == 0) {f[i] = f[i-m]; continue;} 4 f[i] = i*peo[i] - cost[i]; 5 for(int j=0; j<=i-m; j++) 6 f[i] = min(f[i], f[j]+(peo[i]-peo[j])*i-(cost[i]-cost[j])); 7 }
(五)答案在哪?
終於到最后一步了,顯而易見,answer一定在最大的時間后面(廢話),只需要從 t 枚舉到 t+m-1 取一個最小值即可。
注意:
代碼中的 t 和 (五)中的 t 含義相同,都是最后到的那個人的到達時間。
ps:終於寫完了,這個思路是參考了某神犇的(@Sooke),再加上了自己的一些理解,希望能有幫助。