AcWing 301 任務安排2

題目描述:

有 N 個任務排成一個序列在一臺機器上等待執行,它們的順序不得改變。

機器會把這 N 個任務分成若干批,每一批包含連續的若干個任務。

從時刻0開始,任務被分批加工,執行第 i 個任務所需的時間是 Ti。

另外,在每批任務開始前,機器需要 S 的啓動時間,故執行一批任務所需的時間是啓動時間 S 加上每個任務所需時間之和。

一個任務執行後,將在機器中稍作等待,直至該批任務全部執行完畢。

也就是說,同一批任務將在同一時刻完成。

每個任務的費用是它的完成時刻乘以一個費用係數 Ci。

請爲機器規劃一個分組方案,使得總費用最小。

輸入格式

第一行包含整數 N。

第二行包含整數 S。

接下來N行每行有一對整數,分別爲 Ti 和 Ci,表示第 i 個任務單獨完成所需的時間 Ti 及其費用係數 Ci。

輸出格式

輸出一個整數,表示最小總費用。

數據範圍

1≤N≤3∗10^5,
1≤Ti,Ci≤512,
0≤S≤512

輸入樣例:

5
1
1 3
3 2
4 3
2 3
1 4

輸出樣例:

153

分析:

AcWing 300 任務安排1中,我們使用了費用提前計算的技巧,實現了本題平方級別的算法,對於本題增加的數據範圍,顯然平方級別的算法不足以解決問題,我們在上一題中推導出了本題的狀態轉移方程f[i] = min(f[j] + (sc[i] - sc[j])*st[i] + (sc[n] - sc[j])*S),現在需要想辦法在線性的時間內解決本題。

本題需要使用斜率優化DP,又被稱爲凸包(凸殼)優化。雖然說最後的結果不是很複雜,但是沒有接觸過凸包的情況下,想要理解本題,還是比較困難的。所以,先引入幾個有關凸包的概念。先看下凸包的定義:

凸包(Convex Hull)是一個計算幾何(圖形學)中的概念。

在一個實數向量空間V中,對於給定集合X,所有包含X的凸集的交集S被稱爲X的凸包

X的凸包可以用X內所有點(X1,...Xn)的凸組合來構造.

在二維歐幾里得空間中,凸包可想象爲一條剛好包著所有點的橡皮圈。

用不嚴謹的話來講,給定二維平面上的點集,凸包就是將最外層的點連接起來構成的凸多邊形,它能包含點集中所有的點。

正如最後一句話所說,在一個點集中,外層的點構成的包含所有點集中的點的凸多邊形就是凸包。

如上圖所示,點集中外層的點構成的凸多邊形就構成了能夠包含所有點的凸包,其中連接相鄰頂點構成的邊越來越平緩,或者說斜率越來越小構成的一組點叫做上凸殼,而相鄰的邊,斜率越來越大的一組點叫做下凸殼。本題主要考慮的是下凸殼。

如圖所示,開始只有頂點AB構成的凸包,然後加入第三點C1,顯然BC1的斜率是高於AB的,因此AB,BC1構成了一個下凸殼;但是如果新加的點不是C1而是C2,BC2的斜率小於AB,那麼AB和BC2就不能構成下凸殼了,因爲不能作爲點集的下邊界,不能包含在AB下面卻在AC2上面的點,因此,加入C2後,AC2將成爲下凸殼新的邊界了,因此,對於平面上的三點A(x1,y1),B(c2,y2),C(c3,y3),並且x1 < x2 < x3,y1 < y2 < y3。AB與BC能夠作爲凸包當且僅當AB的斜率要小於BC的斜率。

上面介紹的知識現在看還是看不出與本題的關聯的,但是馬上就會用到了,本題的求解步驟類似於凸包的Graham掃描法。對於狀態轉移方程f[i] = min(f[j] + (sc[i] - sc[j])*st[i] + (sc[n] - sc[j])*S),對能夠使f[i]最小的j,有f[i] = f[j] + (sc[i] - sc[j])*st[i] + (sc[n] - sc[j])*S,由於在求f[i]時,sc[i],sc[j],S都是已知量,對式子做下簡單變形,得到f[j] = (st[i] + S) * sc[j] + f[i] - sc[i] * st[i] - sc[n] * S,可以發現 - sc[i] * st[i] - sc[n] * S是常數,f[j]和sc[j]是變量,我們需要做的是,找到合適的j,使得f[i]最小,將f[j]看作y,sc[j]看作x,st[i] + S看作k,f[i] - sc[i] * st[i] - sc[n] * S看作b,式子就化作了y = kx + b這樣簡單的一次函數,f[i]最小時,截距也最小,所以問題就轉化爲了在所有(sc[j],f[j])構成的點集中,找到一條斜率爲st[i] + S的直線,使得截距最小。

如圖所示,我們用一條斜率爲k = st[i] + S的直線不斷往上移動,最先碰到點集中的點時,此時的截距最小,f[i]也最小。可以確定的是,最先碰到的一定是凸多邊形邊界的頂點,而不會是凸包內部包含的點,所以,凸包以外的點可以排除了。現在,需要確定的是,使得截距最小的到底是凸包上的哪一點,圖中直線上移最先遇見的顯然是B點,那在什麼情況下最先遇見的不是AC而是B點呢,只需要AB的斜率小於k,BC的斜率大於k即可,也就說,我們需要維護一個隊列,自隊頭到隊尾斜率遞增,我們從頭遍歷隊列,最先遇見大於k的斜率的端點就是所求的j點。我們在遍歷i的時候逐步的去求f[i],求f[i]的時候用到了0到i-1,也就說所有的j,每次求出了i也需要將i作爲i + 1狀態中j的候選者加入到點集中,也就說,上圖中的點集分別是(sc[0],f[0]),(sc[1],f[1]),...,由於sc是前綴和,而且c數組都是整數,所以隨着i的增加,新加入的(sc[j],f[j])一定是橫縱座標都超過之前的點的,我們需要不停的向點集中添加橫縱座標都增加的點,並且更新加入新點後點集的凸包,還記得開始我們提過的結論:對於平面上的三點A(x1,y1),B(c2,y2),C(c3,y3),並且x1 < x2 < x3,y1 < y2 < y3。AB與BC能夠作爲凸包當且僅當AB的斜率要小於BC的斜率。也就說,原本凸包上有AB構成的邊,加入C後,要想BC也作爲凸包的邊界,只需要BC的斜率大於AB,否則,就要刪除B點,因爲AC肯定在B點的下方,B不能作爲凸包的邊界了。

整理下思路,我們遍歷i的時候,相當於不斷的往點集中加點,並且更新凸包,在求f[i]時,只需要找到凸包中第一個大於k的斜率即可,由於凸包的邊的斜率自左向右是遞增的,所以我們可以維護一個隊列,隊頭的斜率最小,並且自隊頭向隊尾斜率是遞增的。我們還需要注意求f[i]時候的k = st[i] + S,隨着i的增加k也在增加,所以隊列中小於k的斜率肯定也小於後面的k,不可能成爲最優解的,所以在遍歷隊列找第一個大於k的斜率時可以將小於k的斜率都刪掉。另外,爲了維護隊列的單調性,插入新點(sc[i],f[i])時,如果該點與之前相鄰點的斜率不是高於之前隊尾的斜率的,就要將隊尾的斜率出隊,踢出凸包的點集,直至找到一個合適的位置再插入隊列。

最後只剩下一步,就是將上面單調隊列的思路實現爲具體的代碼。單調隊列中存儲的必然是點的編號i,其代表的斜率應該與其更接近隊尾的相鄰的點構成的斜率,所以初始情況下隊頭應該有個哨兵節點0,後面要考慮的就是何時出隊頭,何時出隊尾了。

出隊頭一方面是爲了自小到大找到第一個大於k的斜率,另一方面是爲了刪掉小於k的斜率,一條線至少有兩個點構成,所以只有隊列中有不少於兩個點的情況下才能出隊頭,隊頭存儲的是(sc[q[hh]],f[q[hh]]),與後一個點(sc[q[hh]+1],f[q[hh]+1])構成的斜率是(f[q[hh]+1] - f[q[hh]]) / (sc[q[hh]+1] - sc[q[hh]]),當該斜率小於st[i] + S時就要出隊頭考慮下一個斜率,爲了避免除法實現時需要變形下改爲乘法。

出完隊頭就找到了最優解j,開始更新f[i],任何就是將(sc[i],f[i])加入隊列,同樣是需要比較斜率大小,將該點放在合適的位置使得隊列斜率遞增,當(f[i] - f[q[tt]]) / (sc[i] - sc[q[tt]]) > (f[q[tt]] - f[q[tt]-1]) / (sc[q[tt]] - sc[q[tt]-1])時才能加入到隊尾。

另外,由於計算過程中的乘法可能會爆int,所以本題的數組定義爲long long類型。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 300005;
typedef long long ll;
ll f[N],t[N],c[N];
int q[N];
int main(){
    int n,s;
    scanf("%d%d",&n,&s);
    for(int i = 1;i <= n;i++){
        scanf("%d%d",&t[i],&c[i]);
        t[i] += t[i-1],c[i] += c[i-1];
    }
    int hh = 0,tt = 0;
    for(int i = 1;i <= n;i++){
        while(hh < tt && f[q[hh+1]]-f[q[hh]]<=(t[i]+s)*(c[q[hh+1]]-c[q[hh]])) hh++;
        f[i] = f[q[hh]] - (t[i] + s) * c[q[hh]] + c[i] * t[i] + s * c[n];
        while(hh < tt && (f[q[tt]]-f[q[tt - 1]])*(c[i]-c[q[tt]]) >= (f[i]-f[q[tt]])*(c[q[tt]]-c[q[tt - 1]]))    tt--;
        q[++tt] = i;
    }
    printf("%lld\n",f[n]);
    return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章