动态规划在编程中有着普遍的应用,对于某些问题咱们能够经过动态规划显著的下降程序的时间复杂度。本质上动态规划并非一种算法,而是解决一类问题的思想。本篇博客经过一些很是简单而又经典的问题(好比数塔、0-1背包、彻底背包、走楼梯问题、最长公共子序列等)来帮助你们理解动态规划的通常套路。html
欢迎探讨,若有错误敬请指正 java
如需转载,请注明出处 http://www.cnblogs.com/nullzx/算法
若是咱们解决一个问题的时候能将一个大问题转换成一个或者若干个规模较小的同等性质的问题,当咱们求解出这些小问题的答案后,大问题的答案很容易解决,对于这样的状况,显然咱们能够递归(或者说分治)的方式解决问题。若是在求解这些小问题的过程当中发现有些小问题咱们须要重复计算屡次,那么咱们就干脆把已经求解过的小问题的答案记录下来放在一张表中,这样下次遇到这个小问题,咱们只须要查表就能够直接获得结果,这个就是动态规划的白话讲解。动态规划的难点在于如何定义问题及子问题。编程
1)若是能够将一个规模较大的问题转换成一个或若干个规模较小的子问题,也就是能找到递推关系,这个时候咱们不妨先将程序写成递归的形式。数组
2)若是使用递归求解规模较小的问题上存在子问题重复求解的现象,那么咱们就创建一张表(有可能这个表只有一行)记录须要重复求解的子问题。填表的过程和将大问题划分为子问题的方式相反,咱们会从最简单的子问题开始填表。如今咱们就利用这个套路解决下面这些经典的问题。app
问题描述:菲波那契数列的定义f(n) = f(n-1) + f(n-2), 且f(1)=1, f(2) = 1,求f(n)的值。斐波那契数列的定义自己就是将大问题转换成两个同性质的子问题,因此咱们能够直接根据定义写成递归形式。iphone
public static int recursion(int n) { if (n < 0) { return 0; } if (n == 1 || n == 2) { return 1; } return recursion(n-1) + recursion(n-2); }
咱们以f(6)为例如今把递归的过程画出来优化
咱们发如今求解F(6)时,须要求解F(2)四次,求解F(1)三次,求解F(3)三次,F(4)两次,因此说咱们的算法的效率是很低的。提升效率的办法就是将F(1),F(2),F(3) ….的结果放在表中,下次要计算这些问题的时候咱们直接从表中获取就行了,这就是一个最简单的动态规划的例子。如今咱们按照套路,从最小的子问开始填表就行了。ui
public static int dynamic(int n) { int[] table = new int[n+1]; table[1] = 1; table[2] = 1; /*从小到大填表*/ for (int i = 3; i < table.length; i++) { table[i] = table[i-1] + table[i-2]; } return table[n]; }
须要说明的是,这个例子只是一个入门的例子,实际上它不存在最优子结构的问题,并且也不须要长度为n+1的table数组,只须要两个变量便可(能够理解为动态规划的优化版本),而咱们之因此这样讲解只是为了让你们从动态规划的角度去理解问题。this
问题描述:总共有n个楼梯,每次能够走2个台阶,或者每次走3个台阶,或者每次走5个台阶,请问这n个楼梯总共有几种走法。
n个阶梯的问题,能够分解成三个小问题,即n-2个阶梯有几种走法,n-3个阶梯有几种走法,n-5个阶梯有几种走法,而n个阶梯的走法就是这三种走法的和。或者能够反过来思考,你已经站在最后一个台阶上了,那么到达最后一个台阶的状况只能是三种状况,最后一步刚好走2个台阶刚好到达,最后一步刚好走3个台阶刚好到达,最后一步刚好走5个台阶刚好到达。经过这个思想,咱们就能够写出递归形式的代码。
public static int recursion(int n) { if (n < 0) { return 0; } if (n == 0) { return 1; } return recursion(n - 5) + recursion(n - 3) + recursion(n - 2); }
显然上面递归的处理方式须要重复计算不少子问题,画出递归调用的图就一目了然,因为该图和上一个问题的图很相似,这里就省略了。所以就建立一张表,把子问题的结果都记录下来,dp[i]表示走到第i个阶梯有多少种走法。按照套路,咱们应该从小的阶梯数开始填表。
public static int dynamic(int n) { int[] record = new int[n+1]; record[0] = 1; for (int i = 0; i < record.length; i++) { int n2 = i - 2 >= 0 ? record[i-2] : 0; int n3 = i - 3 >= 0 ? record[i-3] : 0; int n5 = i - 5 >= 0 ? record[i-5] : 0; record[i] = n2 + n3 + n5; } return record[n]; }
一样,这里例子中也不存在最优问题。
问题描述:从顶部出发在每个节点能够选择向下或者向右下走,一直走到底层,要求找出一条路径,使得路径上的数字之和最大。
对于上图所示的数塔:最大值为379, 绿色的的数字就是被选择的节点。
这个问题不能使用贪心算法,请你们本身用三层的阶梯列举出反例。咱们如今试着将这个问题分解成子问题,以下图所示。想求得最大值,咱们只要选择的红色边框数塔最大值和蓝色边框数塔的最大值中更大的那个,而后加上32,就整个数塔的最大值。这样咱们就将一个大的问题转化成了两个规小的问题,而后这两个规模较小的问题还能够继续分解成更小的子问题。根据这个思路,咱们能够获得以下递归形式的代码。
/*咱们用一个二维数组的左下半数组表示数塔*/ public static int recursion(int[][] a){ return recursion(a, 0, 0); } /*参数i表示所在的行,j表示所在的列*/ private static int recursion(int[][] a, int i, int j){ /* * 当分解问题到最下一层时, * (a.length - 1, j)位置为顶点的数塔实际上数塔只有一个元素, * 直接返回 */ if (i == a.length - 1){ return a[i][j]; } /*求(i+1, j)位置为顶点的数塔最大值*/ int r1 = recursion(a, i+1, j); /*求(i+1, j+1)位置为顶点的数塔最大值*/ int r2 = recursion(a, i+1, j+1); /*返回(i,j)为顶点位置的数塔的最大值*/ return Math.max(r1, r2) + a[i][j]; }
上述代码可以获得正确的结果,可是咱们发现计算大一点的数塔计算会很费时间,这主要是重复计算的问题,咱们如今来分析一下为何会出现重复计算的问题。
上图中的紫色边框数塔既存在于红色边框数塔中,也存在于蓝色边框数塔中,会重复计算两次。实际上,咱们使用递归时重复计算的问题显然不止这一个,因此效率不高。为此咱们应该建立一张和数塔形状同样的三角形表用来记录更小的数塔的最大值。咱们table表示这个表,表中table[i][j]位置的值表示以(i,j)为顶点的数塔的最大值。咱们用a[i][j]表示数塔中第i行,第j列的值。那么table[i][j] = a[i][j] + Math.max(table[i-1][j], table[i-1][j-1])。按照套路,咱们应该从最小的数塔开始填表。按照table[i][j]的定义,table表的最下面一行就应该等于数塔表中的最下面一行。
按照定义,咱们就能够填倒数第二行的dp[i][j]。
table[4][0] = 79 + Math.max(0, 71) = 150 table[4][1] = 69 + Math.max(71, 51) = 140 table[4][2] = 78 + Math.max(51, 82) = 160 table[4][3] = 29 + Math.max(82, 91) = 120 table[4][4] = 63 + Math.max(91, 64) = 154
填入到table表的倒数第二行,以下图所示
有了倒数第二行,咱们就能够推出倒数第三行,依次类推,咱们就能够获得最上面table [0][0]的数值,它就表示了整个数塔的最大值。除了最大值,若是咱们还须要知道走了哪些路径,咱们还应该定义一个path表,在填table[i][j]时,同时填写path[i][j]。path[i][j]表示了以(i, j)为顶点的数塔的最大值是由两个子数塔(table[i-1][j]为顶点的数塔和table[i-1][j+1]为顶点的数塔)中的哪个获得的。
public class NumbericalTower { /*最大值对应的各个顶点位置*/ private LinkedList<Map.Entry<Integer, Integer>> pathList; /*存储整个数塔的最大值*/ private int result; public NumbericalTower(int[][] a) { pathList = new LinkedList<Map.Entry<Integer, Integer>>(); dynamic(a); } private void dynamic(int[][] a){ final int N = a.length; /*path[i][j] 表示(i+1, j)为顶点的数塔和(i+1,j+1)为顶点的数塔 *中较大的那个*/ int[][] path = new int[N][N]; /*动态规划对应的表*/ int[][] table = new int[N][N]; /*从最小的数塔开始填表*/ for (int i = N - 1; i >= 0; i--) { /*根据下层数塔的最大值计算上层的数塔的最大值*/ for (int j = 0; j <= i; j++) { if (i == N - 1) { table[i][j] = a[i][j]; path[i][j] = -1; }else if (table[i+1][j] > table[i+1][j+1]) { table[i][j] = table[i+1][j] + a[i][j]; path[i][j] = j; }else{ table[i][j] = table[i+1][j+1] + a[i][j]; path[i][j] = j+1; } } } result = table[0][0]; /*记录最大值对应的顶点*/ int i = 0, j = 0; pathList.add(new SimpleEntry<Integer, Integer>(0, 0)); while (true) { j = path[i][j]; i = i + 1; pathList.add(new SimpleEntry<Integer, Integer>(i, j)); if (path[i][j] == -1) { break; } } } int max(){ return result; } List<Map.Entry<Integer, Integer>> path(){ return pathList; } public static void main(String[] args) { int[][] a = { {32}, {83, 68}, {40, 37, 47}, { 5, 4, 67, 22}, {79, 69, 78, 29, 63}, { 0, 71, 51, 82, 91, 64} }; NumbericalTower nt = new NumbericalTower(a); int max = nt.max(); List<Map.Entry<Integer, Integer>> path = nt.path(); System.out.println("最大值:" + max); System.out.println("\n\n路径为:"); for (Map.Entry<Integer, Integer> entry : path) { int r = entry.getKey(); int c = entry.getValue(); System.out.println("行 : " + r + ", 列:"+ c); } } }
运行结果
最大值:379 路径为: 行 : 0, 列:0 行 : 1, 列:0 行 : 2, 列:1 行 : 3, 列:2 行 : 4, 列:2 行 : 5, 列:3
问题描述:有n 个物品,它们有各自的重量(weight)和价值(value),现有给定容量的背包,如何让背包里装入的物品具备最大的价值总和此时背包中的物品?一个物品只有不拿和拿两种状况,能够用0和1表示,因此称之为0-1背包问题。
咱们来看一个具体的例子。假设有以下物品:
求背包容量在10的时候的能装物品的最大价值,以及装了哪些物品?
咱们可能首先想到的是贪心算法,咱们算出每种物品的单位重量价值(weight/value),而后按照单位重量价值排序。咱们放入物品时首先选择单位重量价值高的物品,直到放不下为止。可是很遗憾,这样得不到最优解。咱们不妨列举一个极端的例子,假设只有两个物品,A的value = 2.9, weight = 2.1;B的value = 3, weight = 3,显然物品A的单位重量价值要大于B的单位重量价值,但对于容量为3的背包,咱们应该选择物品B,因此贪心算法失效。对于0-1背包问题,贪心选择之因此不能获得最优解是由于:它没法保证最终能将背包装满,而部分闲置的背包空间使每公斤背包空间的价值下降了。
回到上面具体的这个问题,它能够表述为
maxValue{宝石、剃须刀、ipad、充电宝、iphone | 背包容量10},
每一个物品只有选和不选两种结果,咱们不妨从第一个物品开始。若是选了宝石,那么问题转化为当前背包已有价值为50,并在剩下的背包容量(10 - 4)的前提下,再剩下的物品中(即剃须刀、ipad、充电宝、iphone)选取出最大的价值;若是不选宝石,那么问题转化为当前背包价值为0,并在剩下的背包容量10的前提下,在剩下的物品中(即剃须刀、ipad、充电宝、iphone)选取出最大的价值。咱们只须要选择:
50 + maxValue{剃须刀、ipad、充电宝、iphone | 背包容量6}
与
0 + maxValue{剃须刀、ipad、充电宝、iphone | 背包容量10}
中较大的那个。而这就直接转化成两个子问题的求解,显然咱们已经能够用分治的方式解决这个问题了。咱们不妨把递归树(或者说分治树)画出来。
上图就是0-1背包问题的递归树,图左文字边表示当前可选的物品,节点中的值表示背包的容量。咱们没有把整个递归树所有都画出来,由于图中咱们就已经发现了须要重复计算的子问题。若是背包容量变大,物品种类变多,那么须要重复计算的子问题就越多。须要说明的是上图中有三个背包容量为7的子问题,可是只有被标记的两个子问题才是重复的子问题,由于这两个子问题的背包容量同样,可选物品同样。为了不子问题的重复求解,咱们就创建一张动态规划表,下次遇到重复的子问题,咱们就直接查表。下图表示了动态规划表和递归树之间的关系。
那咱们如今的主要问题就变成了如何填这样一张表。咱们用一个名为dp的二维数组表示这张表,dp[0]行须要单独初始化,从dp[1]行开始填表,规则:从左到右,从上到下。
dp[i][j]表示前i个物品(包括物品i),在背包容量为j时能装的最大价值。
dp[i][j]为下面二者的最大值:
1)物品i不放入背包中:背包容量为j时,前i-1个物品组合出的最大价值
2)物品i放入背包中:物品i的价值 + 除去物品i占用的重量后,剩余背包容量j-weight(i)由前i-1个物品组合出的最大价值
用公式表示为
1)若是dp[i][j] > dp[i-1][j],说明该物品i被放入到了背包中,令i = i – 1, j = j – weight[i],而后重复步骤1。
2)若是dp[i][j] == dp[i-1][j],且只想获得背包最大价值的其中一种的物品一种组合方式,不妨认为该物品i没有被放入到了背包中,令i = i – 1, 重复步骤1)。
对于步骤2),若是
dp[i][j] == dp[i-1][j] && dp[i][j – weight(i)] + value(i) == dp[i][j]
说明物品i能够放入背包中(令i = i – 1, j = j – weight[i]),也能够不用放入背包中(令i = i - 1)。这里就产生分支,说明放入背包中的物品组合方式不惟一,为了简单起见,咱们找到一种物品的组合方式便可。
package demo; import java.util.LinkedList; import java.util.List; public class KnapsacProblem { /*动态规划表*/ private int[][] dp; /*背包装的最大价值*/ private int maxVal; /*背包最大价值时对应的商品编号*/ private List<Integer> goodsNumber; public KnapsacProblem(int[] weight, int[] values, int capacity){ if ( weight.length != values.length ){ throw new IllegalArgumentException(); } int goodsLen = weight.length; /*第0列不使用*/ this.dp = new int[goodsLen][capacity + 1]; goodsNumber = new LinkedList<Integer>(); /*单独初始化第0行*/ for ( int j = 1; j < capacity + 1; j++){ if (j >= weight[0]){ dp[0][j] = values[0]; } } /*填dp表*/ for ( int i = 1; i < goodsLen; i++ ) { for ( int j = 1; j < capacity + 1; j++ ) { if ( weight[i] <= j ) { dp[i][j] = Math.max(dp[i-1][j], values[i] + dp[i-1][j - weight[i]]); } else { dp[i][j] = dp[i-1][j]; } } } maxVal = dp[goodsLen - 1][capacity - 1]; /*找出使用了哪些物品*/ int j = capacity; for (int i = goodsLen - 1; i > 0; i-- ) { if ( dp[i][j] > dp[i-1][j] ) { goodsNumber.add(i); j = j - weight[i]; } } /*单独处理第0行,回退到第0行时发现背包中还有物品,说明物品0在背包中*/ if (j > 0){ goodsNumber.add(0); } } public int getPackageMaxValue(){ return this.maxVal; } public List<Integer> getGoodsNumber(){ return this.goodsNumber; } public static void main(String[] args){ int[] weight = {4, 5, 2, 1, 2}; int[] values = {50, 40, 60, 20, 30}; int capacity = 10; KnapsacProblem kp = new KnapsacProblem(weight, values, capacity); System.out.println(kp.getPackageMaxValue()); System.out.println(kp.getGoodsNumber()); } }
运行结果
160 [4, 3, 2, 0]
若是咱们仅仅须要知道最大的价值,不须要知道装了哪些物品,咱们就能够对空间复杂度进行优化,动态规划表只须要一维,由于dp[i][?]仅和dp[i-1][?]有关。
Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.
Note:
1. Each of the array element will not exceed 100.
2. The array size will not exceed 200.
Example 1:
Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].
Example 2:
Input: [1, 2, 3, 5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.
这是LeetCode的原题。这个问题本质上仍是0-1背包问题,背包容量是数组之和的一半,物品的价值和体积是1比1的关系,额外条件是须要把背包装满。
问题描述:有n 种物品,它们有各自的重量(weight)和价值(value),现有给定容量的背包,每种物品能够拿任意多个,如何让背包里装入的物品具备最大的价值,以及每种物品装了几个?
假设,咱们仍是利用0-1背包中的物品,背包容量为11。
彻底背包问题也能够转化成0-1背包问题。由于第i个物品最多拿“背包重量/(物品i的重量)”个,也就是说在0-1背包问题中每一个物品i占一行,彻底背包问题中,每一个物品占“背包重量/(物品i的重量)” 个行,按照这个思路显然已经可以解决这个问题。如今咱们不把这个问题转化为0-1背包问题,而从这个问题的根源直接思考。
彻底背包问题能够表述为
maxValue{宝石、剃须刀、ipad、充电宝、iphone | 背包容量10}
每一个物品只有选和不选两种结果,咱们不妨从第一个物品开始。若是选了宝石,那么问题转化为当前背包已有价值为50,并在剩下的背包容量(10 - 4)的前提下,继续在{宝石、剃须刀、ipad、充电宝、iphone}选取出最大的价值;若是不选宝石,那么咱们就在{剃须刀、ipad、充电宝、iphone}中选择一种,那么问题转化为当前背包价值为0,并在剩下的背包容量10的前提下,再剩下的物品中即{剃须刀、ipad、充电宝、iphone }选取出最大的价值。
所以咱们只须要选择:
50 + maxValue{宝石、剃须刀、ipad、充电宝、iphone | 背包容量6}
与
0 + maxValue{剃须刀、ipad、充电宝、iphone | 背包容量10}
中较大的那个。
而这就直接转化成两个子问题的求解,显然咱们已经能够用分治的方式解决这个问题了。咱们一样能够把递归树画出来,一样还会发现存在须要重复求解的子问题,为了不子问题的重复求解,咱们仍是创建一张动态规划表,下次遇到重复的子问题,咱们就直接查表。这里咱们直接给出动态规划表,咱们用一个名为dp的二维数组表示这张表,dp[0]行单独初始化,从dp[1]行开始填表,规则:从左到右,从上到下。
dp[i][j]表示前i个物品(包括物品i),在背包容量为j时能装的最大价值。
dp[i][j]为下面两者的最大值:
一样,从dp表中咱们还能够知道哪些物品被选择了,选择多少次。咱们仍是从右下角开始回溯。
1)dp[i][j] > dp[i-1][j] 说明i号物品被选择了,j = j – weight[i]
2)dp[i][j] == dp[i-1][j] 为了简单起见,咱们认为i号物品没有被选择,令i = i -1(实际上这里一样可能存在分支,即最大价值时物品的组合方式和数量并不惟一,咱们这里为了简单处理,就不考虑这个问题了)。
package demo; import java.util.AbstractMap.SimpleEntry; import java.util.LinkedList; public class AllKnapsacProblem { private int maxVal; private LinkedList<SimpleEntry<Integer, Integer>> goodsIdCount; public int getPackageMaxValue(){ return maxVal; } public LinkedList<SimpleEntry<Integer, Integer>> getGoodsCount(){ return goodsIdCount; } public AllKnapsacProblem(int[] weight, int[] values, int capacity){ /*处理最大价值问题============================================*/ if ( weight.length != values.length ){ throw new IllegalArgumentException(); } int goodsLen = weight.length; /*第0列不使用*/ int[][] dp = new int[goodsLen][capacity + 1]; /*第0行单独处理*/ for (int j = weight[0]; j <= capacity; j++){ dp[0][j] = dp[0][j - weight[0]] + values[0]; } for (int i = 1; i < goodsLen; i++){ for (int j = 1; j <= capacity; j++){ int max1 = dp[i-1][j]; int max2 = j - weight[i] >= 0 ? values[i] + dp[i][j - weight[i]] : 0; dp[i][j] = Math.max(max1, max2); } } maxVal = dp[goodsLen-1][capacity]; /*处理物品种类和个数问题问题============================================*/ /*SimpleEntry<Integer, Integer>:key表示物品编号,value表示物品个数*/ goodsIdCount = new LinkedList<SimpleEntry<Integer, Integer>>(); int i = goodsLen - 1; int j = capacity; SimpleEntry<Integer, Integer> entry = new SimpleEntry<Integer, Integer>(i, 0); while (i > 0){ if (dp[i][j] > dp[i-1][j]){ int n = entry.getValue(); entry.setValue(n+1); j = j - weight[i]; } if (dp[i][j] == dp[i-1][j]){ if (entry.getValue() > 0) { goodsIdCount.add(entry); } i--; entry = new SimpleEntry<Integer, Integer>(i, 0); } } /*单独处理第0行*/ if (j > 0) { goodsIdCount.add(new SimpleEntry<Integer, Integer>(0, j/weight[0])); } } public static void main(String[] args){ int[] values = {50, 40, 60, 20, 30}; int[] weight = {4, 5, 2, 1, 2}; int capacity = 11; AllKnapsacProblem ap = new AllKnapsacProblem(weight, values, capacity); System.out.println("背包价值" + ap.getPackageMaxValue()); for (SimpleEntry<Integer, Integer> entry : ap.goodsIdCount) { System.out.printf("物品%d : %d个\n", entry.getKey(), entry.getValue()); } } }
320 物品3 : 1个 物品2 : 5个
You are given coins of different denominations ([dɪˌnɑ:mɪˈneɪʃn] 面额) and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1。
Example 1:
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
Example 2:
Input: coins = [2], amount = 3
Output: -1
这道题目是LeetCode上面的原题。假设在一堆面值为 1,2,5,11面值的硬币,问最少须要多少个硬币才能找出总值为以兑换15元。面对这个问题咱们也会首先想到贪心算法,可是贪心算法给出的组合方案为{11,1,1,1,1},但其实最优方案为{5,5,5}。若是使用枚举算法,每种硬币都有选0个,选1个,选2个,选…,这样时间复杂度过高。这个问题本质上仍是彻底背包问题,物品的价值和重量比是1比1,额外条件是须要把背包装满,因此咱们可使用动态规划算法去解决它,代码这里就不给出了。
咱们首先看一会儿序列的定义。假设给定一个字符串,咱们抽取任意多个不超过字符串长度的字符,并保持它们的先后关系,这样的字符咱们称之为子序列。对于字符串ABCDEFG而言, BEF、C、AG等等都是它的一个子序列。
Longest common sequence问题:给定两个字符串s1和s2,求这两个字符串中最长的公共子序列。好比给定两个字符串s1:bdcaba和s2:abcbdab,它们的公共子序列
长度为4,最长公共子序列是:bcba。
字符串s1的长度用n表示,字符产s2的长度用m表示,字符串s1和s2的最长公共字串用lcs(n,m)。那么这个问题能够转化为三个子问题
1)求lcs(n-1, m-1)
2)求lcs(n-1, m)
3)求lcs(n, m-1)
当咱们求的上述三个子问题的答案,那么lcs(n, m)的结果就能够经过以下方式获得:
若是s1[n] == s2[m]
lcs(n, m) = lcs(n-1, m-1)+1
若是s1[n] != s2[m] :
lcs(n, m) = max{ lcs(n-1, m-1), lcs(n-1, m), lcs(n, m-1) }
可是实际上lcs(n,m)只要转化成两个子问题lcs(n-1, m)和lcs(n, m-1)就行了。
而子问题lcs(n-1, m-1)是没有必要的,由于lcs(n-1, m-1)一定小于等于lcs(n-1, m)和lcs(n, m-1)中的en任意一个。从常理上来讲很好理解,不可能两个字符串中的任意一个变长了,公共子序列反而减小了。而本质上是因为lcs(n-1, m-1)也是lcs(n-1, m)和lcs(n, m-1)这两个问题的子问题。
经过上面的分析,咱们把大的问题转化成小的问题,就能够经过递归(或者说分治)的方式把问题解决了,下面就是递归对应的代码。
public static void recursion (char[] s1, char[] s2) { maxLen = recursion0 (s1, s1.length-1, s2, s2.length-1); } private static int recursion0 (char[] s1, int idx1, char[] s2, int idx2){ if(idx1 < 0 || idx2 < 0){ return 0; } int max1, max2; max1 = recursion0 (s1, idx1, s2, idx2 - 1); max2 = recursion0 (s1, idx1 - 1, s2, idx2); if (s1[idx1] == s2[idx2]){ return Math.max(max1, max2) + 1; }else{ return Math.max(max1, max2); } }
显然上述也一样存在不少重复计算的子问题,为了下降时间复杂度,要一张二维表记录重复计算的子问题的结果,这张表咱们用dp表示, dp[i][j]就表示以s1[i]和s2[j]结尾的字符串最长公共子序列。按照套路填表规则要从最小的子问题开始,
第0行,表示“b”和“bdcaba”的公共子序列,能够单独处理,同理第0列也能够单独处理,填表完成后如上图所示。从第二行开始,dp表按照从上到下,从左到右的填表顺序填表。根据子递归中子问题的定义,dp[i][j]的取值以下:
当填完整张表时,右下角的值就是公共子序列的最大长度。若是咱们还须要知道公共子序列是什么,那么咱们能够从右下角开始回溯,若是dp[i][j] > dp[i-1][j] 且 dp[i][j] > dp[i][j-1], 说明s1[i]或者s2[j]是公共子序列,不然选择走dp[i-1][j]和dp[i][j-1]中较大的那个,一样第0行要单独处理。
package demo; public class LongestCommonSequence { private int[][] dp; private int maxLen; private String lcs; private char[] s1, s2; public int maxLen(){ return maxLen; } public String getLCS() { return lcs; } public LongestCommonSequence(String str1, String str2) { s1 = str1.toCharArray(); s2 = str2.toCharArray(); dynamic(); getString(); } /*动态规划算法*/ private void dynamic(){ dp = new int[s1.length][s2.length]; /*单独处理第0行*/ for(int j = 0, x = 0; j < s2.length; j++){ if (s1[0] == s2[j]){ x = 1; } dp[0][j] = x; } /*单独处理第0列*/ for (int i = 0, x = 0; i < s1.length; i++) { if (s2[0] == s1[i]){ x = 1; } dp[i][0] = x; } for (int i = 1; i < s1.length; i++) { for(int j = 1; j < s2.length; j++){ if(s1[i] == s2[j]){ dp[i][j] = 1 + dp[i-1][j-1]; }else{ dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); } } } maxLen = dp[s1.length - 1][s2.length - 1]; } /*回溯求出公共子序列*/ private void getString(){ int cnt = maxLen; StringBuffer sb = new StringBuffer(); int i = s1.length - 1, j = s2.length - 1; while (i > 0 && j > 0){ if (dp[i][j] > dp[i-1][j] && dp[i][j] > dp[i][j-1]){ sb.append(s1[i]); i--; j--; cnt--; }else{ if (dp[i-1][j] > dp[i][j-1]){ i--; }else{ j--; } } } /*单独处理第0行, i和j必然有一个为0*/ if (cnt > 0){ while (true){ if (s1[i] == s2[j]){ sb.append(s1[i]); break; } if (i > 0){ i--; } if (j > 0){ j--; } } cnt--; } lcs = sb.reverse().toString(); } public static void main(String[] args){ LongestCommonSequence lcs = new LongestCommonSequence("bcba", "bdcaba"); System.out.println(lcs.maxLen); System.out.println(lcs.getLCS()); } }
枚举算法:若是为了方便的解决这个问题,咱们须要将大问题化简成小问题,将全部小问题中的最优解做为咱们解决大问题的基础。
贪心算法:若是为了方便的解决这个问题,咱们须要将大问题化简成小问题,在全部小问题中,仅选择对当前最有利的小问题做为咱们解决大问题的基础。
动态规划:若是为了方便的解决这个问题,咱们须要将大问题化简成小问题,记录已解决过的小问题,将全部小问题中的最优解做为咱们解决大问题的基础。换句话说,能用贪心算法解决的,动态规划算法也确定能解决,反之不成立。
能用动规解决的问题的特色
1) 问题具备最优子结构性质。若是问题的最优解所包含的子问题的解也是最优的,咱们就称该问题具备最优子结构性质。
2) 无后效性。当前的若干个状态值一旦肯定,则此后过程的演变就只和这若干个状态的值有关和以前是采起哪一种手段或通过哪条路径演变到当前的这若干个状态,没有关系。
[1]. 动态规划:最长上升子序列(LIS)
[2]. 什么是动态规划?动态规划的意义是什么?
[3]. 漫画:什么是动态规划?