【基礎算法】切割鋼管與動態規划


  盡管排序算法還有很多沒有說,但因為這篇文章是已經現成有的,就先上這個,回頭再把排序補一下。

  好的開始~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 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM