1. 程式人生 > >【動態規劃】鋼條切割

【動態規劃】鋼條切割

花了幾天的時間鑽研演算法導論裡的動態規劃,做個總結。首先,演算法導論真是經典,可惜水平有限,於是乎忽略了一些推理與數學理論,留著有機會再深入,其次可能自己的理解不是很到位,有錯誤的地方歡迎提出,謝謝
   動態規劃(dynamic programming)與分治演算法相似,都是通過組合子問題的解來求解原問題(在這裡,“programming”指的是一種表格法,並非編寫計算機程式)。
一、與分治演算法區別:
   1.子問題是否重疊性
   ①分治演算法將問題劃分為互不相交的子問題,遞迴地求解子問題,再將它們的解組合起來,求出原問題的解。
   ②動態規劃應用於子問題重疊的情況,即不同子問題具有公共的子子問題(子問題的求解釋遞迴進行的,將其劃分為更小的子子問題)
   2.子問題是否重複求解

   ①分治演算法會反覆求解那些公共子問題。
   ②動態規劃對每個子子問題只求解一次,將其解儲存在一個表格中。(空間換時間)
二、動態規劃的求解步驟
   1.刻畫一個最優解的結構特徵。
   2.遞迴地定義最優解的值。
   3.計算最優解的值,通常採用自底向上的方法。
   4.利用計算出的資訊構造一個最優解。
   步驟1~3是動態規劃演算法求解問題的基礎,如果我們僅僅需要一個最優解的值,而非解本身,可以忽略步驟4。如果確實要做步驟4,有時就需要在執行步驟3的過程中維護一些額外資訊,以便用來構造一個最優解。
三、鋼條切割
   第一個應用動態規劃的例子是求解一個如何切割鋼條的簡單問題。
   Serling公司購買長鋼條,將其切割為短鋼條出售,切割工序本身沒有成本支出。公司管理層希望知道最佳的切割方案。假定我們知道Serling公司出售一段長度為i英寸的鋼條的價格為pi(i=1,2,..,單位為美元)。鋼條的長度均為整英寸,下圖給出一個價格表的樣例。
   

   鋼條切割問題時這樣的:給定一段長度為n英寸的鋼條和一個價格表pi(i=1,2,...,n),求切割鋼條方案,使得銷量收益rn最大,注意,如果長度為n英寸的鋼條的價格pn足夠大,最優解可能就是完全不需要切割。
   考慮n=4的情況,可以知道切割為p2+p2=5+5=10的收益(其中長度為n英寸的鋼條共有2^(n-1)種不同的切割方案,當然我們可以按照長度非遞減的順序切割小段鋼條,切割方案會少得多),為最優解。
   對於上述價格表樣例,我們可以觀察所有最優收益值ri(i=1,2,..,10)及對應的最優切割方案:
   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=4+3
   r8=22,切割方案8=2+6
   r9=25,切割方案9=3+6
   r10=30,切割方案10=10(無切割)

   更一般地,對於rn(n>=1),我們可以用更短的鋼條的最優切割收益來描述它:
          rn=max(pn,r(1)+r(n-1),r(2)+r(n-2),...,r(n-1)+r(1)) (這裡可以優化一下,例如7=1+6和7=6+1其實是一樣的,故到r(n/2)+r(n-n/2)時即可)
   其中第一個引數pn對應不切割,直接出售長度為n英寸的鋼條方案。其他n-1個引數對應另外n-1種方案;對應每個i=1,2,...,n-1,首先將鋼條切割為長度為i和n-i的兩段,接著求解這兩段的最優切割收益ri和r(n-i)(每種方案的最優收益為兩段的最優收益之和)。
   由於無法預知哪種方案會獲得最優收益,我們必須考察所有可能的i,選取其中收益最大者。
   注意到,為了求解規模為n的原問題,我們先求解形式完全一樣,但規模更小的子問題。即當完成首次切割後,我們將兩段鋼條看成兩個獨立的鋼條切割問題例項。我們通過組合兩個相關子問題的最優解,並在所有可能的兩段切割方案中選取組合收益最大者,構成原問題的最優解。我們稱鋼條切割問題滿足最優子結構(optimal substructure)性質:問題的最優解由相關子問題的最優解組合而成,而這些子問題可以獨立求解
遞迴方法求解最優鋼條切割問題
   除了上述求解,鋼條切割問題還存在一種相似的但更為簡單的遞迴求解:我們將鋼條從左邊切割下長度為i的一段,只對右邊剩下的長度為n-i的一段繼續進行切割(遞迴求解),對左邊的一段不再進行切割。
   即問題分解的方式為:將長度為n的鋼條分解為左邊開始一段,以及剩下部分繼續分解的結果。這樣不做任何切割方案就可以描述為:第一段的長度為n,收益為pn,剩餘部分長度為0,對應的收益為r0=0。於是我們可以得到上述公式的簡化版本:
          rn=max(pi+r(n-i)) (1<=i<=n)
   在此公式中,原問題的最優解只包含一個相關子問題(右端剩餘部分)的解,而不是兩個。
自頂向下遞迴實現

   下面的過程實現了公式的計算,它採用的是一種直接的自頂向下的遞迴方法。(原文是虛擬碼,這裡是C++語言的實現)

   int CUT_ROD(int *p,int n) /* 過程CUT_ROD以價格陣列p[1..n]和整數n為輸入,返回長度為n的鋼條的最大收益 */
   {
       if(n==0) return 0;    /* 若n=0,不可能有任何收益,返回0 */
	   int q=INT_MIN;        /* 將最大收益q初始化為負無窮,以便for迴圈可以正確計算 */
	   for(int i=1;i<=n;i++) q=max(q,p[i]+CUT_ROD(p,n-i));
	   return q;             /* 返回計算結果 */
   }


   但是用遞迴實現,一旦輸入規模稍微變大,程式執行時間會變得相當長,例如,對n=40,程式至少執行好幾分鐘(下面會給出效能對比),很可能超過一個小時。實際上,你會發現,每當將n增大1,程式執行時間差不多就會增加1倍。
   至於效率為什麼那麼差,原因在於CUT_ROD反覆地用相同的引數值對自身進行遞迴呼叫,即它反覆求解相同的子問題,下圖顯示了n=4時的呼叫過程:CUT_ROD(p,n)
   對於i=1,2,...n呼叫CUT_ROD(p,n-i),等價於對j=01,...,n-1呼叫CUT_ROD(p,j)。當這個過程遞迴展開時,它所做的工作量(用n的函式的形式描述)會爆炸性的增長。

   
   這棵遞迴呼叫樹顯示了n=4時,CUT_ROD(p,n)的遞迴呼叫過程。每個結點的標號為對應子問題的規模n,因此,從父結點s到子節點t的邊表示從鋼條左端切下長度為s-t的一段,然後繼續
   遞迴求解剩餘的規模為t的子問題。從根結點到葉結點的一條路徑對應長度為n的鋼條的2^(n-1)種切割方案之一。一般來說這棵遞迴樹共有2^n個結點,其中有2^(n-1)個葉子結點。
   分析執行時間,T(n)=2^n,CUT_ROD為執行時間為n的指數函式
使用動態規劃方法求解最優鋼條切割問題
    現在展示如何將CUT_ROD轉換為一個更高效的動態規劃演算法。
    動態規劃方法的思想如下所述。我們已經看到,樸素遞迴演算法之所以效率很低,是因為它反覆求解相同的子問題。因此,動態規劃方法仔細安排求解順序,對每個子問題只求解一次,並將結果儲存下來。
    如果隨後再次需要此子問題的解,只需查詢儲存的結果,而不必計算。因此,動態規劃方法是付出額外空間來節省計算時間,是典型的時空權衡(time-memorytrade-off)的例子。而時間上的節省可能使非常巨大的:可能將一個指數時間的解轉換為一個多項式時間的解。如果子問題的數量是輸入規模的多項式函式,而我們可以在多項式時間內求出每個子問題,那麼動態規劃方法的總執行時間就是多項式階的。
   動態規劃有兩種等價的實現方法,下面以鋼條切割為例展示這兩種方法。
   第一種方法稱為帶備忘的自頂向下法(top-down with memoization)。此方法仍按自然的遞迴形式編寫過程,但過程會儲存每個子問題的解(通常儲存在一個數組或散列表中)。當需要一個子問題的解時,過程首先檢查是否已經儲存過此解。如果是直接返回儲存的值,從而節省了計算時間;否則,按通常方式計算這個子問題。我們稱這個遞迴過程是帶備忘的(memoized),因為它“記住”了之前已經計算出的結果。
   第二種方法稱為自底向上法(bottom-up method)。這種方法一般需要恰當定義子問題“規模”的概念,使得任何子問題的求解只依賴於“更小的”子問題的求解。當求解某個子問題時,它所依賴的那些更小的子問題都已求解完畢,結果已經儲存。每個子問題只需求解一次,當我們求解它(也是第一次遇見它)時,它的所有前提子問題都已求解完成。
   兩種方法得到的演算法具有相同的漸近執行時間,僅有的差異是在某些特殊情況下,自頂向下方法並未真正遞迴地考察所有可能的子問題。由於沒有頻繁的遞迴函式呼叫開銷,自底向上方法的時間複雜性函式通常具有更小的係數。

   下面給出的是自頂向下CUT-ROD過程的c++語言程式碼,加入了備忘機制:

	int MEMOIZED_CUT_ROD(int *p,int n)
	{
		int *r=new int[n+1];
		for(int i=0;i<=n;i++) r[i]=INT_MIN;        /* 初始化為負無窮,這是一種常見的表示“未知值”的方法(已知的收益總是非負值)*/
		return MEMOIZED_CUT_ROD_AUX(p,n,r);
	}
	int MEMOIZED_CUT_ROD_AUX(int *p,int n,int *r)
	{
		int q;
		if(r[n]>=0) return r[n];         /* 若所需值已知,直接返回 */
		if(n==0) q=0;                    /* 否則計算 */ 
		else                              
		{
			q=INT_MIN;
			for(int i=1;i<=n;i++) q=max(q,p[i]+ MEMOIZED_CUT_ROD_AUX(p,n-i,r));
		}
		r[n]=q;                         /* 存入r[n] */
		return q;
	}

自底向上版本更為簡單:

	int BOTTOM_UP_CUT_ROD(int *p,int n)
	{
		int q;
		int *r=new int[n+1];
		r[0]=0;
		for(int i=1;i<=n;i++)
		{
			q=INT_MIN;
			for(int j=1;j<=i;j++) q=max(q,p[j]+r[i-j]); /* 內for迴圈的迭代次數構成一個等差數列。 */
			r[i]=q;
		}
		return r[n];
	}
     自底向上演算法和自頂向下演算法具有相同的漸近執行時間。
重構解
    前面給出的鋼條切割問題的動態規劃演算法返回最優解的收益值,但並未返回解本身(一個長度列表,給出切割後每段鋼條的長度)。我們可以擴充套件動態規劃演算法,使之對每個子問題不僅儲存最優收益值,還儲存對應的切割方案。利用這些資訊,我們就能輸出最優解。

    下面給出的是BOTTOM_UP_CUT_ROD的擴充套件版本,它對長度為i的鋼條不僅計算最大收益值ri,還儲存最優解對應的第一段鋼條的切割長度si:

	void EXTENDED_BOTTOM_UP_CUT_ROD(int *p,int n,int *r,int *s) /* 原文是返回兩個陣列,這裡通過傳入兩個陣列完成同樣操作 */
	{
		int q;
		r[0]=0;
		for(int i=1;i<=n;i++) 
		{
			q=INT_MIN;
			for(int j=1;j<=i;j++)
			{
				if(q<p[i]+r[i-j])
				{
					q=p[i]+r[i-j];
					s[i]=j;
				}	
			}
			r[i]=q;
		}
	}
     此過程與BOTTOM_UP_CUT_ROD很相似,差別只是在需要額外的陣列s,並在求解規模為i的子問題時將第一段鋼條的最優切割長度j儲存在s[i]中。

    下面過程輸出長度為n的鋼條的完整最優切割方案:

	void PRINT_CUT_ROD_SOLUTION(int *p,int n)
	{
		int *r=new int [n+1];
		int *s=new int [n+1];
		EXTENDED_BOTTOM_UP_CUT_ROD(p,n,r,s);
		while(n>0)
		{
			cout<<s[n];
			n=n-s[n];
		}
	}
     對於前面給出的鋼條切割例項,EXTENDED_BOTTOM_UP_CUT_ROD(p,10,r,s),中陣列r和陣列s如下所示:

    下面給出完整的程式碼實現,以及遞迴與動規解法的效率對比。

#include<iostream>
#include<ctime>
const int INT_MIN=-2147483647 - 1;
using namespace std;
/*自頂向下遞迴*/
int CUT_ROD(int *p,int n) 
{
       if(n==0) return 0;   
	   int q=INT_MIN;        
	   for(int i=1;i<=n;i++) q=max(q,p[i]+CUT_ROD(p,n-i));
	   return q;             
}
/*帶備忘的自頂向下法*/ 
int MEMOIZED_CUT_ROD_AUX(int *p,int n,int *r)
{
	int q;
	if(r[n]>=0) return r[n];         /* 若所需值已知,直接返回 */
	if(n==0) q=0;                    /* 否則計算 */ 
	else                              
	{
		q=INT_MIN;
		for(int i=1;i<=n;i++) q=max(q,p[i]+ MEMOIZED_CUT_ROD_AUX(p,n-i,r));
	}
	r[n]=q;                         /* 存入r[n] */
	return q;
}
int MEMOIZED_CUT_ROD(int *p,int n)
{
	int *r=new int[n+1];
	for(int i=0;i<=n;i++) r[i]=INT_MIN;        /* 初始化為負無窮,這是一種常見的表示“未知值”的方法(已知的收益總是非負值)*/
	return MEMOIZED_CUT_ROD_AUX(p,n,r);
}
/*自底向上法*/
int BOTTOM_UP_CUT_ROD(int *p,int n)
{
	int q;
	int *r=new int[n+1];
	r[0]=0;
	for(int i=1;i<=n;i++)
	{
		q=INT_MIN;
		for(int j=1;j<=i;j++) q=max(q,p[j]+r[i-j]); /* 內for迴圈的迭代次數構成一個等差數列。 */
		r[i]=q;
	}
	return r[n];
}
/*重構解*/
int EXTENDED_BOTTOM_UP_CUT_ROD(int *p,int n,int *r,int *s) /* 原文是返回兩個陣列,這裡通過傳入兩個陣列完成同樣操作 */
{
	int q;
	r[0]=0;
	for(int i=1;i<=n;i++) 
	{
		q=INT_MIN;
		for(int j=1;j<=i;j++)
		{
			if(q<p[j]+r[i-j])
			{
				q=p[j]+r[i-j];
				s[i]=j;
			}	
		}
		r[i]=q;
	}
	return r[n]; 
}
void PRINT_CUT_ROD_SOLUTION(int *p,int n)
{
	int *r=new int [n+1];
	int *s=new int [n+1];
	cout<<EXTENDED_BOTTOM_UP_CUT_ROD(p,n,r,s)<<" = "; 
	while(n>0)
	{
		cout<<"s["<<s[n]<<"] ";
		n=n-s[n];
	}
	cout<<endl; 
}
int main()
{
	int p[41]={0,1,5,8,9,10,17,17,20,24,30,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1};/*為了測試效能*/
	time_t start,finish,totaltime;
	cout<<"——————帶備忘的自頂向下法——————\n";
	start=clock();
	cout<<"n=40時最優解為:"<<MEMOIZED_CUT_ROD(p,40)<<endl;
	finish=clock();
	totaltime=(double)(finish-start)/CLOCKS_PER_SEC;
	cout<<"\n執行時間為"<<totaltime<<"秒!"<<endl;
	cout<<"————————自底向上法————————\n";
	start=clock();
	cout<<"n=40時最優解為:"<<BOTTOM_UP_CUT_ROD(p,40)<<endl;
	finish=clock();
	totaltime=(double)(finish-start)/CLOCKS_PER_SEC;
	cout<<"\n執行時間為"<<totaltime<<"秒!"<<endl;
	cout<<"————————自頂向下遞迴———————\n";
	start=clock();
	cout<<"n=40時最優解為:"<<CUT_ROD(p,40)<<endl;
	finish=clock();
	totaltime=(double)(finish-start)/CLOCKS_PER_SEC;
	cout<<"\n執行時間為"<<totaltime<<"秒!"<<endl;
	cout<<"————————————————————\n";
	cout<<"n=30時最優解為:"<<endl;
	PRINT_CUT_ROD_SOLUTION(p,30);
	cout<<"n=29時最優解為:"<<endl;
	PRINT_CUT_ROD_SOLUTION(p,29);
	return 0;
	
}
當n=30時


當n=40時


   通過比較可以發現,動規與遞迴的效能,在時間上差很多,當n=40時,遞迴的的時間是28194秒,而動規的時間很小,當n=160時也是很小的時間。所以在時間上的節省是巨大的。

相關推薦

動態規劃鋼條切割

花了幾天的時間鑽研演算法導論裡的動態規劃,做個總結。首先,演算法導論真是經典,可惜水平有限,於是乎忽略了一些推理與數學理論,留著有機會再深入,其次可能自己的理解不是很到位,有錯誤的地方歡迎提出,謝謝。    動態規劃(dynamic programming)與分治演算法相似

Java滾動數組動態規劃UVA - 11137 - Ingenuous Cubrency

得到 lose math scanner light clas details 狀態 ann 滾動數組優化自己畫一下就明白了。 http://blog.csdn.net/u014800748/article/details/45849217 解題思路:本題利用遞推關系解決。

動態規劃 Codeforces Round #416 (Div. 2) C. Vladik and Memorable Trip

and main spa def esp 動態 return 價值 can 劃分那個序列,沒必要完全覆蓋原序列。對於劃分出來的每個序列,對於某個值v,要麽全都在該序列,要麽全都不在該序列。 一個序列的價值是所有不同的值的異或和。整個的價值是所有劃分出來的序列的價值之和。

動態規劃Codeforces Round #406 (Div. 2) C.Berzerk

[1] space node sca 一個 for 隊列 ber 動態規劃 有向圖博弈問題。 能轉移到一個必敗態的就是必勝態。 能轉移到的全是必勝態的就是必敗態。 轉移的時候可以用隊列維護。 可以看這個 http://www.cnblogs.com/quintessence

動態規劃CDOJ1271 Search gold

mage images sin class png http std ret urn 方格取數。 但由於題意說金幣數<0就死了,就不能繼續轉移。 #include<cstdio> #include<algorithm> #include&l

動態規劃最長公共子序列問題

clas == 搜索 ios for 參考 pan 公式 是否 題目描述: 給定兩個字符串s1s2……sn和t1t2……tn。求出這兩個字符串最長的公共子序列的長度。字符串s1s2……sn的子序列指可以表示為si1si2……sim(i1<i2<……<im)

TarjanLCA動態規劃推導hdu6065 RXD, tree and sequence

and main ack find turn hdu mes ear 高明 劃分出來的每個區間的答案,其實就是連續兩個的lca的最小值。 即5 2 3 4 這個區間的答案是min(dep(lca(5,2)),dep(lca(2,3),dep(lca(3,4))))。 於是d

動態規劃windy數

center log char enter tdi ++ getc windy數 ace BZOJ1026: [SCOI2009]windy數 Time Limit: 1 Sec Memory Limit: 162 MBSubmit: 7893 Solved: 3

DFS拓撲排序動態規劃Gym - 100642A - Babs' Box Boutique

關鍵字 dag 在一起 ems class std rst ++i box 給你10個箱子,有長寬高,每個箱子你可以決定哪個面朝上擺。把它們摞在一起,邊必須平行,上面的不能突出來,問你最多擺幾個箱子。 3^10枚舉箱子用哪個面。然後按長為第一關鍵字,寬為第二關鍵字,從大到小

原根動態規劃bitset2017四川省賽 K.2017 Revenge

iostream 我們 eset main pen 乘法 四川 動態 概論 題意: 給你n(不超過200w)個數,和一個數r,問你有多少種方案,使得你取出某個子集,能夠讓它們的乘積 mod 2017等於r。 2017有5這個原根,可以使用離散對數(指標)的思想把乘法轉化成加

Codeforces 949DShake It! 動態規劃

href 動態 ++ dot ref ima scanf ces c++ 參考: http://blog.csdn.net/gjghfd/article/details/77824901 所求的是滿足條件的圖中“不同構”的數量,意味著操作的順序是可以忽略的

動態規劃背包問題

urn 兩個 pro 數組實現 可以轉化 轉化 題目 int 遞增   背包問題無疑是最經典的dp問題,其次就是關於字符串匹配問題,數組最長遞增(減)序列長度等等。背包問題變體很多。   動態規劃問題實際上與備忘錄式深搜有些類似。 1. 0-1背包 題目:   有n個重量和

動態規劃背包問題相關題目

scanf man 初始 ads 無法 ger %d val more 1.poj 1742 Description People in Silverland use coins.They have coins of value A1,A2,A3...An Silverla

動態規劃ZZNU-OJ- 2054 : 油田

tex 搜索 n) 正整數 頁面 復制 最優解 使用 包含 2054 : 油田 (一個神奇的功能:點擊上方文字進入相應頁面) 時間限制:1 Sec 內存限制:32 MiB提交:49 答案正確:6 提交 狀態 討論區 題目描述 在太平洋的一片海域,發現了大量的油田! 為了方

動態規劃最大正方形 (洛谷 P1387 最大正方形)

代碼 log mar 最小 down 思路 計數 -m i++ 輸入格式: 輸入文件第一行為兩個整數n,m(1<=n,m<=100),接下來n行,每行m個數字,用空格隔開,0或1。 輸出格式: 一個整數,最大正方形的邊長。 輸入輸出樣例 輸入樣例: 4 4 0

POJ1390 Blocks 動態規劃

data alt ++ his fin tracking out printf box Blocks Time Limit:?5000MS ? Memory

poj 2385動態規劃

ostream bmi 。。 set row include urn get 定義 poj 2385 Apple Catching Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 1400

動態規劃電路布線問題

bar ref end htm tracking arc tools order 情況下 算法筆記——【動態規劃】電路布線問題 原創 2013年03月14日 09:18:27 標簽: 電路布線 / 算法筆記 / 動態規劃 / 最優子結構 12785

動態規劃Part2

好的 貪心 同學 不能 方案 所有 bsp 寶貝 愛好 01背包問題 題目描述:小P同學愛好探險尋寶,一天他去了伊利哇呀半島發現了一批寶藏,但不幸的是小P很懶,出門只帶了一個背包,所以註定他不能帶走所有的寶藏。但是小P又很貪心想帶走盡量多的寶藏。已知每種寶貝的重量與價值是

動態規劃完全背包

max cout col 一份 span blank ref http ++ 完全背包與01背包的區別就是 01背包只有一次, 而完全背包有無限 我的01背包 完全背包 dp[i-1][j - k*weight[i]] +k*value[i] 經歷了01背包,那