洛谷 P5785 [SDOI2012] 任務安排
連結:
弱化版:P2365
題意:
有 \(n\) 個任務待完成,每個任務有一個完成時間 \(t_i\) 和費用係數 \(f_i\),相鄰的任務可以被分成一批。從零時刻開始這些任務會被機器分批完成,在每批任務開始前機器有一個給定啟動時間 \(s\),一批任務的完成時間是這批任務完成時間之和,同一批任務視作在同一時刻完成。
每個任務的費用是他的完成時刻和費用係數的乘積,請最小化總費用。
分析:
如果設 \(dp[i]\) 為第 \(i\) 個任務作為當前這一批任務的最後一個時的最優解,這樣會很麻煩,因為會涉及到 "此時一共有多少批" 這個很麻煩的狀態。這裡有一個dp的trick:費用提前
我們發現把第 \(i\) 個任務作為當前這一批任務的最後一個時,當前批內以及後面的任務都會多一個 \(s\cdot f_i\) 的費用,於是把 \(dp[i]\) 改為第 \(i\) 個任務作為當前這一批任務的最後一個時,加上後面任務的已有貢獻的最優解。可以聯絡狀態轉移理解:
\[dp[i]=dp[j]+(\sum_{k=j+1}^if_i\times\sum_{k=1}^it_i)+s\sum_{k=j+1}^nf_i \]於是設 \(sumf[i]=\sum\limits_{k=1}^if_k\),\(sumt[i]=\sum\limits_{k=1}^it_k\)
\[dp[i]=dp[j]+(sumf[i]-sumf[j])\times sumt[i]+s(sumf[n]-sumf[j]) \]於是
依套路:
\[(dp[j]-s\cdot sumf[j])=sumt[i]sumf[j]+(dp[i]-sumf[i]sumt[i]-s\cdot sumf[n]) \]於是求斜率為 \(sumt[i]\),過點 \((sumf[j],dp[j]-s\cdot sumf[j])\) 的直線的最小截距。
這裡的 \(sumt[i]\) 並不是單調的,所以不能用單調佇列優化,一個優秀的辦法是二分找到第一個與下一個點之間的斜率大於 \(sumt[i]\)
演算法:
單調佇列維護下凸包,然後每次二分找到最優決策點,根據其資訊得到 \(dp[i]\),繼續維護凸包即可。時間複雜度 \(O(n\log n)\)。
重要的細節:
這道題讓我注意到了一個斜率優化dp中的一個很重要的細節,就是維護下凸包的判斷。
這道題在洛谷討論區也有不少人曾發起過討論,稱因為精度問題而必須把判斷斜率改成乘法,但其實並不是這樣一個簡單的藉口,我們來看下面三個程式碼,他們除了維護下凸包的判斷沒有任何區別。
(Y(q[qt])-Y(q[qt-1]))*(X(i)-X(q[qt])) >= (Y(i)-Y(q[qt]))*(X(q[qt])-X(q[qt-1]))
100pts
slope(q[qt],q[qt-1])>=slope(i,q[qt])
80pts
slope(q[qt],q[qt-1])>=slope(i,q[qt-1])
100pts
顯然,這並不是精度的問題,甚至這三種寫法都是錯的!
我們來看下面這種情況:(這種情況雖然在資料上比較難構造,但單純考慮維護下凸包來看還是很常見的)
這樣三個橫座標相等點從上到下按 \(1,3,2\) 的次序被加入下凸包的維護。
顯然,根據人腦判斷,這樣的資料要維護下凸包自然是保留 \(2\) 號點,彈出 \(3\) 號點,但普遍的寫法是不存在 "彈出當前點" 這種操作的,只有 "彈出上一個點"。於是:
如果是第一種判斷,兩邊的乘積都是 \(0\),於是會彈出 \(2\) 號點,此時 \(1,3\) 點間斜率為 -inf
,後面的點可以加入,但在區域性確實出現了正確性的錯誤。
如果是第二種判斷,由於 \(1,2\) 間斜率是 -inf
,\(2,3\) 點間斜率是 +inf
,所以不會有點彈出,此時 \(2,3\) 點之間斜率為 +inf
,這種情況下,後面的點根本無法加入下凸包的維護了,出現了嚴重的問題。
如果是第三種判斷,由於 \(1,2\) 間斜率是 -inf
,\(1,3\) 點間斜率是 -inf
,這裡也涉及取等的問題,如果取等,那麼會彈出 \(2\) 號點,變成第一種情況;如果不取等,那麼 \(2\) 號點不會被彈出,於是變成第二種情況。
所以這題錯誤的真正原因根本不是什麼精度,而是維護下凸包的做法在存在橫座標相等的情況下本來就是假的!
根據這題橫座標非嚴格單調增,一個感覺比較正確的做法是在維護下凸包時先判斷與前面的點橫座標是否相等,如果相等,保留縱座標最低的點;如果不等,按照原本的做法即可。
在這樣的做法下,不等號是否取等是沒有正確性的影響的。取等會彈出一些無用的點,可能會快幾毫秒。
所以在有橫座標相等的情況下,一個正確的寫法是:
if(X(i)==X(q[qt])&&1<qt){if(Y(i)<Y(q[qt]))qt--;else continue;}
while(1<qt&&slope(q[qt],q[qt-1])>=slope(i,q[qt]))qt--;
q[++qt]=i;
這樣寫的話,不管判斷是用的乘法還是斜率,哪兩個斜率判斷,或者取不取等,都是能過的。不排除真的會有精度錯誤,但我認為不能把根據觀察發現除法比乘法劣的情況統一歸咎到精度錯誤上。
程式碼:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
inline int read(){
int p=0,f=1;
char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){p=p*10+c-'0';c=getchar();}
return p*f;
}
#define X(x) (sumf[x])
#define Y(x) (dp[x]-sumf[x]*s)
#define dx(x,y) (X(x)-X(y))
#define dy(x,y) (Y(x)-Y(y))
#define slope(x,y) ((double)dy(x,y)/dx(x,y))
const int N=3e5+5;
const int inf=0x7fffffffffffffff;
int n,s,t[N],f[N],sumt[N],sumf[N],dp[N],q[N],qt;
inline int bin(int key){
int l=1,r=qt,mid;
while(l<r){
mid=(l+r)>>1;
double cp=(mid==qt)?inf:slope(q[mid+1],q[mid]);
if(cp<key)l=mid+1;
else r=mid;
}
return l;
}
signed main(){
n=in,s=in;
for(int i=1;i<=n;i++)
t[i]=in,f[i]=in,
sumt[i]=sumt[i-1]+t[i],
sumf[i]=sumf[i-1]+f[i];
qt=1;
for(int i=1;i<=n;i++){
int qh=bin(sumt[i]);
dp[i]=Y(q[qh])-sumt[i]*X(q[qh])+sumf[i]*sumt[i]+sumf[n]*s;
if(X(i)==X(q[qt])&&1<qt){if(Y(i)<Y(q[qt]))qt--;else continue;}
while(1<qt&&slope(q[qt],q[qt-1])>slope(i,q[qt-1]))qt--;
q[++qt]=i;
}
cout<<dp[n];
return 0;
}
題外話:
這個重要的細節確實讓我對斜優裡維護下凸包部分的印象深了許多。