任務安排1(小數據):https://www.luogu.com.cn/problem/P2365
任務安排2(大數據):https://www.luogu.com.cn/problem/P5785c++
有 \(N\) 個任務排成一個序列在一臺機器上等待執行,它們的順序不得改變。機器會把這 \(N\) 個任務分紅若干批,每一批包含連續的若干個任務。從時刻 \(0\) 開始,任務被分批加工,執行第 \(i\) 個任務所需的時間是 \(T_i\)。另外,在每批任務開始前,機器須要 \(S\) 的啓動時間,故執行一批任務所需的時間是啓動時間 \(S\) 加上每一個任務所需時間之和。算法
一個任務執行後,將在機器中稍做等待,直至該批任務所有執行完畢。也就是說,同一批任務將在同一時刻完成。每一個任務的費用是它的完成時刻乘以一個費用係數 \(C_i\)。函數
請爲機器規劃一個分組方案,使得費用最小。大數據
第一行是 \(N\) ,第二行是 \(S\)。優化
下面 \(N\) 行每行有一對數,分別爲 \(T_i\) 和 \(C_i\),均爲不大於 \(100\) 的正整數,表示第 \(i\) 個任務單獨完成所需的時間是 \(T_i\) 機器費用係數 \(C_i\)。spa
輸出一個整數,表示最小的總費用。code
5 1 1 3 3 2 4 3 2 3 1 4
153
50%的數據保證 \(1 \lt N \le 5000, 1 \le S \le 50, 1 \le T_i, C_i \le 100\) ;
100%的數據保證 \(1 \le N \le 3 \times 10^5, 1 \le S,T_i,C_i \le 512\)。blog
求出 \(T,C\) 的前綴和 \(sumT,sumC\),即排序
設 \(F[i][j]\) 表示把前 \(i\) 個任務分紅 \(j\) 批執行的最小費用,則第 \(j\) 批任務的完成時間就是 \(j \times S + sumT[i]\)。隊列
以第 \(j-1\) 批和第 \(j\) 批任務的分界點爲DP的「決策」,(設第 \(j-1\) 批的最後一個任務是 \(k\),第 \(j\) 批的最後一個任務是 \(i\))狀態轉移方程爲:
實現代碼以下:
#include <bits/stdc++.h> using namespace std; const int maxn = 5000; int n, S, T, C, sumT[maxn], sumC[maxn], f[maxn][maxn], ans = -1; int main() { cin >> n >> S; for (int i = 1; i <= n; i ++) { cin >> T >> C; sumT[i] = sumT[i-1] + T; sumC[i] = sumC[i-1] + C; } memset(f, -1, sizeof(f)); for (int j = 1; j <= n; j ++) { for (int i = j; i <= n; i ++) { if (j == 1) { f[i][j] = (S + sumT[i]) * sumC[i]; continue; } for (int k = j-1; k < i; k ++) { assert(f[k][j-1] != -1); int tmp = f[k][j-1] + (S * j + sumT[i]) * (sumC[i] - sumC[k]); if (f[i][j] == -1 || f[i][j] > tmp) f[i][j] = tmp; } } } for (int i = 1; i <= n; i ++) { if (ans == -1 || ans > f[n][i]) ans = f[n][i]; } cout << ans << endl; return 0; }
該解法的時間複雜度是 \(O(n^3)\)。
本題並無規定須要把任務分紅多少批,在上一個解法中之因此須要批數 \(j\),是由於咱們須要知道機器啓動了多少次(每次啓動都要 \(S\) 單位時間),從而計算出 \(i\) 所在的一批任務的完成時刻。
事實上,在執行一批任務時,咱們不容易直接得知在此以前機器啓動過幾回。但咱們知道,機器因執行這批任務而花費的啓動時間 \(S\),會累加到在此以後全部任務的完成時刻上。
設 \(F[i]\) 表示把前 \(i\) 個任務分紅若干批執行的最小費用,狀態轉移方程爲:
在上式中,第 \(j+1 \sim i\) 個任務在同一批內完成,\(sumT[i]\) 是忽略機器的啓動時間,這批任務的完成時刻。由於這批任務的執行,機器的啓動時間 \(S\) 會對第 \(j+1\) 個以後的全部任務產生影響,故咱們把這部分補充到費用中。
也就是說,咱們沒有直接求出每批任務的完成時間,而是在一批任務「開始」對後續任務產生影響時,就先把費用累加到結果中。這是一種名爲 「費用提早計算」 的經典思想。
實現代碼以下:
#include <bits/stdc++.h> using namespace std; const int maxn = 5000; int n, S, T, C, sumT[maxn], sumC[maxn], f[maxn]; int main() { cin >> n >> S; for (int i = 1; i <= n; i ++) { cin >> T >> C; sumT[i] = sumT[i-1] + T; sumC[i] = sumC[i-1] + C; } memset(f, -1, sizeof(f)); f[0] = 0; for (int i = 1; i <= n; i ++) { for (int j = 0; j < i; j ++) { int tmp = f[j] + sumT[i] * (sumC[i] - sumC[j]) + S * (sumC[n] - sumC[j]); if (f[i] == -1 || f[i] > tmp) f[i] = tmp; } } cout << f[n] << endl; return 0; }
該解法的時間複雜度爲 \(O(N^2)\)。
對上一題的算法二進行優化,先對狀態轉移方程稍做變形,把常數、僅與 \(i\) 有關的項、僅與 \(j\) 有關的項 以及 \(i,j\) 的乘積項分開。
把 \(\min\) 函數去掉,把關於 \(j\) 的值 \(F[j]\) 和 \(sumC[j]\) 看作變量,其他部分看作常數,獲得:
在 \(sumC[j]\) 爲橫座標, \(F[j]\) 爲縱座標的平面直角座標系中,這是一條以 \(S + sumT[i]\) 爲斜率,\(F[i] - sumT[i] \times sumC[i] - S \times sumC[N]\) 爲截距的直線。也就是說,決策候選集合是座標系中的一個點集,每一個決策 \(j\) 都對應着座標系中的一個點 \((sumC[j], F[j])\)。每一個待求解的狀態 \(F[i]\) 都對應着一條直線的截距,直線的斜率是一個固定的值 \(S + sumT[i]\),截距未知。當截距最小化時,\(F[i]\) 也取到最小值。
該問題其實是一個線性規劃問題,高中數學有所涉及。令直線過每一個決策點 \((sumC[j], F[j])\),均可以求得一個截距,其中使截距最小的一個就是最優決策。體如今座標系中,就是用一條斜率爲固定正整數的直線自下而上平移,第一次接觸到某個決策點時,就獲得了最小截距。如圖所示:
對於任意三個決策點 \((sumC[j_1], F[j_1])\),\((sumC[j_2], F[j_2])\) 和 \((sumC[j_3], F[j_3])\),不妨設 \(j_1 \lt j_2 \lt j_3\),由於 \(T,C\) 均爲正整數,亦有 \(sumC[j_1] \lt sumC[j_2] \lt sumC[j_3]\)。根據及時排除無用決策的思想,咱們考慮 \(j_2\) 可能成爲最優決策的條件。
從上圖中咱們發現,\(j_2\) 有可能成爲最優決策,當且僅當 \(j_1\) 到 \(j_2\) 的斜率小於 \(j_2\) 到 \(j_3\) 的斜率,即:
小於號兩側實際上都是鏈接兩個決策點的線段的斜率。通俗地講,咱們應該維護「鏈接相鄰兩點的線段斜率」單調遞增的一個「下凸殼」,只有這個「下凸殼」的頂點纔有可能成爲最優決策。實際上,對於一條斜率爲 \(k\) 的直線,若某個頂點左側線段線段的斜率比 \(k\) 小,右側線段的斜率比 \(k\) 大,則該頂點就是最優決策。換言之,若是把這條直線和全部線段組成一個序列,那麼令直線截距最小化的頂點就出如今按照斜率大小排序時,直線應該排在的位置上。如圖所示:
在本題中,\(j\) 的取值範圍是 \(0 \le j \lt i\),隨着 \(i\) 的增大,每次會有一個新的決策進入候選集合。由於 \(sumC\) 的單調性,新決策在座標系中的橫座標必定大於以前的全部決策,出如今凸殼的最右端。另外,由於 \(sumT\) 的單調性,每次求解「最小截距」的直線斜率 \(S+sumT[i]\) 也單調遞增,若是咱們只保留凸殼上「鏈接相鄰兩點的線段斜率」大於 \(S+sumT[i]\) 的部分,那麼凸殼的最左端點就必定是最優決策。
綜上所述,咱們能夠創建單調隊列 \(q\),維護這個下凸殼。隊列中保存若干個決策變量,它們對應凸殼上的頂點,且知足橫座標 \(sumC\) 遞增、鏈接相鄰兩點的線段斜率也遞增。須要支持的操做與通常的單調隊列題目相似,對於每一個狀態變量 \(i\):
實現代碼以下:
#include <bits/stdc++.h> using namespace std; const int maxn = 300030; int n, q[maxn], l, r; long long S, T, C, sumT[maxn], sumC[maxn], f[maxn]; int main() { cin >> n >> S; for (int i = 1; i <= n; i ++) { cin >> T >> C; sumT[i] = sumT[i-1] + T; sumC[i] = sumC[i-1] + C; } memset(f, -1, sizeof(f)); f[0] = 0; q[l = r = 1] = 0; for (int i = 1; i <= n; i ++) { while (l < r && f[q[l+1]] - f[q[l]] <= (S + sumT[i]) * (sumC[q[l+1]] - sumC[q[l]])) l ++; f[i] = f[q[l]] - (S + sumT[i]) * sumC[q[l]] + sumT[i] * sumC[i] + S * sumC[n]; while (l < r && (f[q[r]]-f[q[r-1]]) * (sumC[i]-sumC[q[r]]) >= (f[i]-f[q[r]]) * (sumC[q[r]]-sumC[q[r-1]])) r --; q[++r] = i; } cout << f[n] << endl; return 0; }
整個算法的時間複雜度爲 \(O(N)\)。
與通常的單調隊列優化DP的模型相比,本題維護的「單調性」依賴於隊列中相鄰兩個元素之間的某種「比值」。由於這個值對應線性規劃的座標系中的斜率,因此咱們在本題中使用的優化方法稱爲「斜率優化」。
以上分析針對 \(T_i\) 爲正數的狀況,接下來咱們來考慮 \(T_i\) 爲負數的狀況。
與任務安排1不一樣的是,任務安排2中任務的執行時間 \(T\) 多是負數。這意味着 \(sumT\) 不具備單調性,從而須要最小化截距的直線的斜率 \(S + sumT[i]\) 不具備單調性。因此,咱們不能在單調隊列中只保留凸殼上「鏈接相鄰兩點的線段斜率」大於 \(S + sumT[i]\) 的部分,而是必須維護整個凸殼。這樣一來,咱們就不須要在隊首把斜率與 \(S + sumT[i]\) 比較。
隊首也不必定是最優決策,咱們能夠在單調隊列中二分查找,求出一個位置 \(p\),\(p\) 左側線段的斜率比 \(S + sumT[i]\) 小,右側線段的斜率比 \(S+sumT[i]\) 大,\(p\) 就是最優決策。
實現代碼以下:
#include <bits/stdc++.h> using namespace std; const int maxn = 300030; int n, q[maxn], l, r; long long S, T, C, sumT[maxn], sumC[maxn], f[maxn]; int my_binary_search(int k) { if (l == r) return q[l]; int L = l, R = r; while (L < R) { int mid = (L + R) / 2; if (f[q[mid+1]] - f[q[mid]] <= k * (sumC[q[mid+1]] - sumC[q[mid]])) L = mid + 1; else R = mid; } return q[L]; } int main() { cin >> n >> S; for (int i = 1; i <= n; i ++) { cin >> T >> C; sumT[i] = sumT[i-1] + T; sumC[i] = sumC[i-1] + C; } memset(f, -1, sizeof(f)); f[0] = 0; q[l = r = 1] = 0; for (int i = 1; i <= n; i ++) { int p = my_binary_search(S + sumT[i]); f[i] = f[p] - (S + sumT[i]) * sumC[p] + sumT[i] * sumC[i] + S * sumC[n]; while (l < r && (f[q[r]]-f[q[r-1]]) * (sumC[i]-sumC[q[r]]) >= (f[i]-f[q[r]]) * (sumC[q[r]]-sumC[q[r-1]])) r --; q[++r] = i; } cout << f[n] << endl; return 0; }