1. 程式人生 > >演算法導論-第15章-動態規劃-15.1 鋼條切割問題

演算法導論-第15章-動態規劃-15.1 鋼條切割問題

一、綜述

動態規劃是通過組合子問題的解而解決整個問題的。

動態規劃對每個子問題只求解一次,將其結果儲存在一張表中。
動態規劃通常用於最優化問題。
動態規劃的設計步驟:
a. 描述最優解的結構
b. 遞迴定義最優解的值
c. 按自底向上的方式計算最優解的值
d. 由計算出的資訊構造一個最優解


動態規劃方法對每個子問題只求解一次,並將結果儲存下來。如果隨後再次需要此子問題的解,只需查詢儲存的結果,而不必重新計算。因此,動態規劃方法是付出額外的記憶體空間來節省計算時間,是典型的時空權衡的例子。時間上的節省是非常巨大的:可能將一個指數時間的解轉化為一個多項式時間的解。
動態規劃有兩種等價的實現方法
1、帶備忘的自頂向下法。

此方法仍按自然的遞迴形式編寫過程,但過程會儲存每個子問題的解(通常儲存在一個數組或散列表中)。當需要一個子問題的解時,過程首先檢查是否已經儲存過此解。如果是,則直接返回儲存的值,從而節省了計算時間;否則,按通常方式計算這個子問題。
2、自底向上法。
這種方法一般需要恰當定義子問題“規模”的概念,使得任何子問題的求解都只依賴於“更小的”子問題求解。因而我們可以將子問題按規模排序,按從小到大的順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已求解完畢,結果已經儲存。每個子問題只需求解一次,當我們求解它時,它的所有前提子問題都已求解完畢。
通常情況下,如果每個子問題都必須至少求解一次,自底向上法會比自頂向下法快,因為自底向上法沒有遞迴呼叫的開銷,表的維護開銷也更小。如果子問題空間中的某些子問題完全不必求解,備忘法就體現出優勢了,因為它只會求解那些絕對必要的子問題。


二、程式碼


15.1 鋼條切割
假設公司出售一段長度為i英寸的鋼條的價格為Pi(i = 1, 2, ...單位:美元),下面給出了價格表樣例:
長度i  1 2 3 4  5  6  7  8  9  10
價格Pi 1 5 8 9 10 17 17  20 24 30
切割鋼條的問題是這樣的:給定一段長度為n英寸的鋼條和一個價格表Pi,求切割方案,使得銷售收益Rn最大。
當然,如果長度為n英寸的鋼條價格Pn足夠大,最優解可能就是完全不需要切割。
對於上述價格表樣例,我們可以觀察所有最優收益值Ri及對應的最優解方案:
R1 = 1,切割方案1 = 1(無切割)
R2 = 5,切割方案2 = 2(無切割)
R3 = 8, 切割方案3 = 3(無切割)
R4 = 10, 切割方案4 = 2 + 2
R5 = 13, 切割方案5 = 2 + 3
R6 = 17, 切割方案6 = 6(無切割)
R7 = 18, 切割方案7 = 1 + 6或7 = 2 + 2 + 3
R8 = 22, 切割方案8 = 2 + 6
R9 = 25, 切割方案9 = 3 + 6
R10 = 30,切割方案10 = 10(無切割)
更一般地,對於Rn(n >= 1),我們可以用更短的鋼條的最優切割收益來描述它:
Rn = max(Pn, R1 + Rn-1, R2 + Rn-2,...,Rn-1 + R1)
首先將鋼條切割為長度為i和n - i兩段,接著求解這兩段的最優切割收益Ri和Rn - i(每種方案的最優收益為兩段的最優收益之和),由於無法預知哪種方案會獲得最優收益,我們必須考察所有可能的i,選取其中收益最大者。如果直接出售原鋼條會獲得最大收益,我們當然可以選擇不做任何切割。
 思路:先將鋼條切成兩條,有n-1種方案,每一種方案的最優解都等於兩個子鋼條的最優解。我們從這n-1個偽最優解再挑出最優的解了。

 自頂向下遞迴實現的虛擬碼:

CUT-ROD(p,n)
 if n == 0
     return 0
 q=負無窮
 for i = 1 to n
     q=max(q,p[i]+CUT-ROD(p,n-i))
 return q


C++程式碼

#include<iostream>
using namespace std;

#define NIL (-0x7fffffff-1)

int max(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}
int cut_rod(int *p,int n)
{
	if(n==0)
		return 0;
	int q=NIL;
	if(n<=10)
	{
		for (int i=0;i<n;i++)
		{
			q=max(q,p[i]+cut_rod(p,n-1-i));
		}
		return q;
	}
	else if(n>10)
	{
		int b=n/10;
		n=n-b*10;
		if(n==0)
			q=0;
		for (int i=0;i<n;i++)
		{
			q=max(q,p[i]+cut_rod(p,n-1-i));;
		}
		return q+b*30;
	}
}
int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	cout<<"Please input a int number: ";
	cin>>n;
	int result=cut_rod(p,n);
	cout<<result<<endl;
	return 0;
}

上面只用了分治策略,這個演算法的效能很差,是指數次的,遞迴呼叫次數T(n)=2^n,因為在子問題的求解中很多都是重複的。

下面介紹動態規劃法
1、帶備忘的自頂向下法

EMOIZED-CUT-ROD(p,n)
let r[0..n] be a new array
for i = 0 to n
//記錄長度為n時已經求得的受益最大的結果
return MEM-CUT-ROD-AUX(p,n,r)

MEMOIZED-CUT-ROD-AUX(p,n,r)
if r[n]>=0
	return r[n]
if n==0
	q=0
else 
	q=負無窮
	for i=1 to n
		q=max(q,p[i]+MEM-CUT-ROD-AUX(p,n-i,r))
r[n]=q
return q


C++程式碼

#include<iostream>
using namespace std;
#define NIL -10000

int max(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}
int memoized_cut_rod_aux(int *p,int n,int *r)
{
	int q=NIL;
	if(r[n]>=0)
		return r[n];
	if(n==0)
		q=0;
	else
		{
			q=NIL;
			if(n<=10)
			{
				for (int i=0;i<n;i++)
				{
					q=max(q,p[i]+memoized_cut_rod_aux(p,n-1-i,r));
				}
				r[n]=q;
				return q;
			}
			else if(n>10)
			{
				int b=n/10;
				n=n-b*10;
				if(n==0)
					q=0;
				for (int i=0;i<n;i++)
				{
					q=max(q,p[i]+memoized_cut_rod_aux(p,n-1-i,r));
				}
				r[n]=q+b*30;
				return q+b*30;
			}
		}
	return 0;
}
int memoized_cut_rod(int *p,int n)
{
	int *r;
	r=(int *)malloc(sizeof(int)*(n+1));
	for (int i=0;i<n;i++)
	{
		r[i]=NIL;
	}
	return memoized_cut_rod_aux(p,n,r);
}

int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	cout<<"Please input a int number: ";
	cin>>n;
	int result=memoized_cut_rod(p,n);
	cout<<result<<endl;
	return 0;
}

2、自底向上版本

//從小到大依次求出每種長度的受益的最優解
 BOTTOM-UP-CUT-ROD(p,n)
 let r[0..n] be a new array
 r[0]=0
 for j=1 to n
     q=負無窮
     for i=1 to j
         q=max(q,p[i]+r[j-i])
	 r[j]=q
 return r[n]
C++程式碼
//自底向上的方法,這裡面的n不能超過11,受P限制
#include<iostream>
using namespace std;
#define NIL -10000

int max(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}
int bottom_cut_rod(int *p,int n)
{
	int *r;
	r=(int *)malloc(sizeof(int)*(n+1));
	r[0]=0;
	for (int j=0;j<n;j++)
	{
		int q=NIL;
		for(int i=0;i<=j;i++)
		{
			q=max(q,p[i]+r[j-i]);
		}
		r[j+1]=q;
	}
	return r[n];
}
int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	cout<<"Please input a int number: ";
	cin>>n;
	cout<<bottom_cut_rod(p,n);
	cout<<endl;
	return 0;
}
3、下面的虛擬碼還保留了切割長度
EXTEND-BOTTOM-UP-CUT-ROD(p,n)
 let r[0..n] and s[0..n] be new arrays
 r[0]=0
 for j = 1 to n
     q=負無窮
     for i =1 to j
        if q < p[i]+r[j-i]
             q=p[i]+r[j-i]
             s[j]=i
             r[j]=q
 return r and s

4、 打印出切割長度
 PRINT-CUT-ROD-SOLUTION(p,n)
 (r,s)=EXTEND-BOTTOM-UP-CUT-ROD(p,n)
 while n >0
     print s[n]
     n=n-s[n]

C++程式碼
#include<iostream>
using namespace std;
#define NIL -10000

void extern_bottom_up_cut_rod(int *p,int n,int *r,int *s)
{
	r[0]=0;
	s[0]=0;
	for (int j=0;j<n;j++)
	{
		int q=NIL;
		for (int i=0;i<=j;i++)
		{
			if(q<(p[i]+r[j-i]))
			{
				q=p[i]+r[j-i];
				s[j+1]=i+1;
				r[j+1]=q;
			}
		}
	}
}
void print_cut_rod_solution(int *s,int n)
{
	while (n>0)
	{
		cout<<s[n]<<'\t';
		n=n-s[n];
	}
}

int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	int *r;
	int *s;

	cout<<"Please input a int number: ";
	cin>>n;

	r=(int *)malloc(sizeof(int)*(n+1));//存放n時的最大受益
	s=(int *)malloc(sizeof(int)*(n+1));//存放n時的切割長度

	extern_bottom_up_cut_rod(p,n,r,s);

	cout<<r[n]<<endl;//最大受益

	cout<<"切割的長度分別是: "<<endl;
	print_cut_rod_solution(s,n);
	cout<<endl;
	free(r);
	free(s);
	return 0; 
}