1. 程式人生 > 其它 >演算法學習筆記(45)——區間DP

演算法學習筆記(45)——區間DP

區間DP

到目前為止,我們介紹的線性 DP 一般從初態開始,沿著階段的擴張向某個方問遞推,直至計算出目標狀態,區間 DP 也屬於線性 DP 中的一種,它以“區間長度”作為 DP 的“階段”,使用兩個座標(區間的左、右端點),描述每個維度。在區間 DP 中,一個狀態由若干個比它更小且包含於它的區間所代表的狀態轉移而來,因此,區間 DP 的決策往往就是劃分區間的方法。區間 DP 的初態一般就由長度為 1 的“元區間”構成。這種向下劃分、再向上遞推的模式與某些樹形結構,例如線段樹,有很大的相似之處。我們把區間 DP 作為線性 DP 中一類重要的分支單獨進行講解,將使讀者更容易理解下一節樹形 DP 的內容。同時,藉助區間 DP 這種與樹形相關的結構,我們也將提及記憶化搜尋——其本質是動態規劃的遞迴實現方法。

石子合併

題目連結:AcWing 282. 石子合併

題目描述

設有 \(N\) 堆石子排成一排,其編號為 \(1,2,3,\dots,N\)。每堆石子有一定的質量,可以用一個整數來描述,現在要將這 \(N\)
堆石子合併成為一堆。每次只能合併相鄰的兩堆,合併的代價為這兩堆石子的質量之和,合併後與這兩堆石子相鄰的石子將和新堆相鄰,合併時由於選擇的順序不同,合併的總代價也不相同。找出一種合理的方法,使總的代價最小,輸出最小代價。

輸入格式

第一行一個數 \(N\) 表示石子的堆數 \(N\)
第二行 \(N\) 個數,表示每堆石子的質量(均不超過 \(1000\))。

輸出格式

輸出一個整數,表示最小代價。

資料範圍

\(1 \le N \le 300\)

輸入樣例

4
1 3 5 2

輸出樣例

22

若最初的第 \(l\) 堆石子和第 \(r\) 堆石子被合併成一堆,則說明 \(l \sim r\) 之間的每堆石子也己經被合併,這樣 \(l\)\(r\) 才有可能相鄰。因此,在任意時刻,任意一堆石子均可以用一個閉區間 \([l,r]\) 來描述,表示這堆石子是由最初的第 \(l \sim r\) 堆石子合併而成的,其重量為 \(\sum_{i=1}^{r}A[i]\)。另外,一定存在一個整數 \(k(l \le k <r)\) ,在這堆石子形成之前,先有第 \(l \sim k\)

堆石子(閉區間 \([l,k]\))被合併成一堆,第 \(k+1 \sim r\) 堆石子(閉區間 \([k+1,r]\))被合併成一堆,然後這兩堆石子才合併成 \([l,r]\)

對應到動態規劃中,就意味省兩個長度較小的區間上的資訊向一個更長的區間發生了轉移,劃分點 \(k\) 就是轉移的決策。自然地,應該把區間長度 \(len\) 作為 DP 的階段。不過,區間長度可以由左端點和右端點表示出,即 \(len = r-l+1\)。 本著動態規判“選擇最小的能覆蓋狀態空間的維度集合”的思想,可以只用左、右端點表示 DP 的狀態。

\(F[l,r]\) 表示把最初的第 \(l\) 堆到第 \(r\) 堆石子合併成一堆,需要消耗的最少體力。則容易寫出狀態轉移方程:

\[F[l,r]=\min_{l \le < r} \lbrace F[l,k]+F[k+1,r] \rbrace + \sum\limits_{i=l}^{r}A_i \]

初值:\(\forall l \in [1,N],F[l,l]=0, \text{其餘為正無窮}\)
目標:\(F[1,N]\)

最後強調,程式設計實現動態規劃的狀態轉移方程時,務必分清階段、狀態與決策,三者應該按照從外到內的順序依次迴圈。而 \(\sum_{i=l}^{r}A_i\) 可以使用字首和計算。

狀態數量是 \(N^2\) 個,狀態計算 \(N\) 次,所以總的時間複雜度是 \(O(N^3)\)

#include <iostream>

using namespace std;

const int N = 310, INF = 0x3f3f3f3f;

int n;       // n堆石子
int s[N];    // 字首和陣列,表示前i堆石子的質量和
int f[N][N]; // f[l][r]表示合併[l,r]區間內的石子的最小代價

int main()
{
    cin >> n;
    
    // 預處理字首和
    for (int i = 1; i <= n; i ++ ) cin >> s[i], s[i] += s[i - 1];

    // 列舉每個階段(區間長度),len等於1時只有一堆,代價為0,而堆中變數自動初始化為0,不需要操作,所以從2開始迴圈
    for (int len = 2; len <= n; len ++ )
        // 列舉狀態表示的區間左端點
        for (int l = 1; l <= n - len + 1; l ++ ) {
            // 通過區間長度與左端點計算出右端點
            int r = l + len - 1;
            // 每次處理前將該狀態初始化為正無窮
            f[l][r] = INF;
            // 迴圈處理每一個決策
            for (int k = l; k < r; k ++ )
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
    
    cout << f[1][n] << endl;
    
    return 0;
}