【NOJ1148】【DP_動態規劃】石子合併
1148.石子合併
時限:1000ms 記憶體限制:10000K 總時限:3000ms
描述
在一個圓形(圓形!!!圓形!圓形!)操場的四周擺放著n堆石子(n<= 100),現要將石子有次序地合併成一堆。規定每次只能選取相鄰的兩堆合併成新的一堆,並將新的一堆的石子數,記為該次合併的得分。編一程式,讀入石子堆數n及每堆的石子數(<=20)。選擇一種合併石子的方案,使得做n-1次合併,得分的總和最小; 比如有4堆石子:4 4 5 9 則最佳合併方案如下: 4 4 5 9 score: 0 8 5 9 score: 8 13 9 score: 8 + 13 = 21 22 score: 8 + 13 + 22 = 43
輸入
可能有多組測試資料。 當輸入n=0時結束! 第一行為石子堆數n(1<=n<=100); 第二行為n堆的石子每堆的石子數,每兩個數之間用一個空格分隔。
輸出
合併的最小得分,每個結果一行。
#include <iostream> using namespace std; int n; int stone[101]; int dp(); int memo[101][101]; int num(int i, int j); //返回第i堆石子累加到第j堆石子的累加和 int main() { while(cin>>n&&n) { for(int i=0; i<n; i++) { cin>>stone[i]; } if(n==1) { cout<<'0'<<endl; } else { cout<<dp()<<endl; } } return 0; } int dp() { //備忘錄初始化 for(int i=0; i<n; i++) { for(int j=0; j<n; j++) { if(i==j) { memo[i][j]=0; } else { memo[i][j]=INT_MAX; } } } //填寫備忘錄(石子踏馬是環形的啊啊啊) int minscore=INT_MAX; for(int len=1; len<n; len++) { for(int i=0,j=(i+len)%n; i<n; i++,j=(j+1)%n) { for(int k=0; k<len; k++) { memo[i][j]=min(memo[i][j], memo[i][(i+k)%n]+memo[(i+k+1)%n][j]+num(i, j)); if(len==n-1) //已經填寫到長度最大的情況 { minscore=min(minscore, memo[i][j]); //記錄最優解 } } } } return minscore; } int num(int i, int j) //返回第i堆石子累加到第j堆石子的累加和 { int x=0; if(i<j) { for(int k=i; k<=j; k++) { x+=stone[k]; } } else { for(int k=i; k<n; k++) { x+=stone[k]; } for(int k=0; k<=j; k++) { x+=stone[k]; } } return x; }
【後記】
1.剛開始寫的時候以為和矩陣連乘積那道題一模一樣,於是美滋滋地,張藝興的小歌兒聽著,鍵盤劈里啪啦的敲著,敲碼一時爽,WA火葬場。。。
2.然後才發現這石子踏馬的是環形擺著的,不過略一思索,發現只需要擴大for迴圈的遍歷範圍,不管越不越界,多套幾個(…)%n就好了。在矩陣連乘積那道題裡我還講:備忘錄表的下三角用不上、不寫了,結果這道題就用上了。。。
照例上圖:
圖中寫的就是dp函式的工作過程,先初始化,再填表,建議配合程式碼食用。
本題中遞推公式如下,其中k 為從第 i 堆石子開始數的第k個斷開位置,範圍是[0, len-1]:
我們在下圖的右邊填表過程中可以看到(注意標綠色的數字),每次len++後,新的迴圈都需要填兩個斜行的表,而 i 的範圍總是[0, n-1],j 的範圍總是[ (i+len)%n, (i+len+n-1)%n];
下圖中可以看出,最後的答案有n個,相當於把這個環從n個位置斷開,當作一條線(像矩陣連乘積那道題似的)分別求解而得到的n個最優解,在這些解中選一個代價最小的,就是最終真正的最優解。
3.本題中還有一個需要注意的地方:單獨處理只有一堆石子(n==1)的情況,此時代價為0(無法合併)。