前言
动态规划(Dynamic Programming,DP),本文不做动态规划的定义介绍,直接通过代码来理解其思想。
本文是笔者在学习动态规划时的一个总结,题目来自于一位知乎大神,属于一道看完非常容易理解的题目,话不多说直接上题目
题目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )机器人每次只能向下或者向右移动一步机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
如下图所示,下图图是一个7 x 3 的网格。有多少可能的路径?
对于这种题目,这里我们的思路首先是总结规律,由上首先可得规律
假定start的坐标为(0,0),finsh的坐标为(i,j)
那么首先找到规律:
- f(i,j)=f(i-1,j)+f(i,j-1),即从(0,0)到一个坐标的可能的路径由到达该坐标的上坐标和左坐标的和
- 需要注意坐标i和j不能小于0
- 当i或者j=0时可能性必定为1,因为只能一直往一个方向走(除了(0,0)的可能性为0)
递归算法
那么首先最容易理解的解法即是递归,代码如下
private static long count(int i, int j) {
if (i == 1 && j == 0) {
return 1;
}
if (i == 0 && j == 1) {
return 1;
}
long k1 = 0;
// 当i==0时,即没有左坐标了,可能性为0
if (i > 0) {
k1 = count(i - 1, j);
}
long k2 = 0;
// 当j==0时,即没有上坐标了,可能性为0
if (j > 0) {
k2 = count(i, j - 1);
}
return k1 + k2;
}
这种递归思路是比较容易想到的,但是其存在比较严重的问题
1、存在大量重复的计算
如:
计算f(5,5)时需要计算f(4,5)和f(5,4)
计算f(4,5)时需要计算f(3,5)和f(4,4)
计算f(5,4)时需要计算f(4,4)和f(5,3)
如上f(4,4)计算了两遍,当i和j的指数增长时这种重复的计算极为明显
2、递归调用在java中会占用大量的栈深度,当深度太大时就会导致栈溢出
如上递归调用当i和j等于20时,笔者执行时等待了一分钟仍然没有结果返回,可见重复计算的非常之多,即每一次f(i,j)都会执行非常多次递归,直到执行到f(1,0)或者f(0,1)为止
动态规划
基于此可以引出动态规划的思想,根据上面递归调用存在的问题,我们需要解决的问题是
1、使用循环代替递归,避免产生大量的栈
2、缓存计算值,避免重复计算
代码如下:
/**
* 这里的规律:
* 1、从坐标0,0到达i,j时的可能的情况等于坐标i-1,j加上i,j-1的情况
* 2、当坐标的i和j其中一个值为0时,情况数为1,因为只能往下或者往左,
* 当i或者j为0则只有往一个方向一直走
*/
private static long count2(int i, int j) {
long[][] array = new long[i + 1][j + 1];
array[0][0] = 0;
array[0][1] = 1;
array[1][0] = 1;
for (int k1 = 1; k1 <= i; k1++) {
for (int k2 = 1; k2 <= j; k2++) {
// k1-1==0 或者k2-1==0时值为1,因为只能往下或者往左
long value1 = (k1-1 == 0) ? 1 : array[k1 - 1][k2];
long value2 = (k2-1 == 0) ? 1 : array[k1][k2 - 1];
long value = value1 + value2;
array[k1][k2] = value;
}
}
// 循环结束,即所有的格子上都已经填充完值了,并且填充过程中只是简单的把缓存中的值取出来
// 做了加法,不存在额外的计算
return array[i][j];
}
上述代码并不难理解,即使用二维数组缓存计算的值,从坐标(0,0)依次往外扩散,计算每一个坐标的值,因为规律是每个坐标的值等于左坐标加上坐标的值,这两个值都是可以在二维数组中直接拿到的,不存在额外的计算,所以效率非常之高
上述代码笔者执行i,j等于1000时只需要592毫秒,可见效率之高
总结
笔者的理解动态规律最重要的思想就是
- 把大问题转换为小问题,如首先找到规律,如上大问题f(i,j)可以拆分为f(i-1,j)+f(i,j-1),以此类推,而最小的问题是f(0,1),f(1,0),这两个问题是已经有解的
- 基于最小问题向外扩散,从最小的问题入手,即如上从位置f(0,0)向外扩散
- 扩散的过程中把值进行缓存,缓存到数组中,这样的目的可以导致扩散途中需要计算的值全部是可以在缓存中拿到的,避免了不必要的计算