1. 程式人生 > 實用技巧 >題解 P5785 【[SDOI2012]任務安排】

題解 P5785 【[SDOI2012]任務安排】

本題解用於本蒟蒻加深演算法印象,也歡迎大家閱讀

本篇題解將分為四塊,一步一步地講解本題,

\(Part~1:~O(n^3)\)

\(n^3\) 演算法應該非常的顯然,我們設 \(f_{i,j}\) 為到 \(i\) 個任務,分為 \(j\) 個批次所使用的最少時間,易得轉移方程為:

\[\begin{array}{c} f_{i,j}=min(f_{k,j-1}+(sumt_i+s*j)*(sumc_i-sunc_k))\\ (0\le k<i) \end{array} \]

但是這個複雜很明顯是過不了的( \(O(n^2)\) 都過不了,這還想過???),所以我們考慮優化。

\(Part~2:~O(n^2)\)

因為每一個前面的 \(s\) 是每次會對後面的動態規劃產生影響的,所以我們考慮在設計狀態的時候就將 \(s\) 考慮進去,即 \(f_i\) 表示到第 \(i\) 個點,前面 \(i\) 個點的實際用時與 \(s\) 對於後面的影響值的和最小值,這個思想叫做費用提前,我們可以結合狀態轉移方程來一起食用:

\[\begin{array}{c} f_i=min(f_j+sumt_i*(suma_i-suma_j)+s*(suma_n-suma_j)\\ (0\le j<i) \end{array} \]

看上去好像沒什麼問題,最優子結構是成立的,這個複雜度是 \(O(n^2)\)

\(Part~3:~O(n)\)

來到今天的重頭戲——斜率優化

我們可以先看看這題的弱化版。轉移方程和題目大意是一模一樣的,唯一的不同點是 \(t_i\) 是非負的。

對於上面的狀態的轉移方程,我們進行一些移項:

\[\begin{array}{c} f_i=min(f_j+sumt_i*(suma_i-suma_j)+s*(suma_n-suma_j)\\ f_i=f_j+sumt_i*suma_i-sumt_i*suma_j+s*suma_n-s*suma_j\\ f_j=(sumt_i+s)*suma_j+f_i-sumt_i*suma_i-s*suma_n \end{array} \]

此時,如果我們將 \(suma_j\)

看作自變數, \(f_j\) 看作因變數,對於每一個 \(i\)\(sumt_i\)\(suma_i\)\(suma_n\)\(s\)又都是定值,我們可以將這個式子看作一個待定 \(f_i\) 值的函式。

對於這個函式,我們要使得 \(f_i\) 的數值最小,就是使得函式的截距最小(因為其他的都是定植),而如何找到使得函式截距最小的點呢?我們來畫一下圖:

可以發現,我們將這個函式的直線向上移動的時候,最先碰到的一個點就是滿足條件的點,而我們如何維護這麼一個點呢,我們可以發現一些可能的點的性質:

  1. 任意兩個可能的點的連線下側不能有點存在

  2. 所有可能被選中的點構成的集合中,相鄰的兩個點的斜率是單調遞增的,即下凸殼。

大家可以結合影象理解理解:

因為這個函式的斜率 \(sumt_i+s\) 是單調遞增的,因為 \(t_i\) 是非負的,所以我們可以發現,對於已經放棄的點,最後也不可能被重新啟用。

而每進入一個點時,由於 \(suma_i\) 是單調遞增的(因為 \(a_i\) 是非負的),所以我們是向著 \(x\) 軸正方向更新點的,根據性質 \(1\) ,如果這個點向當前最左點連線後,有點出現在直線的下方,則說明最左點不成立,而之後也不可能成立。

結合上面兩種操作和性質 \(2\) ,我們可以想到用單調佇列維護,維護方式就是上面的操作。

程式碼如下:

while(head<tail&&cal(q[head],q[head+1])<=sumt[i]+s)
++head;
f[i]=f[q[head]]+sumt[i]*(suma[i]-suma[q[head]])+s*(suma[n]-suma[q[head]]);
while(head<tail&&cal(q[tail-1],q[tail])>=cal(q[tail-1],i))
--tail;
q[++tail]=i;

\(Part~4:~O(nlogn)\)

而針對這一道題目,我們可以發現 \(t_i\) 是有負的,所以我們第一操作就必須放棄了,因為函式的斜率並非是單調遞增的。

但是我們依舊可以維護下凸殼的單調性,而對於當前遍歷到的函式斜率,我們可以通過二分的方式找到符合條件的點,即該點與其左側點的連線斜率小於函式斜率,該點與其右側殿的連線的斜率大於函式斜率。

程式碼如下:

int l=head,r=tail-1,mid,res=q[tail];
while(l<=r)
{
	mid=(l+r)>>1;
	if(cal(q[mid],q[mid+1])>sumt[i]+s)
	{
		res=q[mid];
		r=mid-1;
	}
	else
	l=mid+1;
}
f[i]=f[res]+sumt[i]*(suma[i]-suma[res])+s*(suma[n]-suma[res]);
while(head<tail&&cal(q[tail-1],q[tail])>=cal(q[tail-1],i))
--tail;
q[++tail]=i;

總結

我們可以發現,得出斜率優化函式的方法是提取出關於 \(j\) 的項,一個作為自變數,一個作為因變數,也就是說,如果這個項數是大於 \(2\) 的,那麼就不可以用斜率優化了。