有路径问题浅谈动态规划
什么是动态规划
在网上找到一个很形象的例子,猿爸爸把 1+1+1+1+1+1+1+1 = 写在纸上,问小猿(咦):「它们加起来是多少哇?」
(数了一会…)「8 !」
猿爸爸在左边又加了个 1+,再问一次小猿:「现在呢?」
(迅速地)「9 !」
「为什么小猿这么快就知道了呢?」
「因为你刚刚加了 1 啊~」
「所以只要记得之前的结果,就不用再计一次数啦。」
嗯,动态规划就是一种「先记住些事,方便后面节约时间」的神奇方法
定义
Dynamic Programming is mainly an optimization over plain recursion. Wherever we see a recursive solution that has repeated calls for same inputs, we can optimize it using Dynamic Programming. The idea is to simply store the results of subproblems, so that we do not have to re-compute them when needed later. This simple optimization reduces time complexities from exponential to polynomial.
以上GeeksforGeeks对动态规划的诠释,于我而言,我认为动态规划的核心思想就是如何将原有的问题拆分成子问题。如果这个问题我们要是可以用滴归来解决,那么我们一般可以通过动态规划的思路来降低时间复杂度。
动态规划套路
我认为动态规划总体来讲可以分为三个步骤:
设定状态
初始化
状态转移方程具体这三步指的是什么,我将通过路径问题来向大家逐一介绍。
网上有很多大神总结动态规划可以分为自下而上和自上而下,这一块我天资愚钝,没有领悟得很透彻,就不过多深入去说。
例题
本题是我从leetcode上的第62题,可以说是一道非常经典的动态规划入门题:
题目让我们找到从图的左上角到右下角的所有路径,告诉我们只能向下或者向右走。
题目分析
首先这个题是一个计数问题,提到计数这个关键词,那么很快我们就可以联想到程序猿爸爸让儿子1+1+1....
状态
我们可以将这个题转化为一个m*n的矩阵,矩阵上的每一个格子的值就代表从左上到达该格子的所有路径总和。
如图所示,由A到B最多有两个路径:1 右 下 2 下 右。所以B对应的状态为2,A对应的状态为1
初始化
明确了状态之后,我们需要对矩阵初始化,为了题目给定了矩阵的长和宽,那么我们就可以创建一个对应的m*n的矩阵,然后我们知道初始位置是左上角,所以其初始值为1,然后最上面的一行和最左面的一行值也都为1。因为我们只有一种方式可以到达他们。
状态转换方程
回到图中,我们已经已知了初始值,那么我们如何求解到问号位置的值呢?我们发现到达?位置的路径总和就是等于到达?位置上方和左边的值相加。因为题目告诉我们只有两种移动方式。对应的方程就是res[i][j] = res[i - 1][j] + res[i][j - 1];
结果展示
所以如果我们给出一个4*4的矩阵,那么一共存在20种走法从左上走到右下
代码实现
```
public int uniquePaths(int m, int n) {
//corner case
if (m == 0 || n == 0)
return 0;
if (m == 1 || n == 1)
return 1;
// init state
int[][] res = new int[m][n];
for (int i = 0; i < m; i++) {
res[i][0] = 1;
}
for (int i = 0; i < n; i++) {
res[0][i] = 1;
}
// loop the matrix
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// state change equation
res[i][j] = res[i - 1][j] + res[i][j - 1];
}
}
return res[m - 1][n - 1];
}
```
时间复杂度是m*n, 空间复杂度也是m*n。这里可以通过滚动数组将时间复杂度优化到n。
follow up
为什么说动态规划是对递归的的一种优化,其实这个题,我们用深度优先搜索也可以做。
示例代码如下:
```
public int uniquePaths2(int m, int n) {
if(m==0 && n==0)
return 0;
return dfs(0,0,m,n);
}
public int dfs(int i,int j,int m,int n)
{
if(i==m-1 && j==n-1)
return 1;
if(i>=m || j>=n)
return 0;
int sum=0;
sum+=dfs(i,j+1,m,n)+dfs(i+1,j,m,n);
return sum;
}
```
但是我们会发现搜索是超过时间限制的,为什么因为搜索的时间复杂度是2^(m+n)。
因为搜索的数据结构就是二叉树,好比我们要想知道4*4矩阵的结果,就先把4*4拆分成对应的(3,4)与(4,3)相加,再将(3,4)拆分成(2,4)和(3,3)以此类推不停地拆分直到(0,0)的初始位置,然后我们发现这个过程中出现了很多重复的问题,并且每一个点都可以拆分成两个点。所以当m,n越大,花费时间也越多。
总结
动态规划和递归一直以来都是我算法的弱项,和平铺直叙的线性思维不同,递归和动态规划是通过对问题的拆分,很多时候是逆向思维。我发现此类问题都是代码量很小,但是需要想的很周全。之后,我会多通过练习来加深自己对其的认识。