演算法導論 動態規劃之「鋼管切割」問題

2021-06-28 06:30:15 字數 3568 閱讀 4446

動態規劃,其實跟分治法有些相似,基本思想都是將複雜的問題分成數個簡單的子問題,然後再去解決。它們的區別在於,分治法關注的子問題不相互「重疊」,而動態規劃關注的子問題,多是相互「重疊」的。比如在快速排序中,我們將資料分成兩部分,這兩部分再分別快速排序的遞迴思想,也就是將整個問題的排序劃分為子問題子陣列的排序,但是這兩個子陣列的排序之間並沒有相互聯絡,a子陣列的排序不會因為b子陣列的排序而得到任何「好處」或者「壞處」。但是有些時候,劃分的子問題之間卻是有聯絡的,比如下面的「鋼管切割」問題:

某公司生產長鋼管,然後一般,會將鋼條切斷,變成不同長度,然後去售賣。其中有個問題是,不同長度的鋼管的售價是不一樣的,但是它們並不是完全按照比例來,比如2公尺的鋼管售價要比3公尺的鋼管售價要少,但是並不是2比3的比例。鋼管的長度售價表如下:

長度i1      2      3      4      5      6      7      8      9      10

**pi

1      

5      

8      

9     

10    

17    

17     

20    

24    

30於是問題就來了,比如30公尺長的鋼管,要如何切割,切割成多長的幾條,才能讓售價最高,收益最高呢?

最簡單直接的想法,就是用暴力破解,n長的鋼管,可以分解成i長和n-i長的兩段,因為i可以從0~n取值,所以我們可以對i不進行繼續切割,於是對於長為i的這一段,可以直接呼叫價錢陣列p[i]來得到價錢,然後加上對n-i遞迴呼叫求最優收益的函式的返回值。在過程之中記錄這些組合的最優收益,等迴圈結束的時候,就能得到最優的收益價錢。

假設r[n]代表的是n長的鋼管的切割最佳收益值,陣列p代表上面表中的**,其中p[0]=0,從p[1]~p[10]對應上面表中的資料,那麼按照上面的想法,有公式:

r[n]=max(p[i]+r[n-i]),i從1到n,當n=0時,r[n]=0,因為0長的鋼管售價當然為0。

於是給以下實現**:

int cut_rod(int* p, int n) 

int q = -1;

for (int i = 1; i <= n; i++)

return q;

}

這種方法比較容易理解,但是效能是不是好呢?

可以簡單的以n=4的情況來看一下:

n=4的劃分(其中前面的那一段是直接使用p[i],後面一段呼叫函式來求最佳收益):

cut_rod(p,4)的劃分可能:

①1長和3長:p[1]+cut_rod(p,3)

②2長和2長:p[2]+cut_rod(p,2)

③3長和1長:p[3]+cut_rod(p,1)

④4長和0長:p[4]+cut_rod(p,0)

而其中cut_rod(p,3)又可以劃分為陣列p中元素與cut_rod(p,0),cut_rod(p,1)和cut_rod(p,2);以此類推,可以給出一種以遞迴呼叫樹的形式展示cut_rod遞迴呼叫了多少次:

不難從圖中看出,做了大量重複工作,以n=2的節點為例,分別在n=4和n=3的時候都被呼叫了。根據上圖,可以給出遞迴呼叫次數的乙個公式,假設t(n)表示cut_rod第二個引數為n時的呼叫次數,t(0)這時候是為1的,因為根結點的第一次呼叫也要算進去。於是有:

t(n)=1+t(0)+t(1)+...+t(n-1)

使用歸納法,可以比較容易的得出:t(n)=2^n

指數次冪的呼叫次數,顯然太大,我們稍微讓n大一點,則會讓整個過程變的漫長。

而實際上我們不需要在每次都去重新計算cut_rod的在n=2時的結果,只需要在第一次計算的時候將結果儲存起來,然後再需要的時候直接使用即可。這其實就是所謂的動態規劃演算法。

這裡的思路有兩種,一種叫帶備忘的自頂向下方法,是順著之前的**,當需要的時候去檢查是不是已經計算好了,如果是,則直接使用,如果不是,則計算,並儲存結果。第二種思路是自底向上方法,不論需不需要,先將子問題一一解決,然後再來解決更一級的問題,但要注意的是,我們需要先從最小的子問題開始,依次增加規模,這樣每一次解決問題的時候,它的子問題都已經計算好了,直接使用即可。

帶備忘的自頂向下方法:

int memoized_cut_rod_aux(int* p, int n, int* r) 

int q = -1;

if (n == 0) else

} r[n] = q;

return q;}/*

* 自頂向上的cut-rod的過程

*/int memoized_cut_rod(int* p, int n)

return memoized_cut_rod_aux(p, n, r);

}

自底向上的方法:

/*

* 自底向上的方式,先計算更小的子問題,然後再算較大的子問題,由於較大的子問題依賴於更小的子問題的答案,所以在計算較

* 大的子問題的時候,就無需再去計算更小的子問題,因為那答案已經計算好,且儲存起來了

*/int bottom_up_cut_rod(int p, int n)

r[j] = q;

} return r[n];

}

上面兩種演算法的時間複雜度都是o(n^2)。

上面的**只給出了最優的收益值,但是卻沒有給出最優收益到底是在那種切割分配方式下得到的,比如說n=9時,最佳收益為25,要分成3和6兩段。這裡可以使用另乙個陣列s來儲存分段情況,比如s[9]儲存3,然後我們讓n=9-3,就可以得到s[6]的最佳分段情況,發現就是6,於是就不需要繼續。

只需要將**稍微修改即可達到目的:

#includeusing namespace std;

/* * 儲存結果的結構體,裡面包含r和s兩個陣列,分別儲存最佳收益和最佳收益時的分段數值

*/struct result

~result()

};result* extended_bottom_up_cut_rod(int p, int n)

} res->r[i] = q;

} return res;

}int main() ;

int n = 9;

result* res = extended_bottom_up_cut_rod(p, n);

cout << "最佳收益:" << res->r[9] << endl;

//迴圈輸出實際的最佳分割段長

cout << "分段情況:";

while (n > 0)

delete res;

return 0;

}

執行上面程式,我們就可以的得到長度為9的鋼管的最佳收益以及對應的切割情況:

最佳收益:25

分段情況:3 6 

動態規劃 鋼管切割問題

給一條鋼管,切割成不同長度的鋼管 也可以不切割 不同的鋼管長度對應不同的價值,求這根鋼管獲得的最大價值。輸入 第一行包含乙個正整數l,為鋼管的長度。第二行包含l個正整數a1 al,為長度從1到l的鋼管對應的價值。輸出 輸出一行,為鋼管能獲得的最大價值。輸入樣例1 5 1 2 4 8 16 輸出樣例1...

演算法導論 動態規劃之鋼條切割

動態規劃和分治策略相似,不同的是,它針對的問題所分解出的小問題數量很多且很多是重複的。動態規劃就是使得這些重複的小問題只需要計算一次,避免重複計算。鋼條切割問題 給定一段長度為n英吋的鋼條和乙個 表pi i 1,2,n 求切割鋼條方案,使得銷售收益rn最大。注意,如果長度為n英吋的鋼條的 pn足夠大...

動態規劃 鋼條切割《演算法導論》

給定各長度鋼鐵單位 以及乙個長度為n的鋼條,求最大效益 1 10 分別為p include includeint memorized cut rod aux int p,int len,int r 樸素遞迴演算法 演算法複雜度為2 n級這裡寫 int cut rod int p,int len in...