歡迎回來~繼續我們的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專題 3 | LCS最長公共子序列 - POJ 1458
DP專題 4 | 骨頭收集愛好者 - POJ 1458( 0-1揹包)
DP專題 5 | 顏色的長度 - UVA1625(線性DP)
個人公衆號(acm-clan):ACM算法日常
專注於基礎算法的研究工作,深入解析ACM算法題,五分鐘閱讀,輕鬆理解每一行源代碼。內容涉及算法、C/C++、機器學習等。