首页 > 代码库 > 什么是 “动态规划” , 用两个经典问题举例

什么是 “动态规划” , 用两个经典问题举例

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 用普通的递归来解斐波那契数列

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int fib(int n){  
  2.     if(n == 0 || n == 1){  
  3.         return 1;  
  4.     }  
  5.     return fib(n - 1) + fib(n - 2);  
  6. }  


先不考虑负数,上面的写法是非常经典的递归。当n == 5的时候:fib(5)的计算过程如下:

  1. fib(5)
  2. fib(4) + fib(3)
  3. (fib(3) + fib(2)) + (fib(2) + fib(1))
  4. ((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
  5. (((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

效率是非常低的,规模会成指数上升。这个解法有什么问题呢?就是没有保存一些已经计算过的值,老是重复计算。


2.2 改用动态规划来解斐波那契数列


我们知道上面的问题在于没有保存一些已经计算的值,修改如下,改用两个变量已经计算过的,感谢@水晶男人的建议:

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int fib(int n)    
  2. {    
  3.     int prev=1,next=1,tmp=2;   
  4.   
  5.     for(int i = 2; i <= n; i++){    
  6.         tmp = prev + next;  
  7.         prev = next;  
  8.         next = tmp;  
  9.     }    
  10.     return tmp;        
  11. }    




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,因为我们要比较两个数。程序变量写的比较啰嗦,主要为了写的明白些。

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int minPath(vector<vector<int> > triangle, int index, int level) {    
  2.     if( level== triangle.size()-1) {    
  3.         return triangle[level][index];   
  4.     }else{  
  5.         int current = triangle[level][index];  
  6.         int nextFirst = minPath(triangle, index, level+1);  
  7.         int nextSecond = minPath(triangle, index+1, level+1);  
  8.   
  9.         int nextFirstTotal = nextFirst + current;  
  10.         int nextSecondTotal = nextSecond + current;  
  11.   
  12.         return nextFirstTotal < nextSecondTotal ? nextFirstTotal : nextSecondTotal;  
  13.     }    
  14.   
  15. }    
  16.   
  17. int minimumTotal(vector<vector<int> > &triangle) {  
  18.       
  19.     if(triangle.size()==0) return 0;    
  20.      
  21.     return minPath(triangle, 0, 0);  
  22. }  


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就是我们要的结果。


[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int minimumTotal(vector<vector<int> > &triangle) {  
  2.     int triangleSize = triangle.size();  
  3.     if(triangleSize == 0){  
  4.         return 0;  
  5.     }  
  6.     //初始化一个数组来记录最后一行到当前行的最短距离  
  7.     vector<int> saveMinDistance(triangle[triangleSize - 1].size(), 0);  
  8.     //刚开始的值为最后一行的值  
  9.     for(int i = 0; i < triangle[triangleSize - 1].size(); ++i){  
  10.         saveMinDistance[i] = triangle[triangleSize - 1][i];  
  11.     }  
  12.       
  13.     int first,second,current;  
  14.     first = second = current = 0;  
  15.     //从倒数第2行开始求到第1行  
  16.     for(int i = triangleSize - 2; i >=0; i--){  
  17.         //当第N行,需要求N+1个最短距离,并且保存他们  
  18.         for(int j = 0; j < i + 1; j++){   
  19.             current = triangle[i][j];  
  20.             first = current + saveMinDistance[j];  
  21.             second = current + saveMinDistance[j + 1];  
  22.             //保存最短距离  
  23.             saveMinDistance[j] = first < second ? first : second;  
  24.         }  
  25.     }  
  26.     return saveMinDistance[0];  
  27. }    


稍微优化点,去除了初始化最后一行,也放到for循环中:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. int minimumTotal(vector<vector<int> > &triangle) {  
  2.     int triangleSize = triangle.size();  
  3.     if(triangleSize == 0){  
  4.         return 0;  
  5.     }  
  6.     //初始化一个数组来记录最后一行到当前行的最短距离  
  7.     vector<int> saveMinDistance(triangle[triangleSize - 1].size() + 1, 0);  
  8.       
  9.     int first,second,current;  
  10.     first = second = current = 0;  
  11.     //从倒数第1行开始求到第1行  
  12.     for(int i = triangleSize - 1; i >=0; i--){  
  13.         //当第N行,需要求N+1个最短距离,并且保存他们  
  14.         for(int j = 0; j < i + 1; j++){   
  15.             current = triangle[i][j];  
  16.             first = current + saveMinDistance[j];  
  17.             second = current + saveMinDistance[j + 1];  
  18.             //保存最短距离  
  19.             saveMinDistance[j] = first < second ? first : second;  
  20.         }  
  21.     }  
  22.     return saveMinDistance[0];  
  23. }    


总的来说动态规划还是比较难的技巧。因为首先当前题目要可以用动态规划的思想来做,其次要完整的把整个题目都想清楚,保存什么值,如何for循环,都是要考虑的因素。

我们这里用两道题目来窥探下动态规划,动态规划还是比较难的技巧,后期再见