盡管排序算法還有很多沒有說,但因為這篇文章是已經現成有的,就先上這個,回頭再把排序補一下。
好的開始~BigMoyan有一個好基友叫zou先生,zou先生除了是BigMoyan在學校的社團老大外,還是一家專門為夜總會提供鋼管的公司的區域經理。最近,zou經理發現這樣一個事情,夜總會需要各種長度的鋼管用作各種用途,然而每種長度的鋼管的價格卻不一樣,總而言之如下表。
從前,zou經理總是傻乎乎的把總公司發來的10m長鋼管以25塊的價格賣出去,但是某天他跟學校的打菜阿姨聊起人生和事業的時候,打菜阿姨卻來了一句:
“明明每根鋼管能賣27塊錢,你只賣25,學過C++嗎?”
這深深傷害了zou經理的心靈,於是他向室友Jie求救。
以下是BigMoyan轉述Jie的分析:
首先我們正式的表達一下這個問題,有一段長度為i的鋼管,整段出售的價格為Pi,求適當的切割鋼條方案使得獲利最大。為了便於zou理解問題,jie先用一個例子做了一下簡單說明。
比如你現在有一段長為4m的鋼管,如果整段賣的話,賣9塊。然而你也可以切成兩段2m的鋼管分別賣,這樣一共可以賣5*2=10塊,所以並不是整段賣就一定獲益最大的。假設對於長為i的鋼管,獲益最大的售價為Ri,我們試着窮舉一下。
長為1的鋼管,i=1,只能整段出售,R1=1
長為2的鋼管,i=2,很容易發現整段出售獲益最高,R2=2
…
長為4的鋼管,i=4,上面分析了應該切成兩段,R4=10=5+5
長為5的鋼管,整段出售是10塊,分成1+4,因為R4=10,所以可以賣11塊,分成2+3,則可賣13塊,因此R5=2+3=13
…
由於jie看了BigMoyan關於快速排序的介紹,受到分治策略的影響,於是jie剛開始自然而然的認為這是一個可以遞歸解決的問題,對於長為i的鋼管,其最大獲益為:
Ri= max{Pn, R1+Rn-1,R2+Rn-2,…Ri-1+R1}
這個意思是說,為了求Ri,我們可以把鋼管分為和為i的兩段,而這兩段各自又是一個求大獲益的問題,正如BigMoyan曾經說過的那樣,想要理解遞歸,首先,你要理解遞歸。然后在各種分法中找出最大的那個,自然就是結果。注意上式中第一個參數Pn為整段出售的價格。
Jie把思路告訴了BigMoyan,BigMoyan很快為zou寫了這樣一段程序(沒錯我就是這樣一個仗義的人),用C++寫大概是下面這個樣子(BigMoyan用的最多的依然是Python,可惜Python沒有做尾遞歸優化,只能暫時用一把C++了):
#include<iostream> using namespace std; int Max(int i, int j){ return i>j?i:j; } int cut_rod(int p[],int n){ if(n==0) return 0; int f=0; int q=-1; for(int i=1;i!=n;i++){ f=Max(p[n-1],cut_rod(p,i)+cut_rod(p,n-i)); q=Max(q,f); } return q; } int main(){ int p[10]={1, 5, 8, 9, 10, 17, 17, 20, 24, 25}; int f=cut_rod(p,10); cout<<f; return 1; }
老規矩,碰到代碼不要慌,拿個實例稍微分析一下,例如求長為5的鋼管的最大收益。調用函數cut_rod(p,5)
進去以后, if測試失敗,進入for循環
i=1:
f=Max(p[4],cut_rod(p,1)+cut_rod(p,4));
找出了p[4]和cut_rod(p,1)+cut_rod(p,4)的大者,前者為5m鋼管整段出售的價格(下標從0開始所以是5-1=4),后者將5m分為1m和4m,將兩段的最大收益加起來作為進行1+4分割的最大收益。
q=Max(q,f);
第一趟的時候q=-1,所以此時q=f保存1+4分割時的最大收益。
i=2:
f=Max(p[4],cut_rod(p,1)+cut_rod(p,4));
如上,此次找的是整段出售和2+3分割的最大收益之間的大者。
q=Max(q,f);
q在上次保存了f,這次將新f和上一次的值(q)比較,拿出最大的作為最大收益
后面以此類推
然而在測試代碼的時候BigMoyan發現,這代碼執行效率太他喵的低了!n比較大的時候,基本上n增加1,代碼運行時間就要加兩倍,我要敢測試一個500m長的鋼管的最大收益(假設數據都有),估計zou經理都要畢業了。
稍加分析發現,執行效率低的原因在於子問題一直在重復計算!計算10m鋼管收益的時候要把1~9m的收益全算一次,而為了計算9m鋼管的收益,我又要把1~8m的都算一次。
心!好!累!
此時聰明的jie也已經察覺到了問題所在,這個問題看起來是遞歸,其實卻與遞歸略有不同。遞歸的子問題互相不相交,而這個東西的子問題是相交的,相同的子問題被一遍一遍的計算,才導致了效率的低下。
Jie略一思忖,計上心來,既然子問題被一遍遍的計算,我們何不以空間換時間,把已經被計算好的子問題的結果保存起來,當需要的時候首先查詢,如果這個問題已經算好了,直接拿來用就是,若沒算好再進行計算。
BigMoyan拍案叫絕,再次改寫了代碼,於是就有了下面這段C++ code:
nt cut_rod(int p[],int n){ int *r=new int[n]; for(int i=0;i!=n;i++) r[i]=-1; return cut_rod_memo(p,n,r); }
調用函數名保持一致方便理解,首先建立一個備忘錄r,r用來保存已經被計算好的子問題的答案。將它初始化為-1。我們還是以計算5m鋼管為例,那么r被初始化為[-1,-1,-1,-1,-1].
接着調用cut_rod_memo來計算最大收益,那么cut_rod_memo是什么呢?
nt cut_rod_memo(int p[], int n, int r[]){ if(r[n-1]>=0) return r[n-1]; if(n==0) int q=0; else{ int f=0; int q=-1; for(int i=1;i!=n;i++){ f=Max(p[n-1],cut_rod_memo(p,i,r)+cut_rod_memo(p,n-i,r)); q=Max(q,f); } r[n-1]=q; return q; } }
好的,不要頭大,BigMoyan慢慢分析。
進入cut_rod_memo后,首先查詢5m的結果是不是已經算好了,答案是當然沒有,r[4]=-1。於是進入else語句,這里的語句跟之前遞歸的版本一模一樣,區別只在於遞歸調用的是cut_rod_memo,因為該函數里第一個語句就是判斷子問題答案有沒有做好,所以避免了多次計算相同的子問題,下面展示這一過程。
i=1:
由之前的無腦遞歸的版本可知,for循環解決了i=1時的最大收益,在for循環完成后,將其保存在r[0]中,此時r[0]=1
i=2:
在遞歸調用cut_rod_memo(p,1,r)時,因為已經算好r[0]=1,故cut_rod_memo的第一個if判斷成功,返回r[0],此時的返回值由已經計算過的結果直接拿出,沒有重新計算。
其他情況同理。
BigMoyan與jie兩人相對而笑,笑容中充滿着對對方的贊賞。他們一起來到zou經理折戟的銀樺食堂,與打菜阿姨如此這般一講,本指望收到阿姨贊美的眼光,沒想到阿姨把勺子里的菜抖掉一些,淡然說道:
“唔?這不就是動態規划嗎,什么新鮮東西,值得來浪,刷卡!”
Ps.動態規划問題當然不止這一類,但其基本思想是一致的,就是不要重復解決已經解決過的問題,感興趣的同學可以百度一下動態規划
PPS:下面是全部C++代碼
1 #include<iostream> 2 using namespace std; 3 4 int Max(int i, int j){ 5 return i>j?i:j; 6 } 7 8 int cut_rod_memo(int p[], int n, int r[]){ 9 if(r[n-1]>=0) 10 return r[n-1]; 11 if(n==0) 12 int q=0; 13 else{ 14 int f=0; 15 int q=-1; 16 for(int i=1;i!=n;i++){ 17 f=Max(p[n-1],cut_rod_memo(p,i,r)+cut_rod_memo(p,n-i,r)); 18 q=Max(q,f); 19 } 20 r[n-1]=q; 21 return q; 22 } 23 } 24 25 int cut_rod(int p[],int n){ 26 int *r=new int[n]; 27 for(int i=0;i!=n;i++) 28 r[i]=-1; 29 return cut_rod_memo(p,n,r); 30 } 31 32 int main(){ 33 int p[10]={1, 5, 8, 9, 10, 17, 17, 20, 24, 25}; 34 int f=cut_rod(p,10); 35 cout<<f; 36 return 1; 37 }