鋼條切割問題:給定一段長度爲n英寸的鋼條和一個價格表pi(i=1,2,...,n)求切割鋼條方案,使得銷售收益rn最大。
注意,如果長度爲n英寸的鋼條的價格pn足夠大,最優解可能就是完全不需要切割(具體問題描述見算法導論第三版第15章),以下都是用C語言實現,僞代碼參考算法導論。
第一次嘗試:自頂向下的遞歸算法
<span style="font-size:18px;">#include <stdio.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
int CutRod(int *p,int n);
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
printf("%d\n",CutRod(p,10));
}
int CutRod(int *p,int n)
{
if(n==0)
return 0;
int q=INT_MIN;
for (int i=0;i<n;i++)
{
int tmp=CutRod(p,n-1-i)+p[i];
q=(q>tmp?q:tmp);
}
return (q);
}</span>
上面只用了分治策略,這個算法的性能是很差的,爲T(n)=2^n,這種樸素遞歸算法效率之所如此底是因爲在子問題的求解中很多都是重複的。
第二次嘗試:備忘錄與自底向上
動態規劃方法是仔細安排求解順序,對每個子問題只求解一次,並將結果保存下來(其實是付出額外的內存空間來節省運行時間,是典型的時空權衡)。
動態規劃的兩種方法——帶備忘錄的自頂向下法和自底向上法,首先介紹下帶備忘錄的自頂向下法:
備忘錄方法是動態規劃方法的變形。與自底向上的動態規劃算法不同的是,備忘錄方法的遞歸方式是自頂向下的,此方法仍然按照遞歸的形式,但過程中會保存每個子問題的解。當需要一個子問題的解時,首先檢查是否已經保存過此解。如果是,則直接返回保存的解,從而節省時間。
C實現:
<span style="font-size:18px;">#include <stdio.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
int Memorize_Cut_Rod_Aux(int *p,int n,int *r);
int Memorize_Cut_Rod(int *p,int n);
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
printf("%d\n",Memorize_Cut_Rod(p,10));
}
int Memorize_Cut_Rod_Aux(int *p,int n,int *r)
{
if(n==0)
return 0; //這個地方要注意,和書上做了點調整
if(*(r+n-1)>0)
return (*(r+n-1));
int q=0;
for (int i=1;i<n+1;i++)
{
int tmp=Memorize_Cut_Rod_Aux(p,n-1-i,r)+p[i];
q=(q>tmp?q:tmp);
}
*(r+n-1)=q;
return (q);
}
int Memorize_Cut_Rod(int *p,int n)
{
int r[10];
for (int i=0;i<n;i++)
*(r+i)=INT_MIN;
return (Memorize_Cut_Rod_Aux(p,n,r));
}</span>
自底向上法——這種方法一般需要恰當定義子問題“規模”的概念,使得任何子問題的求解都依賴於“更小的”子問題的求解。因而我們可以將子問題按規模排序,按由小至大的順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已經求解完畢,結果已經保存。每個子問題只需要求解一次,當我們求解它(也是第一次遇到它)時,它的所有前提子問題都已求解完成。
C實現:
<pre name="code" class="cpp"><span style="font-size:18px;">#include <stdio.h>
#include <stdlib.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
int r[11]={0};
for (int j=1;j<11;j++)
{
int q=INT_MIN;
for (int i=1;i<j+1;i++)
{
int tmp=p[i]+r[j-i];
q=(q>tmp?q:tmp);
}
r[j]=q;
}
for(int i=1;i<11;i++)
printf("%d\n",r[i]);
}</span>
下面是上面的擴展版本,還保存了最優解對應的第一段鋼條的切割長度Sj;
<span style="font-size:18px;">#include <stdio.h>
#include <stdlib.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
void Extended_Bottom_Up_Cut_Rod(int *p,int n,int *r);
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
int r[11]={0};
int s[11]={0};
int n=10,q=INT_MIN;
for (int j=1;j<=n;j++)
{
for (int i=1;i<=j;i++)
{
if (q<p[i]+r[j-i])
{
q=p[i]+r[j-i];
s[j]=i;
}
}
r[j]=q;
}
for(int i=1;i<11;i++)
printf("%d,%d\n",r[i],s[i]);
}
void Extended_Bottom_Up_Cut_Rod(int *p,int n,int *r)
{
for(int i=0;i<2*n;i++)
r[i]=0;
for (int j=0;j<n;j++)
{
int q=INT_MIN;
for (int i=0;i<j-1;i++)
{
if(q<p[i]+r[j-i-1])
{
q=p[i]+r[j-i-1];
r[n+j-1]=i;
}
}
r[j]=q;
}
}</span>