演算法導論-第15章-動態規劃-15.1 鋼條切割問題
阿新 • • 發佈:2019-01-10
一、綜述
動態規劃是通過組合子問題的解而解決整個問題的。
動態規劃對每個子問題只求解一次,將其結果儲存在一張表中。
動態規劃通常用於最優化問題。
動態規劃的設計步驟:
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;
}