洛谷P2365/5785 任務安排 題解 斜率優化DP

任務安排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\),即排序

\[sumT[i] = \sum_{j=1}^{i} T_j \]

\[sumC[i] = \sum_{j=1}^{i} C_j \]

\(F[i][j]\) 表示把前 \(i\) 個任務分紅 \(j\) 批執行的最小費用,則第 \(j\) 批任務的完成時間就是 \(j \times S + sumT[i]\)隊列

以第 \(j-1\) 批和第 \(j\) 批任務的分界點爲DP的「決策」,(設第 \(j-1\) 批的最後一個任務是 \(k\),第 \(j\) 批的最後一個任務是 \(i\))狀態轉移方程爲:

\[F[i][j] = \min_{0 \le k \ \lt i} \{ F[k][j-1] + (S \times j + sumT[i]) \times (sumC[i] - sumC[k]) \} \]

實現代碼以下:

#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\) 個任務分紅若干批執行的最小費用,狀態轉移方程爲:

\[F[i] = \min \{ F[j] + sumT[i] \times (sumC[i] - sumC[j]) + S \times (sumC[N] - sumC[j]) \} \]

在上式中,第 \(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\) 的乘積項分開。

\[F[i] = \min_{0 \le j \lt i} \{ F[j] - (S + sumT[i]) \times sumC[j] \} + sumT[i] \times sumC[i] + S \times sumC[N] \]

\(\min\) 函數去掉,把關於 \(j\) 的值 \(F[j]\)\(sumC[j]\) 看作變量,其他部分看作常數,獲得:

\[F[j] = (S + sumT[i]) \times sumC[j] + F[i] - sumT[i] \times sumC[i] - S \times sumC[N] \]

\(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\) 的斜率,即:

\[\frac{F[j_2] - F[j_1]}{sumC[j_2] - sumC[j_1]} \lt \frac{F[j_3] - F[j_2]}{sumC[j_3] - sumC[j_2]} \]

小於號兩側實際上都是鏈接兩個決策點的線段的斜率。通俗地講,咱們應該維護「鏈接相鄰兩點的線段斜率」單調遞增的一個「下凸殼」,只有這個「下凸殼」的頂點纔有可能成爲最優決策。實際上,對於一條斜率爲 \(k\) 的直線,若某個頂點左側線段線段的斜率比 \(k\) 小,右側線段的斜率比 \(k\) 大,則該頂點就是最優決策。換言之,若是把這條直線和全部線段組成一個序列,那麼令直線截距最小化的頂點就出如今按照斜率大小排序時,直線應該排在的位置上。如圖所示:

在本題中,\(j\) 的取值範圍是 \(0 \le j \lt i\),隨着 \(i\) 的增大,每次會有一個新的決策進入候選集合。由於 \(sumC\) 的單調性,新決策在座標系中的橫座標必定大於以前的全部決策,出如今凸殼的最右端。另外,由於 \(sumT\) 的單調性,每次求解「最小截距」的直線斜率 \(S+sumT[i]\) 也單調遞增,若是咱們只保留凸殼上「鏈接相鄰兩點的線段斜率」大於 \(S+sumT[i]\) 的部分,那麼凸殼的最左端點就必定是最優決策。

綜上所述,咱們能夠創建單調隊列 \(q\),維護這個下凸殼。隊列中保存若干個決策變量,它們對應凸殼上的頂點,且知足橫座標 \(sumC\) 遞增、鏈接相鄰兩點的線段斜率也遞增。須要支持的操做與通常的單調隊列題目相似,對於每一個狀態變量 \(i\)

  1. 檢查隊首的兩個決策變量 \(Q_l\)\(Q_{l+1}\),若斜率 \(\frac{F[Q_{l+1}] - F[Q_l]}{sumC[Q_{l+1}] - sumC[Q_l]} \le S + sumT[i]\),則讓 \(Q_l\) 出隊,繼續檢查新的隊首。
  2. 直接取隊首 \(j = Q_l\) 爲最優決策,執行狀態轉移,計算出 \(F[i]\)
  3. 把新決策 \(i\) 從隊尾插入,在插入以前,若三個決策點 \(j_1 = Q_{r-1}, j_2 = Q_r, j_3 = i\) 不知足斜率單調遞增(不知足下凸性,即 \(j_2\) 是無用決策),則直接從隊尾讓 \(Q_r\) 出隊,繼續檢查新的隊尾。

實現代碼以下:

#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;
}
相關文章
相關標籤/搜索