1 什么是Dynamic programming:
我觉得WIKI百科对动态规划解释的简单而又清晰:
动态规划(英語:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法(暴力算法)。
简单总结下就是:通过子最优解解获取整体最优解的算法。
2 状态转移方程
百度百科对状态转移方程的解释很清晰:
动态规划中本阶段的状态往往是上一阶段状态和上一阶段决策的结果。若给定了第K阶段的状态Sk以及决策uk(Sk),则第K+1阶段的状态Sk+1也就完全确定。也就是说Sk+1与Sk,uk之间存在一种明确的数量对应关系,记为Tk(Sk,uk),即有Sk+1= Tk(Sk,uk)。 这种用函数表示前后阶段关系的方程,称为状态转移方程 。 在上例中状态转移方程为 Sk+1= uk(Sk) 。
3 DP和递归
你可能会有疑问,递归也是通过递归的方式,拆解一个大问题为小问题,最终求解的算法,那他和动态规划有何异同:
我们先举个简单的跳台阶的例子比较DP和递归,看一下动态规划的精髓:
3.1 一个台阶总共有n级,如果一次可以跳1级,也可以跳2级。求总共有多少总跳法。
递归:
我们列出台阶和跳法之间的关系表:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 5 | 8 | f(6-1)+f(6-2) | f(n-1)+f(n-2) |
我们可以很清晰的求出递归的解法:
public static int solution(int n){
if (n==1){
return 1;
}
if (n==2){
return 2;
}
return solution(n-1)+solution(n-2);
}
动态规划:
****我们从动态规划的角度求解这个问题,假如我们现在台阶为n级,我们处在第一级,那么我们接下来可以1 跳一步,或则 2 跳两步。那么跳一步后就是n-1个台阶有几种跳法的问题了,调两步后也是n-2个台阶有几种跳法的问题,我们可以很快想出状态转移方程:f(n)=f(n-1)+f(n-2):
那么解法也就跃然纸上了:
public static int solution(int n) {
int[] solution = new int[n + 1];
for (int i = 1; i < n + 1; i++) {
if (i == 1) {
solution[i] = 1;
}
if (i == 2) {
solution[i] = 2;
}
// 不断的从子最优解中求当前最优解
solution[i] = solution[i - 1] + solution[i - 2];
}
return solution[n];
}
根据上述两个算法可以看到,动态规划的核心是找到状态转移方程,然后借助数组记录子最优解,不断的通过求子最优解,最终计算得出整体大问题的最优解。由以上两个算法也可以看出,动态规划和递归思想上是相同的--分解问题,但是手段确实相反的,动态规划是自底向上,不断从子问题求解;递归是自上向下,不断分解问题求最终解。
3.2 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “S” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “E” )。问总共有多少条不同的路径?
| S | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
|---|---|---|---|---|---|---|---|
| 1 | (X,Y-1) | ||||||
| 1 | (X-1,Y) | E(X,Y) |
我们分析一下,机器人到达E(end)终点的前一步,要么是1 (X-1,Y),要么是2 (X,Y-1),(因为规定机器人只能向下或则向右移动)那么我们只需要计算出F((X-1,Y))和F((X,Y-1))有多种路径,便可以求出机器人到达E(X,Y),有多少种路径。所以状态转移方程为:F(X,Y)=F((X-1,Y)) +F((X,Y-1))。我们额外看一种特殊情况,就是边界情况,上表绿色部分,因为机器人只能向下或则向右运动,那么只要处在边界,就只有一种运动路径,一直向右或则一直向下。
那么解法也就跃然纸上了:
public static int solution(int x, int y) {
int[][] solution = new int[x][y];
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
// 当前节点在边际,则必然只有一种跳法
if (i == 0 || j == 0) {
solution[i][j] = 1;
}
// 当前的前一步,机器人要么是从[i][j-1]跳过来,要么是从[i-1][j]跳过来
solution[i][j] =solution[i][j-1] +solution[i-1][j];
}
}
return solution(x-1,y-1);
}
3.3 矩阵中的最短路径:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。
| 1 | 2 | 1 | 7 | 2 |
|---|---|---|---|---|
| 1 | 5 | 7 | 8 | 2 |
| 3 | 1 | 8 | 4 | 3 |
| 5 | 6 | 7 | 0 | 3(x,y) |
这个和4.2十分相似,我们要到达终点(x,y),但是只能向下或则向右移动,那么我们到达终点前一步,要么途径(x,y-1)要么途径(x-1,y)。我们假定矩阵某个位置(x.y)的长度为val(x,y),那么其实从起点到达终点(x,y)的最短路径的转移方程就是F(x,y)=min(F(x,y-1),F(x-1,y))+val(x,y),解释一下就是如果到达终点前往右走和往下走的最短路径加上当前节点的长度,以此类推,我们可以求出整个矩阵每个节点的最小路径。
有了状态转移方程,那么解法也就跃然纸上了:
public int solution(int[][] val) {
int x = val.length;
int y = val[0].length;
int[][] solution = new int[x][y];
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
// 起点
if (i == 0 && j == 0) {
solution[i][j] = val[i][j];
}
// 左边缘
if (i == 0) {
solution[i][j] = solution[i][j - 1] + val[i][j];
}
// 右边缘
if (j == 0) {
solution[i][j] = solution[i - 1][j] + val[i][j];
}
// 左边值 和 上边值 的最小值 加上当前值
solution[i][j] = Integer.max(solution[i - 1][j], solution[i][j - 1]) + val[i][j];
}
}
return solution[x - 1][y - 1];
}
3.4 最长字串问题:给定两个字符串,求出它们之间最长的相同子字符串的长度
假如我们有两个字符串str1="abcde",str2="eweqabceg",怎么计算可以得到str1和str2的最长相同子字符串?首先肯定是暴力解法,对两个数组循环后穷举求解,时间复杂度为O(n3),有没有更优解法:
我们思考一下,如果把两个字符串每个字符拆开,一个为X轴一个为Y轴,拼成一个二维数组:
| e | a | e | q | a | b | c | e | g | |
|---|---|---|---|---|---|---|---|---|---|
| a | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| b | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 0 | 0 |
| c | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 0 | 0 |
| d | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 | 0 |
| e | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
我们把X轴和Y轴字符不同的坐标设置为0,如果相同,则加上左上角坐标的值+=1,坐标上的值表示当前字符串重复的长度,我们只需求出整个相同子字符串坐标每一个值的二维数组后,找出整个二位数组的最大值即可。状态转移方程即为:如果横纵坐标相同则:f(x,y)=f(x-1,y-1),否则f(x,y)=0.
有了状态转移方程,则解法也就跃然纸上了:
public int solution(String str1, String str2) {
int x = str1.length();
int y = str2.length();
int[][] solution = new int[x][y];
int maxLength = 0;
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
// 边界场景,防止数组越界
if (i == 0 || j == 0) {
if (str1.charAt(i) == str2.charAt(j)) {
solution[i][j] = 1;
} else {
solution[i][j] = 0;
}
}
// 正常场景
if (str1.charAt(i) == str2.charAt(j)) {
solution[i][j] = 1 + solution[i - 1][j - 1];
} else {
solution[i][j] = 0;
}
maxLength = Integer.max(maxLength, solution[i][j]);
}
}
return maxLength;
}
3.5 01背包问题:
给定一组多个()物品,每种物品都有自己的重量(
)和价值(
),在限定的总重量/总容量(
)内,选择其中若干个(也即每种物品可以选0个或1个),设计选择方案使得物品的总价值最高。
这个问题同样有两个变量,正序排列的物品重量和背包总承重量。我们建立一个二维数组分析一下这个问题:
| 1(3) | 3(5) | 4(7) | 6(9) | |
|---|---|---|---|---|
| 1 | 3 | 0 | 0 | 0 |
| 2 | 3 | 3 | 3 | 3 |
| 3 | 3 | 5 | 5 | 5 |
| 4 | 3 | 5 | 7 | 7 |
| 5 | 3 | 8 | 10 | 10 |
| 6 | 3 | 8 | 10 | 10 |
| 7 | 3 | 5 | 7 | 12 |
总结一下,我们现在就是要解决的问题是:重量为X的背包,放入Y多个不同重量的商品,求能放入商品的最大价值,我们用F(X,Y)表示背包重量为X时,放第Y件商品的最大价值。那么假如当前放入商品的重量为w[i]价值为v[i],那么当前背包能放入的商品的最大值就是:放入当前商品价值的v[i]+F(x-w[i]),Y-1) 和不放入当前商品F(X,Y-1) 获得价值的最大值,那么状态转移方程就是:F(X,Y)=max(F(X,Y-1),(F(X-wi,Y-1)+vi)),这个状态转移方程,你得细细品,是不是这个理?
有了状态转移方程,处理一下边界情况,问题解法也随之跃然纸上:
public void solution(int[] w, int[] v, int c) {
int[][] solution = new int[c][w.length];
for (int i = 0; i < c; i++) {
for (int j = 0; j < w.length; j++) {
if (j == 0) {
// 边界1 只有第一件商品,没得选,最大价值就是v[0]
solution[i][j] = v[j];
}
if (i - w[j] >= 0) {
// 正常的状态转移方程求解
solution[i][j] = Integer.max(solution[i][j - 1], solution[i - w[j]][j - 1] + v[j]);
// 边界2 放完第j件商品剩余容量为0(i是重量,从0开始,所以数组下标为0的时候就是1)
} else if (i - w[j] >= -1) {
solution[i][j] = Integer.max(solution[i][j - 1], v[j]);
// 边界3 第j件商品容量大于背包容量,就不放这件商品
} else {
solution[i][j] = solution[i][j - 1];
}
}
}
return solution[c - 1][w.length - 1];
}
以上基本上覆盖了动态规划的所有常规场景,一些其它较为复杂场景大多是以上场景变种。
以上所有全为原创,可随意转载使用,无需标注出处。