1. 程式人生 > >動態規劃專題之石子合併

動態規劃專題之石子合併

動態規劃專題講義

專題九:合併石子問題
/*
	Name: 動態規劃專題之石子合併
	Author: 巧若拙 
	Description: 
在一個操場上擺放著一排N堆石子。現要將石子有次序地合併成一堆。規定每次只能選相鄰的2堆石子合併成新的一堆,並將新的一堆石子數記為該次合併的得分。
試設計一個演算法,計算出將N堆石子合併成一堆的最小得分。
輸入描述 Input Description
第一行是一個數N。1≤N≤100
以下N行每行一個數A,表示石子數目。1≤A≤200
輸出描述 Output Description
共一個數,即N堆石子合併成一堆的最小得分。
樣例輸入 Sample Input
7
13 7 8 16 21 4 18
樣例輸出 Sample Output
239
*/
#include<iostream>

using namespace std;

const int INT_MAX = 2147483647;
const int MAX = 1000;

int A[MAX+1];//記錄每堆石子的數量 
int Sum[MAX+1];//記錄前n堆石子的數量 
int B[MAX+1][MAX+1];//記錄第i堆石子至第j堆石子的最優解
int S[MAX+1][MAX+1];//記錄從哪裡斷開的才可得到最優解
bool flag[MAX+1]; //記錄A[i]是否已經被輸出過 

int StonesCombined(int i, int j);//自頂向下,使用備忘錄陣列的動態規劃演算法  
int StonesCombined_2(int n);//自底向上的動態規劃演算法:遞增括號中石子堆數量 
int StonesCombined_3(int n);//自底向上的動態規劃演算法:逆序掃描  
int StonesCombined_4(int n);//自底向上的動態規劃演算法:順序掃描  
void Traceback(int i, int j);//根據s[][]記錄的各個子段的最優解,將其輸出

int main()
{
	int n = 0;
	cin >> n;
	for (int i=1; i<=n; i++)
	{
		cin >> A[i];
		Sum[i] = Sum[i-1] + A[i];
	}
		
  // cout << StonesCombined(1, n) << endl;//自頂向下,使用備忘錄陣列的動態規劃演算法 
    cout << StonesCombined_2(n) << endl;//自底向上的動態規劃演算法 
    Traceback(1, n);
    cout << endl;
    
    return 0;
}

演算法1:自頂向下,使用備忘錄陣列的動態規劃演算法,需要用到全域性變數S[MAX+1][], 另有B[MAX+1][]初始化為0。
int StonesCombined(int i, int j) 
{
 	if (B[i][j] != 0)  
        return   //語句1
	if (i == j)
		return   //語句2
	int t, m = INT_MAX;
	for (int k=i; k<j; k++)
	{
	 	t = StonesCombined(i, k) + StonesCombined(k+1, j) + Sum[j] - Sum[i-1];
	 	if (t < m)
	 	{
	        m = t;
	        S[i][j] = k;  //記錄從哪裡斷開的才可得到最優解
        }
	} 
	return B[i][j] = m;
}
問題1:將語句1和語句2補充完整。

參考答案: 
問題1:語句1:return B[i][j];
        語句2:return 0;

演算法2:自底向上的動態規劃演算法:遞增括號中石子堆數量,需要用到全域性變數S[MAX+1][], 另有B[MAX+1][]初始化為0。
int StonesCombined_2(int n)  
{
	for (int len=2; len<=n; len++) //語句1
	{
	 	for (int i=1; i<=n-len+1; i++)  //左邊界
	 	{
		 	int j =   //語句2
		 	int t, m = INT_MAX;
		 	for (int k=i; k<j; k++) 
			{
			 	t = B[i][k] + B[k+1][j] + Sum[j] - Sum[i-1];
			 	if (t < m)
			 	{
			        m = t;
			        S[i][j] = k;
		        }
			} 
			B[i][j] = m;
		}
	}
	
	return B[1][n];
}
問題1:語句1能否改為:for (int len=n; len>=2; len--) ?為什麼?
問題2:將語句2補充完整。

參考答案: 
問題1:不能。len代表當前被合併在一起的石子堆的數量,演算法的思想是先計算所有相鄰的2堆石子合併在一起的解,再計算所有所有相鄰的3堆石子合併在一起的解,逐步擴大子問題的規模。在計算較大規模的問題時,可以呼叫較小規模子問題的解。所以len必須從小到大。
問題2:語句2:int j = i + len - 1; //右邊界

演算法3:自底向上的動態規劃演算法:逆序掃描,需要用到全域性變數S[MAX+1][], 另有B[MAX+1][]初始化為0。
int StonesCombined_3(int n)  
{
	for (int i=n-1; i>0; i--)  
	{
	 	for (int j=i+1; j<=n; j++)// 語句1 
	 	{
		 	int t, m = INT_MAX;
		 	for (int k=i; k<j; k++) // 語句2
			{
			 	t = B[i][k] + B[k+1][j] + Sum[j] - Sum[i-1];
			 	if (t < m)
			 	{
			        m = t;
			        S[i][j] = k;
		        }
			} 
			B[i][j] = m;
		}
	}
	
	return B[1][n];
}
問題1:語句1能否改為:for (int j=n; j>i; j--)?為什麼?
問題2:語句2能否改為:for (int k=j-1; k>=i; k--)?為什麼?

參考答案: 
問題1:不能。i和j分別代表當前被合併石子堆的左,右邊界,演算法3的基本思想是在確定左邊界的情況下,逐步擴大子問題的右邊界。在計算較大規模的問題時,可以呼叫較小規模子問題的解。所以j必須遞增。
問題2:可以。k代表分隔石子堆的位置(即把[i,k]和[k+1,j]兩堆石子合併成[i,j]一堆石子),只要滿足i<=k<j,k遞增或遞減均可。

演算法4:自底向上的動態規劃演算法:順序掃描,需要用到全域性變數S[MAX+1][], 另有B[MAX+1][]初始化為0。
int StonesCombined_4(int n)  
{
	for (int j=2; j<=n; j++)  
	{
	 	for (int i=j-1; i>0; i--)// 語句1 
	 	{
		 	int t, m = INT_MAX;
		 	for (int k=i; k<j; k++)  
			{
			 	t = B[i][k] + B[k+1][j] + Sum[j] - Sum[i-1];
			 	if (t < m)
			 	{
			        m = t;
			        S[i][j] = k;
		        }
			} 
			B[i][j] = m;
		}
	}
	
	return B[1][n];
}
問題1:語句1能否改為:for (int i=1; i<j; i++)?為什麼? 

參考答案: 
問題1:不能。i和j分別代表當前被合併石子堆的左,右邊界,演算法4和演算法3的思路剛好相反,它是在確定右邊界的情況下,逐步擴大子問題的左邊界,所以i必須遞減。

拓展練習:原題只要求輸出最優解,並未要求輸出具體的合併方法。
但是我們在上述演算法中引入了一個二維陣列S[][]記錄k的值,即從哪裡分開石子堆才能得到最優解。現在請你根據S[][]記錄的資訊,設計一個遞迴函式void Traceback(int i,int j);// i和j分別代表當前被合併石子堆的左,右邊界。
請用括號把合併在一起的石子堆括起來,輸出最終合併情形。
例如:對於題目給出的樣例:13 7 8 16 21 4 18,則輸出:(((13 (7 8))16)(21 (4 18)))

參考答案:
void Traceback(int i,int j)//根據s[][]記錄的各個子段的最優解,將其輸出
{
    if (i == j)
	   return ;
    
    cout << "(";
    Traceback(i, S[i][j]);
    if (flag[i] == 0)
    {
        cout << A[i] << " ";
        flag[i] = 1;
    }
    Traceback(S[i][j]+1, j);
    if (flag[j] == 0)
    {
        cout << A[j];
        flag[j] = 1;
    }
    cout << ")";
}

課後練習:
練習1: 3546_矩陣鏈乘法
描述:給定有n個要相乘的矩陣構成的序列(鏈)<A1,A2,A3,.......,An>,要計算乘積A1A2.....An。一組矩陣是加全部括號的。
矩陣鏈加括號對運算的效能有很大影響。
僅當兩個矩陣A和B相容(即A的列數等於B的行數),才可以進行相乘運算。如果A是一個p×q矩陣,B是q×r矩陣,結果C是p×r的矩陣。
計算C的時間由乘法運算次數決定的,次數為p×q×r。
矩陣鏈乘法問題可表述為:給定n個矩陣構成的一個鏈<A1,A2,A3.......,An>,其中i=1,2,3,4.....,n,矩陣Ai的維數為Pi-1 ×Pi,
對乘積A1A2A3.....An,以一種最小標量乘法次數的方式進行加全部括號。
輸入描述 Input Description
如果有n個數組,則第一行輸入n+1個整數值
輸出描述 Output Description
所有的陣列以一種最小標量乘法次數的方式進行加全部括號。
樣例輸入 Sample Input
1 2 3 4 
樣例輸出 Sample Output
((A1A2)A3)