NUIST OJ 1410 石子合併 [DP]
題目
題目描述
設有N堆沙子排成一排,其編號為1,2,3,…,N(N<=100)。每堆沙子有一定的數量。現要將N堆沙子併成為一堆。歸併的過程只能每次將相鄰的兩堆沙子堆成一堆(每次合併花費的代價為當前兩堆沙子的總數量),這樣經過N-1次歸併後成為一堆,歸併的總代價為每次合併花費的代價和。找出一種合理的歸併方法,使總的代價最小。
例如:有3堆沙子,數量分別為13,7,8,有兩種合併方案,
第一種方案:先合併1,2號堆,合併後的新堆沙子數量為20,本次合併代價為20,再拿新堆與第3堆沙子合併,合併後的沙子數量為28,本次合併代價為28,將3堆沙子合併到一起的總代價為第一次合併代價20加上第二次合併代價28,即48;
第二種方案:先合併2,3號堆,合併後的新堆沙子數量為15,本次合併代價為15,再拿新堆與第1堆沙子合併,合併後的沙子數量為28,本次合併代價為28,將3堆沙子合併到一起的總代價為第一次合併代價15加上第二次合併代價28,即43;
採用第二種方案可取得最小總代價,值為43。
輸入描述
輸入由若干行組成,第一行有一個整數,n(1≤n≤100);表示沙子堆數。第2至n+1行是每堆沙子的數量。
輸出描述
一個整數,歸併的最小代價。
樣例輸入
7
13
7
8
16
21
4
18
樣例輸出
239
題目分析
首先這個題目需要注意的是:相鄰的堆才可以合併,一開始我沒有看到這個條件,因此每次選兩個最小的合併,這顯然是錯誤的。
思來想去,這道題只能用區間型DP來做了
本題的區間DP
用dp[i][j]表示合併[i,j]區間所需要的最小代價
那麼可以方便的得到如下關係
若i=j,則dp[i][j]=0
否則,則dp[i][j]=min{dp[i][k]+dp[k+1][j]}+sum[i][j]
發現:計算dp[i][j]的大區間,需要先計算出dp[i][j]的小區間,所以可以考慮設定一個變數len,從小往大遞增,根據此來控制先算小區間,再算大區間
最後,本題要求的是合併所有石子,即[i,j]區間和並的最小代價。
因此,本題答案儲存在dp[1][n]內
整體程式碼與執行結果
/*
[email protected]
*/
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
#define maxn 105
using namespace std;
int a[maxn];
int dp[maxn][maxn];
int sum[maxn][maxn];
int main() {
int n;
while (cin >> n) {
memset(dp, 0 , sizeof(dp));
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = 1; i <= n; ++i) {
sum[i][i] = a[i];
for (int j = i+1; j <= n; ++j) {
sum[i][j] = sum[i][j - 1] + a[j];
}
}
for (int len = 2; len <= n; ++len) {
for (int i = 1; i <= n - len + 1; ++i) {
dp[i][i + len - 1] = dp[i][i] + dp[i + 1][i + len - 1] + sum[i][i + len - 1];
for (int k = i + 1; k < i + len - 1; ++k) {
dp[i][i + len - 1] = min(dp[i][i + len - 1], dp[i][k] + dp[k + 1][i + len - 1] + sum[i][i + len - 1]);
}
}
}
cout << dp[1][n] << endl;
}
return 0;
}
結果如下
另一種寫法
我這裡是用了len來控先算小的區間再算大的區間
這個博文卻讓j從後往前推巧妙的避免了這個問題。
for(int i=1; i<=n; ++i){
for(int j=i-1; j>=1; --j){
for(int k=j; k<i; ++k){
dp[j][i] = min(dp[j][i], dp[j][k] + dp[k+1][i] + sum[j][i] );
}
}
}
未完待續
我現在不能理解的是,為什麼不能從一排石子鍾,每次找出相鄰兩個加和最小的一組數字進行合併。這是所謂的貪心吧?看不出來哪裡錯了哇。