动态规划算法
-
1 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
-
2 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
-
3 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
-
4 动态规划可以通过填表的方式来逐步推进,得到最优解.
动态规划比较适合用来求解最优问题,比如求最大值、最小值等等。它可以非常显著地降低时间复杂度,提高代码的执行效率。
动态规划算法和分治算法很相似, 动态规划法求解的问题,经分解后得到的子问题往往 是互相联系的即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。分治算法分解的子问题是相互独立的
使用场景
一般能用动态规划解决的问题都能用回溯法解决,动态规划是对回溯法进行精简,去除回溯法无用的回溯
使用动态规划解决问题一般需要满足三个特征:1. 最优子结构 2. 无后效性 3. 重复子问题
1. 最优子结构
最优子结构指的是,问题的最优解包含子问题的最优解。反过来说就是,我们可以通过子问题的最优解,推导出问题的最优解。如果我们把最优子结构,对应到
我们前面定义的动态规划问题模型上,那我们也可以理解为,后面阶段的状态可以通过前面阶段的状态推导出来。
2 无后效性
无后效性有两层含义:
1 在推导后面阶段的状态的时候,我们只关心前面阶段的状态值,不关心这个状态是怎么 一步一步推导出来的。
2 某阶段状态一旦确定,就不受之后阶段的决策影响。无后效性是一个非常 “ 宽松 ” 的要求。只要满足前面提到的动态规划问题模型,其实基本上都会满足无后效性。
3. 重复子问题
不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。 动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
动态规划解决背包问题
问题: 对于一组不同重量、不同价值、不可分割的物品, 将某些物品
装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?对应的物品重量 价值 及背包最大价值如下:
static int[] goodsWeight = new int[] {2 ,2,4,5,8};
static int[] goodsValues = {1000,3000,6000,7000,3000 };
static int knapsackMaxWeight = 10;
上文说了对于动态规划问题都可以使用递归回溯法解决
回溯法
使用回溯法解决我们首先需要确定递归结束条件
这个比较容易当当前背包重量> 背包的最大重量或者当前物品数量> 物品最大数量时递归结束
这时我们返回背包里的价值的最大值
if (weight == knapsackMaxWeight || currentGoodNum == goodNum ) {
return Math.max(goodValue,maxWeightValue);
}
决策背包是否能放入下一个weight重量的物品
maxWeightValue = getMaxValueOfGoodsInKnapsack(currentGoodNum+1, weight,goodValue);
如果达到递归结束开始回溯,设置当前物品已经访问如果背包还有剩余空间,即还能放入其他物品则 继续放入物品;此时物品的重量应该为当前背包的重量+放入的该物品重量
//设置背包里当前物品已经访问
isVisitedKnapsack[currentGoodNum][weight] = true;
int currentWeight = weight + goodsWeight[currentGoodNum];
if (currentWeight <= knapsackMaxWeight){
if (isVisitedKnapsack[currentGoodNum][currentWeight]) return maxWeightValue;
maxWeightValue= getMaxValueOfGoodsInKnapsack(currentGoodNum+1, currentWeight,goodValue+goodsValues[currentGoodNum]);
}
回溯法完整代码
public static int getMaxValueOfGoodsInKnapsack(int currentGoodNum, int weight, int goodValue){
if (weight == knapsackMaxWeight || currentGoodNum == goodNum ) {
return Math.max(goodValue,maxWeightValue);
}
maxWeightValue = getMaxValueOfGoodsInKnapsack(currentGoodNum+1, weight,goodValue);
//设置背包里当前物品已经访问
isVisitedKnapsack[currentGoodNum][weight] = true;
int currentWeight = weight + goodsWeight[currentGoodNum];
if (currentWeight <= knapsackMaxWeight){
if (isVisitedKnapsack[currentGoodNum][currentWeight]) return maxWeightValue;
maxWeightValue= getMaxValueOfGoodsInKnapsack(currentGoodNum+1, currentWeight,goodValue+goodsValues[currentGoodNum]);
}
return maxWeightValue;
}
动态规划法
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态
动态规划的解题核心:
第一步:状态的定义
第二步:求状态转移方程
1 我们使用个二维数组记录背包里物品的实时价值,行代表物品的个数,列代表物品的重量
此数组就是记录了背包不同物品下的实时价值状态
int[][] knapsackValues = new int[goodNum][maxWeight+1];
2 当物品是0的时候即放第一个物品,肯定背包里都是的状态都是物品1的价值,物品1的重量为2则从角标为2开始都是1000
3 当物品个数为1的时候即我们需要放入物品重量2的第二物品时:
此时我们需要考虑到2个问题:
A :如果背包装不下当前物品
此时n个物品所产生的背包价值为前n-1物品所产生的背包最大物品价值
B: 背包装的下当前物品
1 在满足背包最大重量下不装当前 物品,那么此时我们的规划思路和装不下当前物品策略一样 :此时n个物品所产生的背包价值为前n-1物品所产生的背包最大物品价值
2 在满足背包的最大重量下装当前品:那么此时我们需要给当前物品预留背包空间,那么当前背包的物品总价值就为前n-1个物品所产生的价值+当前物品价值
策略 :选取1 2 下的最大价值作为当前物品的最佳组合,即选择最优解
4 重复2 3 步骤,直到遍历完所有物品则最右下角即为满足背包最大重量下背包所装物品最大价值
由第三步骤的分析我们很容易得出状态转移方程:
伪代码:
if(当前背包物品重量> 当前物品重量时)
取最优解Math.max(前n-1物品所产生的背包最大物品价值, 前n-1个物品所产生的价值+当前物品价值 )
else 前n-1物品所产生的背包最大物品价值
翻译成代码:
if (j >= goodWeights[i]) {
knapsackValues[i][j] = Math.max(knapsackValues[i - 1][j], goodsValues[i] + knapsackValues[i - 1][j - goodWeights[i]]);
else knapsackValues[i][j] = knapsackValues[i - 1][j];
5 如何找到放入背包的物品序列号呢?
因为最右下角是左后的背包价值,所以我们逆推:
从最右下角开始回溯,如果发现当前n个物品最佳组合价值== 前n-1个物品最佳组合价值说明当前n没有放入背包,反之放入了背包,则从背包去除当前物品重量继续向前回溯
int maxKnapsackWeight = 0;
//从最右下角开始回溯,发现当前n个物品最佳组合价值== 前n-1个物品最佳组合价值说明当前n没有放入背包,反之放入了背包,则从背包去除当前物品重量继续回溯
while (j >=0&& goodNum>=0) {
int end = knapsackValues[goodNum][j];
if (end >0 ) {
maxKnapsackWeight= Math.max(end,maxKnapsackWeight);
if (goodNum -1 >=0) {
int current = knapsackValues[goodNum-1][j];
if ( current!= end ){
System.out.printf("第%d个商品放入到背包\n", goodNum );
j -= goodWeights[goodNum];//减去当前物品重量后继续回溯
}
}
else System.out.printf("第%d个商品放入到背包\n", goodNum );
}
goodNum--;
}
System.out.println("最大价值为:"+maxKnapsackWeight);
完整代码:
public static void knapsack4(int[] goodWeights,int[] goodsValues,int maxWeight){
int goodNum = goodWeights.length;
int[][] knapsackValues = new int[goodNum][maxWeight+1];
for (int i = 1; i < goodNum; i++) { //物品层数
for (int j = 1; j <=maxWeight ; j++) { //动态的背包大小
if (j >= goodWeights[0]) knapsackValues[0][j] = goodsValues[0]; //定义背包初始状态为第一个物品的价值
if (j >= goodWeights[i]) {
//goodsValues[i] 当前物品价值 knapsackValues[i-1][j] 上一层规划装入的前i-1个物品最大价值;knapsackValues[i-1][j-goodWeights[i]] 装入当前物品后剩余空间装入的最大物品价值
knapsackValues[i][j] = Math.max(knapsackValues[i - 1][j], goodsValues[i] + knapsackValues[i - 1][j - goodWeights[i]]);
}
else knapsackValues[i][j] = knapsackValues[i - 1][j];
}
}
int j = knapsackValues[--goodNum].length-1;
int maxKnapsackWeight = 0;
//从最右下角开始回溯,发现当前n个物品最佳组合价值== 前n-1个物品最佳组合价值说明当前n没有放入背包,反之放入了背包,则从背包去除当前物品重量继续回溯
while (j >=0&& goodNum>=0) {
int end = knapsackValues[goodNum][j];
if (end >0 ) {
maxKnapsackWeight= Math.max(end,maxKnapsackWeight);
if (goodNum -1 >=0) {
int current = knapsackValues[goodNum-1][j];
if ( current!= end ){
System.out.printf("第%d个商品放入到背包\n", goodNum );
j -= goodWeights[goodNum];
}
}
else System.out.printf("第%d个商品放入到背包\n", goodNum );
}
goodNum--;
}
System.out.println("最大价值为:"+maxKnapsackWeight);
}
以上动态规划解决背包问题我们申请了一个二维数组来记录背包物品的实时价值状态
其实我们只需要一维数组就可以搞定,此一维数组来代替二维数组的功能:
每一层物品,就对应着一个一维数组;下一层规划则复用上一层的一维数组,即在上一层的规划基础上更新本层的物品价值;本层的不同背包重量下物品放入互相独立不收影响
缺点:是由于是此数组是实时变化的我们无法记录放入背包的物品序列号
public static int repeatKnapsack(int goodNum, int[] goodWeights, int[] goodsValues, int maxWeight) {
int[] dp = new int[maxWeight + 1];
for (int i = 0; i < goodNum; i++) {
if (goodWeights[i] <= maxWeight) {//超过最大值的肯定无法放入了
//这里不能正序??--其实我们使用一维数组来当做二维数组来使用;每一层物品,就对应着一个一维数组;下一层规划则复用上一层的一维数组,即在上一层的规划基础上更新本层的物品价值
//如果我们正序遍历,假如该物品为0即对应第一个物品,重量为2 价值为1000;在任意背包大小下只能放入物品0,因为此时只有一个物品0;
//动态规划每一个物品的放入依赖上一层规划的结果,而本层的不同背包重量下物品放入互相独立不收影响。
//但事实上我们是一维数组后面不同背包重量下放入的物品 会依赖前一背包重量,造成物品价值的重复计算。
for (int j = maxWeight; j > 0; j--) {
if (j >= goodWeights[i]) {
if (dp[j] < dp[j - goodWeights[i]] + goodsValues[i]) {
dp[j] = dp[j - goodWeights[i]] + goodsValues[i];
}
}
}
}
}
return dp[maxWeight];
}
动态规划解决数字塔问题
3
1 5
8 4 3
2 6 7 9
6 2 3 5 1
从上往下每个数字只能走到他正下方数字或者正右方数字,求数字塔从上到下所有路径中和最大的路径
当从上往下看时,每进来新的一行,新的一行每个元素只能选择他正上方或者左左方的元素,也就是说,第一个元素只能连他上方的元素,最后一个元素只能连他左上方的元素,其他元素可以有两种选择,所以需要选择加起来更大的那一个数字,并把这个位置上的数字改成相应的路径值;转移方程: dp[i][j] = Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j];
public static void minNumberInRotateArray(int[][] n){
int max = 0;
int[][] dp = new int[n.length][n.length];
dp[0][0] = n[0][0];
for(int i=1;i<n.length;i++){
for(int j=0;j<=i;j++){
if(j==0){
//如果是第一列,直接跟他上面数字相加
dp[i][j] = dp[i-1][j] + n[i][j];
}else{
//如果不是第一列,比较他上面跟上面左面数字谁大,谁大就跟谁相加,放到这个位置
dp[i][j] = Math.max(dp[i-1][j-1], dp[i-1][j]) + n[i][j];
}
max = Math.max(dp[i][j], max);
}
}
System.out.println(max);
}
最大连续公共子串和最大公共子序列
最大连续公共子串
最大连续公共子串–要求在母串连续存在
“abcbcbcef”, “abcbced” 我们需要求得最大连续公共子串为[bcbce, abcbc]
画出矩阵图
可以看出2条红色的线对应最长公共连续子串 [bcbce, abcbc]
判断A的第i个元素B的第j个元素是否相同即判断A[i - 1]和 B[j -1]是否相同,如果相同它就是dp[i - 1][j- 1] + 1,相当于在两个字符串都去掉一个字符时的最长公共子串再加 1;否则最长公共子串取0。所以整个问题的初始状态为:
dp[i][0]=0,dp[0][j]=0
相应的状态转移方程为:
dp[i][j]={0,dp[i−1][j−1]+1 ]
//最大连续公共字串-- 要求在母串中连续地出现
public static List<String> lcs(String str1, String str2) {
int len1 = str1.length();
int len2 = str2.length();
int result = 0; //记录最长公共子串长度
LinkedList<String> strList = new LinkedList<>();
int[][] c = new int[len1+1][len2+1];
for (int i = 1; i <= len1; i++) {
for( int j = 1; j <= len2; j++) {
boolean isEqual = str1.charAt(i-1) == str2.charAt(j-1);
if (isEqual) {
c[i][j] = c[i-1][j-1] + 1;
if ( c[i][j] >= result ) {
result = c[i][j];
String substring = str1.substring(i - result, i );//截取连续的子串
if (!strList.contains(substring)){
strList.addFirst(substring);
}
}
} else {
c[i][j] = 0;
}
}
}
//保留最长的子串,删除多余的
return strList.stream().takeWhile(e->e.length() >= strList.getFirst().length()).toList();
}
最大公共子序列
最大公共子序列不要求连续
求:“ABCBDAB”, "BDCABA” 的最大公共子序列–ABCBDAB
我们设 X=(x1,x2,…xn) 和 Y={y1,y2,…ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y),要找它们的最长公共子序列就是要求最优化问题,有以下几种情况:
1、n = 0 || m = 0,不用多说最长的也只能是0,LCS(n,m) = 0
2、X(n) = Y(m),说明当前序列也是相等的,那就给这两个元素匹配之前的最长长度加一,即LCS(n,m)=LCS(n-1,m-1)+1
3、X(n) != Y(m),这时候说明这两个元素并没有匹配上,那所以最长的公共子序列长度还是这两个元素匹配之前的最长长度,即max{LCS(n-1,m),LCS(n,m-1)}
可以得出状态转移方程:
public static String maximumCommonSubsequence(String str1, String str2) {
int len1 = str1.length();
int len2 = str2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
boolean isEqual = str1.charAt(i - 1) == str2.charAt(j - 1);
if (isEqual) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
//回溯公共子序列
int i = len1;
int j = len2;
StringBuilder sb = new StringBuilder();
while (i > 0 && j > 0) {
if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
sb.append(str1.charAt(i - 1));
i--;
j--;
} else if (dp[i][j - 1] >= dp[i - 1][j]) {
j--;
} else {
i--;
}
}
return sb.reverse().toString();
}