動態規劃(dynamic programming)與分治法相似,都是經過組合子問題的解來求解原問題。可是不一樣的是:算法
分治法將問題劃分爲互不相交的子問題,遞歸地求解子問題,而後將子問題的解進行組合進而求出原問題的解。ide
動態規劃中,容許子問題之間存在交集,若是使用分治法,則對於交集的求解可能會執行屢次,形成重複計算,而動態規劃卻將交集的解保存到一個表格中,避免了重複計算的開銷(自底向上的遞歸實現)。優化
動態規劃經常使用來求解最優化問題,目的是尋找一個最優解(可能存在多個最優解),一個動態規劃算法一般包含以下3個步驟:spa
實例分析:鋼條切割code
問題描述:將n個單位長度的鋼條進行切割,在切割長度必須爲整數的狀況下,一共有2n-1種切割方案(因爲n個單位中間能夠有n-1個切割點,在每一個點中都有2種選擇:切或不切。所以,一共有2n-1種)。假設長度爲i的鋼條能夠得到收益爲pi,則如何切割才能使得總收益r最大?blog
長度i遞歸 |
1ci |
2io |
3event |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
收益pi |
1 |
5 |
8 |
9 |
10 |
17 |
17 |
20 |
24 |
30 |
分析:
假設將鋼條切割爲k段能夠得到最大收益,則有:
n = n1 + n2 + ... + nk
r = p1 + p2 + ... + pk
通常地,總收益r能夠以下表示:
r = max(pn, r1 + rn-1, r2 + rn-2, ..., rn-1 + r1),即,鋼條能夠不切割,或者先將鋼條切割爲兩端:長度爲i和n – i,接着遞歸地求解ri和rn-i。固然咱們也能夠對長度爲i的鋼條再也不進行第二次切割,那麼r的表達式變爲以下:
r = max(pn, p1 + rn-1, p2 + rn-2, ..., pn-1 + r1)
接着採用自底向上實現遞歸求解(關於自頂向下和自底向上兩種遞歸實現的區別見以下遞歸的兩種不一樣實現方式部分):
動態規劃中對於遞歸的求解採用自底向上求解順序,對每一個子問題只求解一次,並將結果保存下來,而沒必要從新計算,這是典型的以空間換時間的實例。
1 int CutSteel(int* p_proceeds, const int length) 2 { 3 int revenue[length + 1]; 4 revenue[0] = 0; 5 6 for (int n = 1; n <= length; ++n) 7 { 8 int temp = INT_MIN; 9 for (int i = 1; i <= n; ++i) 10 temp = std::max(temp, p_proceeds[i] + revenue[n - i]); 11 revenue[n] = temp; 12 } 13 14 return revenue[length]; 15 } 16 17 void test_dynamic() 18 { 19 int length = 5; 20 int proceeds[11] = // 不一樣長度的收益表 21 { 22 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30 23 }; 24 25 cout << CutSteel(proceeds, length) << endl; // 13 26 }
具有兩個特徵的問題適合採用動態規劃解決:最優子結構、子問題重疊。
若是一個問題的最優解包含其子問題的最優解,那麼咱們就稱此問題具有最優子結構的性質。咱們可使用子問題的最優解來構造原問題的最優解。如前所述,長度爲n的鋼條的最優解能夠由切割後的長度爲i的鋼條和長度爲n-i的鋼條的最優解求得。
對於不一樣的問題,最優子結構的不一樣體如今兩個方面:
1. 原問題的最優解涉及幾個子問題
2. 在肯定最優解使用哪一種子問題時,咱們須要考慮多少種選擇
如上鋼條切割問題中,僅僅使用了一個子問題,即長度爲n-i的鋼條的最優切割。可是咱們必須考察i的n個不一樣的取值中,哪個會產生最優解。
咱們可使用子問題的總數和每一個子問題須要考慮的選擇數這兩個因子的乘積來大體分析動態規劃的時間複雜度,如上鋼條切割問題中,共有Θ(n)個子問題,每一個子問題最多須要有n種不一樣的選擇,所以時間複雜度爲Ο(n2)。
動態規劃中對於重疊子問題的求解是一次性的,不須要重複計算。其求解過程主要是利用自底向上的遞歸解法。
動態規劃的步驟能夠概括爲兩步:
以斐波那契數列爲例:
/*
求斐波那契數列的第n項
思路1:自頂向下(一般咱們使用的方式)
*/
double Fibonacci(unsigned int n);
/*
求斐波那契數列的第n項
思路1不足:當n很小時能夠,可是當n逐漸增大時,其遞歸調用深度呈指數增長,
好比n = 40時須要5秒,而n = 50時須要665秒...
這主要是因爲遞歸中的重複計算引發的(遞歸的兩大弊端:1 重複計算 2 遞歸調用棧溢出):
好比咱們想計算fibonacci(5),則須要計算fibonacci(4)和fibonacci(3),而計算fibonacci(4)
時須要計算fibonacci(3)和fibonacci(2),如此fibonacci(3)便被重複計算了一次...。當n增大時,同一項可能被重複計算好屢次,形成時間的浪費
思路2:自底向上
爲了不重複計算,咱們能夠從下向上進行:由fibonacci(1)和fibonacci(0)計算獲得
fibonacci(2),由fibonacci(2)和fibonacci(1)計算獲得fibonacci(3),由fibonacci(3)和
fibonacci(2)計算獲得fibonacci(4),由fibonacci(4)和fibonacci(3)計算獲得fibonacci(5)
如此,沒有了重複的計算,時間複雜度降爲了O(n)
*/
double Fibonacci(unsigned int n, bool flag); // 當flag爲true時,使用自底向上計算
1 double Fibonacci(unsigned int n) 2 { 3 if (n == 0) 4 return 0; 5 6 if (n == 1) 7 return 1; 8 9 return Fibonacci(n - 1) + Fibonacci(n - 2); 10 } 11 12 double Fibonacci(unsigned int n, bool flag) 13 { 14 double result; 15 16 if (!flag) 17 result = Fibonacci(n); // 若是flag = false,則調用自頂向下遞歸解法 18 else 19 { 20 if (n == 0) 21 return 0; 22 23 if (n == 1) 24 return 1; 25 26 double fib_n_minus_1 = 1; // f(n - 1) 27 double fib_n_minus_2 = 0; // f(n - 2) 28 double fib_n = 0; // f(n) 29 30 for (unsigned int i = 2; i <= n; ++i) 31 { 32 fib_n = fib_n_minus_1 + fib_n_minus_2; // f(n) = f(n - 1) + f(n - 2) 33 34 fib_n_minus_2 = fib_n_minus_1; 35 fib_n_minus_1 = fib_n; 36 } 37 38 result = fib_n; 39 } 40 41 return result; 42 }