DP專題 6 | 石子合併 CH5301(區間DP)

歡迎回來~繼續我們的DP專題,上一篇我們講了一個較爲複雜的線性DP問題,這一次讓我們看一看區間DP問題。

 

區間DP直觀上可以理解成對於一個區間計算最優解的問題。先來看下本題的題目,直接上中文。

 

題目大意:設有N堆沙子排成一排,其編號爲1,2,3,…,N(N<=300)。每堆沙子有一定的數量,可以用一個整數來描述,現在要將這N堆沙子合併成爲一堆,每次只能合併相鄰的兩堆,合併的代價爲這兩堆沙子的數量之和,合併後與這兩堆沙子相鄰的沙子將和新堆相鄰,合併時由於選擇的順序不同,合併的總代價也不相同,如有4堆沙子分別爲 1  3  5  2 我們可以先合併1、2堆,代價爲4,得到4 5 2 又合併 1,2堆,代價爲9,得到9 2 ,再合併得到11,總代價爲4+9+11=24,如果第二步是先合併2,3堆,則代價爲7,得到4 7,最後一次合併代價爲11,總代價爲4+7+11=22;問題是:找出一種合理的方法,使總的代價最小。輸出最小代價。 

 

區間DP通用的轉移方程如下:

f(i,j) = min{f[i,k] + f[k+1,j] + cost(i,j)

 

其中cost爲將區間i~j合併起來的代價,可以用前綴和來計算(前綴和傳送門)。

 

區間DP中要比較小心的是階段的劃分,本題中使用區間長度len作爲階段,爲什麼要選用len呢?單純的看狀態轉移方程,我們很難確定如何劃分階段,因爲裏面沒有任何關於len的信息。

 

對於使用遞推的方式來解決DP問題,我們都需要從初始值推導出後續值,比如從0一直到N,而不能反着來。區間DP也有同樣的處理方式,我們必須先從區間長度爲0的初始值出發,也就是說,f(i,j)其實可以等價於一維的F(len)。

 

F(0) => f(0,0) f(1,1) f(2,2) ...... f(n,n)

 

那麼使用len作爲階段就很正常的。

 

實現過程中除了注意len作爲階段,還需要注意初始化f[k][k]=0, 而f[i][j]初始化爲比較大的數字。

 

打印語句可以比較清晰的看出是如何遞推的。

 

示例:

4

1 3 5 2

f[1][2] -> f[1][1]+f[2][2]
  f[1][2]=4
f[2][3] -> f[2][2]+f[3][3]
  f[2][3]=8
f[3][4] -> f[3][3]+f[4][4]
  f[3][4]=7
f[1][3] -> f[1][1]+f[2][3]
f[1][3] -> f[1][2]+f[3][3]
  f[1][3]=13
f[2][4] -> f[2][2]+f[3][4]
f[2][4] -> f[2][3]+f[4][4]
  f[2][4]=17
f[1][4] -> f[1][1]+f[2][4]
f[1][4] -> f[1][2]+f[3][4]
f[1][4] -> f[1][3]+f[4][4]
  f[1][4]=22

源代碼:G++

#include <string.h>
#include <cmath>
#include <cstdio>
#include <iostream>

using namespace std;

#define N 1100
int a[1100];
int s[1100];
int f[N][N];

int main() {
#ifdef __MSYS__
    freopen("test.txt", "r", stdin);
#endif
    int n;
    scanf("%d", &n);

    // 注意初始化爲比較大的數字
    memset(f, 0x3f, sizeof(f));
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        s[i] = s[i - 1] + a[i];
        // l和r相等不需要合併,爲0
        f[i][i] = 0;
        printf("s[%d]=%d a[%d]=%d\n", i, s[i], i, a[i]);
    }

    // 計算階段,高階依賴低階,以長度爲階
    for (int len = 2; len <= n; ++len) {
        // len=2 1/2/3 =(4-2+1)
        for (int l = 1; l <= n - len + 1; ++l) {  // left
            int r = l + len - 1;                 // right
            //
            for (int k = l; k < r; ++k) {
                // 計算最小值
                f[l][r] = std::min(f[l][r], f[l][k] + f[k + 1][r]);
                printf("f[%d][%d] -> f[%d][%d]+f[%d][%d]\n", l, r, l, k, k+1, r);
            }
            // 加上cost
            f[l][r] += s[r] - s[l - 1];
            printf("  f[%d][%d]=%d\n", l, r, f[l][r]);
        }
    }

    printf("%d\n", f[1][n]);

    return 0;
}

DP專題系列:

DP專題 1 | 數字三角形 - POJ 1163(簡單DP)

DP專題 2 | LIS POJ - 2533(經典DP)

DP專題 3 | LCS最長公共子序列 - POJ 1458

DP專題 4 | 骨頭收集愛好者 - POJ 1458( 0-1揹包)

DP專題 5 | 顏色的長度 - UVA1625(線性DP)

前綴和、二維前綴和與差分的小總結

 

個人公衆號(acm-clan):ACM算法日常

專注於基礎算法的研究工作,深入解析ACM算法題,五分鐘閱讀,輕鬆理解每一行源代碼。內容涉及算法、C/C++、機器學習等。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章