什麼是 “動態規劃” , 用兩個經典問題舉例。
1.什麼是動態規劃?
看了很多題解,一般解決者開始就說用DP來解,然後寫了巢狀的for迴圈,不是很容易看懂,但是確實解出來了,我們這次來看下到底什麼是動態規劃?它有什麼特點呢?容我抄一段話:
動態規劃(Dynamic programming,DP),通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量: 一旦某個給定子問題的解已經算出,則將其記憶化儲存,以便下次需要同一個子問題解之時直接查表。 這種做法在重複子問題的數目關於輸入的規模呈指數增長時特別有用。
我又對動態規劃有了新的認識,動態規劃的核心是把問題分解成相對簡單的子問題,一般有遞迴的思想,但一般最終用for迴圈來解,這樣效率更高,也就是說遞迴的也是動態規劃,只是效率不高
2.經典問題1:斐波那契數列(Fibonacci polynomial)
我們直接用例子來舉例:
什麼是斐波那契數列呢?
就是類似這樣的數列:1,1,2,3,5,8,13,21 ...
我們這裡從0開始,就是除了第0個數和第1個數為1以外,後面的數等於前面兩個數之和。
2.1 用普通的遞迴來解斐波那契數列
int fib(int n){
if(n == 0 || n == 1){
return 1;
}
return fib(n - 1) + fib(n - 2);
}
先不考慮負數,上面的寫法是非常經典的遞迴。當n == 5的時候:fib(5)的計算過程如下:
fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
效率是非常低的,規模會成指數上升。這個解法有什麼問題呢?就是沒有儲存一些已經計算過的值,老是重複計算。
2.2 改用動態規劃來解
斐波那契數列
我們知道上面的問題在於沒有儲存一些已經計算的值,修改如下,改用兩個變數已經計算過的,感謝@水晶男人
int fib(int n)
{
int prev=1,next=1,tmp=2;
for(int i = 2; i <= n; i++){
tmp = prev + next;
prev = next;
next = tmp;
}
return tmp;
}
3.經典問題2 Triangle (Leetcode)
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.
For example, given the following triangle
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
The minimum path sum from top to bottom is
11
(i.e., 2 + 3 +5 +1 = 11).
這道題目就比上面的斐波那契數難些了。當弄下一個數時,還只能找相鄰的。比如當前一步是5,那下一步只能是1或者8。也就是說下一步的index只可能等於當前index,或者index+1。
3.1用遞迴來解 Triangle
我們的思路是從頭部開始,注意到遞迴主要要處理好引數和終止條件。我們這裡的終止條件是到達三角形的最後一層。要不斷變化的就是層數和index,因為我們要比較兩個數。程式變數寫的比較囉嗦,主要為了寫的明白些。
int minPath(vector<vector<int> > triangle, int index, int level) {
if( level== triangle.size()-1) {
return triangle[level][index];
}else{
int current = triangle[level][index];
int nextFirst = minPath(triangle, index, level+1);
int nextSecond = minPath(triangle, index+1, level+1);
int nextFirstTotal = nextFirst + current;
int nextSecondTotal = nextSecond + current;
return nextFirstTotal < nextSecondTotal ? nextFirstTotal : nextSecondTotal;
}
}
int minimumTotal(vector<vector<int> > &triangle) {
if(triangle.size()==0) return 0;
return minPath(triangle, 0, 0);
}
3.2用DP來解Triangle
我們注意到遞迴非常慢,為了求第1層的最短距離,要先求第2層。為了求第2層,要先求第3層。鋪了很多坑,最後全填上才能到結果。這個題目也很有意思,From top to bottom. 我們可以換個思路,從底部往上求。
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
我們可以用一個數組記錄已經求出的最短距離,剛開始初始化為最後一行的數,就是4,1,8,3,然後求最後一行到倒數第2行的最短距離。因為 1 + 6 < 4 + 6, 1 + 5 < 8 + 5, 3 + 7 < 8 + 7。所以我們更新這個陣列為7,6,10,3。最後一個3其實沒用了。我們以此類推:
最後一行到倒數第3行的最短距離更新為: 9,10,10,3。
最後一行到倒數第4行的最短距離更新為:11,10,10,3。
第一個數11就是我們要的結果。
int minimumTotal(vector<vector<int> > &triangle) {
int triangleSize = triangle.size();
if(triangleSize == 0){
return 0;
}
//初始化一個數組來記錄最後一行到當前行的最短距離
vector<int> saveMinDistance(triangle[triangleSize - 1].size(), 0);
//剛開始的值為最後一行的值
for(int i = 0; i < triangle[triangleSize - 1].size(); ++i){
saveMinDistance[i] = triangle[triangleSize - 1][i];
}
int first,second,current;
first = second = current = 0;
//從倒數第2行開始求到第1行
for(int i = triangleSize - 2; i >=0; i--){
//當第N行,需要求N+1個最短距離,並且儲存他們
for(int j = 0; j < i + 1; j++){
current = triangle[i][j];
first = current + saveMinDistance[j];
second = current + saveMinDistance[j + 1];
//儲存最短距離
saveMinDistance[j] = first < second ? first : second;
}
}
return saveMinDistance[0];
}
稍微優化點,去除了初始化最後一行,也放到for迴圈中:
int minimumTotal(vector<vector<int> > &triangle) {
int triangleSize = triangle.size();
if(triangleSize == 0){
return 0;
}
//初始化一個數組來記錄最後一行到當前行的最短距離
vector<int> saveMinDistance(triangle[triangleSize - 1].size() + 1, 0);
int first,second,current;
first = second = current = 0;
//從倒數第1行開始求到第1行
for(int i = triangleSize - 1; i >=0; i--){
//當第N行,需要求N+1個最短距離,並且儲存他們
for(int j = 0; j < i + 1; j++){
current = triangle[i][j];
first = current + saveMinDistance[j];
second = current + saveMinDistance[j + 1];
//儲存最短距離
saveMinDistance[j] = first < second ? first : second;
}
}
return saveMinDistance[0];
}
總的來說動態規劃還是比較難的技巧。因為首先當前題目要可以用動態規劃的思想來做,其次要完整的把整個題目都想清楚,儲存什麼值,如何for迴圈,都是要考慮的因素。
我們這裡用兩道題目來窺探下動態規劃,動態規劃還是比較難的技巧,後期再見。