在作ACM題的時候咱們經常會遇到一類題:動態規劃。數組
初學者經常以爲動態規劃難以理解,而且用到動態規劃的題每每能用遞歸解決,但咱們知道,遞歸每每用時較長,並且動態規劃的思想很值得咱們借鑑學習,因此,動態規劃仍是頗有必要了解學習的。緩存
和之前同樣,我將嘗試用一種通俗易懂的方式解釋動態規劃的思想。函數
1、遞歸學習
在解釋動態規劃以前,有必要先了解遞歸。優化
試看這麼一道題:spa
Leetcode #62. Unique Pathscode
A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).blog
The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).遞歸
How many possible unique paths are there?leetcode
分析這道題,就會獲得某一點的路徑數量 f[x][y]=f[x-1][y]+f[x][y-1],也就是說咱們在(x,y)點只能有兩種選擇,一種是向右走,一種是向下走,分別是 f[x-1][y] 和f[x][y-1]。
很明顯,咱們能夠用dfs深搜遞歸獲得答案。
/* 深搜 runtime exceeded */ class Solution { public: int dfs(int m,int n){ if(m<1||n<1) return 0; if(m==1&&n==1) return 1; return dfs(m-1,n)+dfs(m,n-1); } int uniquePaths(int m, int n) { return dfs(m,n); } };
但很惋惜,只用這樣的遞歸的話在leetcode上面提交是會「runtime exceeded"的,咱們須要優化一下,如何優化呢?認真分析咱們會發如今遞歸的過程當中有不少是重複的過程。
好比說 向右走一步(x-1)再向下走一步(y-1) 和 向下走一步(y-1)再向右走一步(x-1) 走到的位置是相同的,可是咱們在兩個過程當中仍然重複的去求了 f[x-1][y-1]的路徑數。
這時候咱們再想:若是有一個」備忘錄「能記住咱們曾經求得的路徑數就行了,這樣便避免了重複過程。
/* 深搜+備忘錄(用一個cache做緩存) 0ms 28.59% */ class Solution { public: vector<vector<int>> cache; int dfs(int m,int n){ if(m<0||n<0) return 0; if(m==0||n==0) return 1; if(cache[m][n]>0) return cache[m][n]; return cache[m][n]=dfs(m-1,n)+dfs(m,n-1); } int uniquePaths(int m, int n) { cache=vector<vector<int>>(m,vector<int>(n,0)); cache[0][0]=1; return dfs(m-1,n-1); } };
這裏用了一個cache二維數組看成「備忘錄」,leetcode上AC。
2、動態規劃
既然咱們能夠自頂向下的遞歸獲得答案,天然咱們也能夠自底向上的用動態規劃獲得答案。
根據前面的公式,f[x][y]=f[x-1]f[y]+f[x]+f[y-1],說明某一位置的路徑數與它下面一位的路徑數和右邊一位的路徑數有關,因此在最下一行的路徑數全爲1,由於最下一行的下面沒有路可走。
如圖所示:
並且finish的上面一格路徑也爲1(只有一條路,就是向下)。
這樣對於第二行來講,路徑數就能肯定了(由於路徑數=右邊的+下面的)。
如圖:
第二行肯定了,第一行也就天然肯定了。
因此start位置的路徑數也就肯定了,這就是動態規劃解決此題的整個過程。
而且,在實際寫代碼的過程當中,咱們沒必要用一個二維數組來存放數據,只需用一個滾動數組每次更新數據便可。
即 f[j]=f[j]+f[j+1]
代碼以下:
class Solution { public: //動態規劃 int uniquePaths(int m, int n) { vector<int> nums(n,0); nums[0]=1; while(m--){ for(int cnt=1;cnt<n;cnt++) nums[cnt]+=nums[cnt-1]; } return nums[n-1]; } };
3、總結
如此咱們便獲得用動態規劃解題的通常過程:
1. 肯定狀態轉換方程,在這題裏,方程是:f[x][y]=f[x-1]f[y]+f[x]+f[y-1]。
2.肯定初始狀態的值。 最下面一行所有初始化爲1。
3.解題
4、其它例題
leetcode# 64. Minimum Path Sum
Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time.
Example 1:
[[1,3,1],
[1,5,1],
[4,2,1]]
Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum.
/*Solution 1:dfs+備忘錄 9ms 20.82% 1.注意到這裏定義了兩個函數dfs 和getOrUpdate,參考前面的例題,這裏由於每一步都有權重的關係,僅用一個函數是作不出來的。 2.注意到備忘錄的初始化與前面不一樣,初始化每一個都爲-1,也是由於權重的關係,權重可能爲0 3.if(m<0||n<0) 注意返回的是INT_MAX而不是0 */ class Solution { public: //dfs+備忘錄 vector<vector<int>> cache; int dfs(vector<vector<int>>& grid,int m,int n){ if(m<0||n<0) return INT_MAX; if(m==0&&n==0) return grid[m][n]; return min(getOrUpdate(grid,m-1,n),getOrUpdate(grid,m,n-1))+grid[m][n]; } int min(int m,int n){ return m<n?m:n; } int getOrUpdate(vector<vector<int>>&grid,int m,int n){ if(m<0||n<0) return INT_MAX; if(m==0&&n==0) return grid[m][n]; if(cache[m][n]>=0) return cache[m][n]; return cache[m][n]=dfs(grid,m,n); } int minPathSum(vector<vector<int>>& grid) { int m=grid.size(); if(m<=0) return 0; int n=grid[0].size(); cache=vector<vector<int>>(m,vector<int>(n,-1)); return dfs(grid,m-1,n-1); } }; /* Solution 2: 動態規劃 9ms 20.82% */ class Solution { public: //動態規劃 // ans[i][j]=min(ans[i][j-1],ans[i-1][j])+grid[i][j] int min(int m,int n){ return m<n?m:n; } int minPathSum(vector<vector<int>>& grid) { int m=grid.size(); if(m<=0) return 0; int n=grid[0].size(); vector<int> ans(n,0); ans=grid[m-1]; for(int cnt=n-2;cnt>=0;cnt--){ ans[cnt]+=ans[cnt+1]; } int i=m; for(int i=m-2;i>=0;i--){ ans[n-1]+=grid[i][n-1]; for(int j=n-2;j>=0;j--){ ans[j]=min(ans[j],ans[j+1])+grid[i][j]; } } return ans[0]; } };