【算法】动态规划:背包问题、股票问题、序列问题、编辑距离、回文串问题

一、背包问题

1、概念

对于动态规划问题:
(1)确定dp数组(dp table)以及下标的含义
(2)确定递推公式
(3)dp数组如何初始化
(4)确定遍历顺序
(5)举例推导dp数组

在这里插入图片描述
总结:01背包问题:二维数组先遍历背包或者容量都可以,for循环都是从小到大。
一维数组先遍历背包或者容量都可以,但是外循环从小到大,内循环从大到小
完全背包问题:一律用一维数组。
组合问题:外遍历物品内遍历背包
排列问题:外遍历背包内遍历物品。

2、01背包

二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

1.1 创建二维数组

注意,先遍历物品还是先遍历容量都无所谓,这个顺序是都可以的。需要初始化第一排和第一列,在遍历的时候无论是内循环还是外循环都是顺序遍历。
递推公式:dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
dp[i][j]:表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
dp[i - 1][j]:表示容量为j,如果不放第i个物品,那么就等于容量为j时放0-(i-1)可以达到的最大价值。
dp[i - 1][j - weight[i]] + value[i - 1]:dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

public static void testweightbagproblem(int[] weight, int[] value, int bagsize){
    int wlen = weight.length, value0 = 0;
    // 定义dp数组:dp[i][j]表示背包容量为j时,0-i物品能获得的最大价值
    int[][] dp = new int[wlen][bagsize + 1];
    // 初始化:背包容量为0时,能获得的价值都为0
    for (int i = 0; i <= wlen; i++){
        dp[i][0] = value0;
    }
    // 初始化:当背包容量大于weight[0]的时候,才可以放入
    for (int j = weight[0]; j <= wlen; j++) {
        dp[0][j] = value[0];
    }
    // 遍历顺序:先遍历物品,再遍历背包容量
    for (int i = 1; i <= wlen; i++){
        for (int j = 1; j <= bagsize; j++){
            if (j < weight[i]){
                dp[i][j] = dp[i - 1][j];
            }else{
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
            }
        }
    }
    //打印dp数组
    for (int i = 0; i <= wlen; i++){
        for (int j = 0; j <= bagsize; j++){
            System.out.print(dp[i][j] + " ");
        }
        System.out.print("\n");
    }
}

1.2 创建一维数组

注意,同样是先遍历物品还是被包容两都无所谓,但是外循环是顺序遍历,内循环是倒序遍历,因为只有内循环倒序遍历才会防止物品重复加入到背包中。
递推公式:dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);

public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
    int wLen = weight.length;
    // 定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
    int[] dp = new int[bagWeight + 1];
    // 遍历顺序:外遍历物品,内遍历背包容量
    for (int i = 0; i < wLen; i++){
    	// 从后往前遍历,如果容量小于目前的物品重量那肯定是放不下去,所以不用管,只需要保证容量大于等于物品重量。
    	// 这时就要比较放与不放的价值大小了。
        for (int j = bagWeight; j >= weight[i]; j--){
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    // 打印dp数组
    for (int j = 0; j <= bagWeight; j++){
        System.out.print(dp[j] + " ");
    }
}

1.3 组合种类

种类的问题就是动态数组的含义和递推公式不太一样。
(1)动态数组依然是dp[j],但是其含义是和为j的情况数,因为求的就是种类数嘛。
(2)递推公式:在求最大重量的时候都是进行比较大小,但是在求情况数的时候一般使用两种情况相加。
dp[j] = dp[j] + dp[j - nums[i]];
如果不考虑nums[i],那么就是有dp[j]中情况,可以使总和为j;
如果考虑nums[i],那么为了保持总和为j,需要其实就是dp[j - nums[i]]种情况,每一种情况加上nums[i];

public int findTargetSumWays(int[] nums, int target) {
    // nums的总和为sum,加号的总和为left, 减号的总和是right
    // left - right = target
    // left + right = sum
    // left - (sum - left) = target -> left = (target + sum) / 2;
    int sum = 0;
    for (int num : nums) sum += num;
    if ((target + sum) % 2 == 1) return 0;
    if (Math.abs(target) > sum) return 0;
    int left = (target + sum) / 2;
    int[] dp = new int[left + 1];
    dp[0] = 1;
    for (int i = 0; i < nums.length; i++) {
        for (int j = left; j >= nums[i]; j--) {
            // dp[j] = dp[j] + dp[j - nums[i]];
            // 如果不考虑nums[i],那么就是有dp[j]中情况,可以使总和为j
            // 如果考虑nums[i],那么为了保持总和为j,需要其实就是dp[j - nums[i]]种情况,每一种情况加上nums[i]
            // 所以是dp[j] = dp[j] + dp[j - nums[i]];
            dp[j] += dp[j - nums[i]];
        }
    }
    return dp[left];
}

2、完全背包:每件物品可以放无数次

纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。

1、组合问题:顺序不重要

注意:先遍历物品还是背包依然没有不同都可以,注意内外循环都需要是顺序的,01背包讲过,如果内循环是顺序的,会造成物品重复添加,但是在这种题目里面,我们就是由无数个可以添加。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
递推公式:dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);

//先遍历物品,再遍历背包
private static void testCompletePack(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 0; i < weight.length; i++){ // 遍历物品
        for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

//先遍历背包,再遍历物品
private static void testCompletePackAnotherWay(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 1; i <= bagWeight; i++){ // 遍历背包容量
        for (int j = 0; j < weight.length; j++){ // 遍历物品
            if (i - weight[j] >= 0){
                dp[i] = Math.max(dp[i], dp[i - weight[j]] + value[j]);
            }
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

2.2 排列问题:顺序有要求

dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。
因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。
在动态规划:494.目标和 (opens new window) 和 动态规划:518.零钱兑换II (opens new window)中我们已经讲过了,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
如果求排列数就是外层for遍历背包,内层for循环遍历物品。为什么外层遍历背包,内层遍历物品呢?因为如果外层遍历物品,内层遍历背包,物品的顺序是固定的,对于每一个背包容量,都是先加入前面的物品再加入后面的物品。所以需要先遍历背包,在遍历物品。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int i = 0; i <= target; i++) { // 先遍历背包容量
            for (int j = 0; j < nums.length; j++) { // 在遍历物品
                if (i >= nums[j]) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }
}

二、股票问题

股票问题一般都是用一个二维数组表示持有和不持有,这里的持有表示有可能当天买入,也有可能是之前买入了今天没有任何操作,同样,不持有也是表示当天卖出也可能是之前就卖出了,但是一直没有其他操作。

dp[i][0] 表示第i天持有股票所得现金。
dp[i][1] 表示第i天不持有股票所得现金。

1、只能买卖一次

dp[i][0] 表示第i天持有股票所得现金。
dp[i][1] 表示第i天不持有股票所得现金。

  • 如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来:
    1⃣️第i-1天持有股票:那么说明今天没有任何操作 dp[i][0] = dp[i-1][0];
    2⃣️第i-1天没有持有改股票:由于只能买卖一次,那么这次持有股票就是卖入,而且没有前面的累积金额,所以dp[i][0] = -prices[I];
    所以第i天持有股票:dp[i][0] = Math.max(dp[i-1][0], -prices[I]);
  • 如果第i天不持有股票即dp[i][1], 那么可以由两个状态推出来:
    1⃣️第i-1天持有股票:那么说明今天将股票卖出了 dp[i][1] = dp[i-1][0] + prices[i];
    2⃣️第i-1天没有持有改股票:说明今天没有任何操作,所以dp[i][1] = dp[i-1][1];
    所以第i天持有股票:dp[i][1] = Math.max(dp[i-1][0] + prices[i], dp[i-1][1]);

2、可以买卖多次

dp[i][0] 表示第i天持有股票所得现金。
dp[i][1] 表示第i天不持有股票所得现金。

  • 如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来:
    1⃣️如果第i-1天持有:保持现状,dp[i][0] = dp[i-1][0];
    2⃣️如果第i-1天没有持有:说明第i天买入了,相对于只能买卖一次的情况,买卖多次在买入的时候是需要加上第i-1天没有持有的累积收益,也就是dp[i][0] = dp[i-1][1] - prices[i];
    所以第i天持有股票:dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);
  • 如果第i天不持有股票即dp[i][1], 那么可以由两个状态推出来:
    1⃣️如果第i-1天持有:说明第i天卖出,所以需要买入第i-1天买入后的累积收益dp[i][1] = dp[i-1][0] + prices[i];
    2⃣️如果第i-1天没有持有:保持现状,dp[i][1] = dp[i-1][1];
    所以第i天没有持有股票:dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);

3、只能买卖2次

一天一共就有五个状态,
dp[i][0]:没有操作
dp[i][1]:第一次买入
dp[i][2]:第一次卖出
dp[i][3]:第二次买入
dp[i][4]:第二次卖出
dp[i][j]表示第i天状态j所剩最大现金。

  1. 如果第i天没有任何操作,那么dp[i][0] = dp[i-1][0];

  2. 如果第i天是第一次买入,那么dp[i][1] 有以下两种操作:
    1⃣️第i-1天就已经持有了,所以这次持有没有任何操作:dp[i][1] = dp[i-1][1];
    2⃣️第i-1天没有持有,因为是第一次买入,那么上一次只能是没有任何操作才可以:dp[i][1] = dp[i-1][0] - prices[i];
    所以第i天第一次买入:dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[I]);

  3. 如果第i天是第一次买出,那么dp[i][2] 有以下两种操作:
    1⃣️第i-1天持有,那么今天就卖出,因为是第一次卖出,所以只能是第一次买入与其计算:dp[i][2] = dp[i-1][1] + prices[I];
    2⃣️第i-1天没有持有,说明没有任何操作:dp[i][2] = dp[i-1][2];
    所以第i天是第二次卖出:dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + prices[i]);

  4. 第i天是第二次买入,与第一次同理:dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] - prices[i]);

  5. 第i天是第二次卖出,与第一次卖出同理:dp[i][4] = Math.max(dp[i-1][4], dp[i-1][3] + prices[i]);

4、只能买卖k次

找规律,当只允许买卖两次的时候,是创建了5种情况,那么买卖k次就是有2 * k + 1,其他同理。

5、卖出后又冷静期

四种状态:
dp[i][0]:表示持有状态(有可能是第i天买入,也有可能是之前就买入了)
dp[i][1]:表示卖出状态,今天卖出
dp[i][2]:表示卖出状态,之前就卖出了,已经度过冷冻期了。
dp[i][3]:表示冷冻期状态。

  1. 第i天持有状态:
    1⃣️前一天就是持有状态:dp[i-1][0]
    2⃣️前一天是卖出状态,已经过了冷冻期,今天买入的:dp[i-1][2] - prices[i];
    3⃣️前一天是冷冻期,那么今天就过了冷冻期,所以:dp[i-1][3] - prices[i];
    所以:dp[i][0] = Math.max(dp[i-1][0], Math.max(dp[i-1][2], dp[i-1][3]) - prices[i]);
  2. 第i天是卖出状态,今天卖出:
    前一天只能是持有状态,才可以在今天就卖出:dp[i][1] = dp[i-1][0] + prices[i]
  3. 第i天是卖出状态,过了冷静期:
    1⃣️前一天也是这个状态:dp[i-1][2]
    2⃣️前一天是冷冻期状态:dp[i-1][3]
    所以:dp[i][2] = Math.max(dp[i-1][2], dp[i-1][3]);
  4. 第i天是冷冻期:
    前一天只能是卖出状态:dp[i][3] = dp[i-1][1];

6、卖卖需要手续费

与第二种可以买卖无数次的情况相同,只是在买入的时候需要扣除掉手续费。

三、序列问题

1、最长递增子序列

不需要连续,所以要循环判断是否递增。
当数组遍历到i的时候,j=i-1向前遍历到0,如果nums[i]>nums[j]的话,即说明满足递增要求,就可以递增数量+1,取最大值即可。

for (int i = 0; i < dp.length; i++) {
   for (int j = 0; j < i; j++) {
       if (nums[i] > nums[j]) {
          dp[i] = Math.max(dp[i], dp[j] + 1);
       }
   }
}

2、最长连续递增子序列

连续的递增子序列,不需要循环判断了,只需要判断是否比上一个大与否。

// 主要因为每个数都可以看着是一个长度为1的递增子序列,所以初始化的时候全都设为1
Arrays.fill(dp, 1);
for(int i = 1; i < n; i++) {
    if (nums[i] > nums[i-1]) {
        dp[i] = dp[i-1] + 1;
        max = Math.max(max, dp[i]);
    }
}

3、 最长重复子数组

int[][] dp = new int[n+1][m+1];
int max = 0;
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
        if (nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
        max = Math.max(max, dp[i][j]);
    }
}

4、 最长公共子数组

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

(求公共顺序子数组)

int[][] dp = new int[len1+1][len2+1];
// dp[i][j]表示以i-1和j-1结尾的最长公共子串
int max = 0;
for (int i = 1; i <= len1; i++) {
    for(int j = 1; j <= len2; j++) {
        if (text1.charAt(i-1) == text2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
        else dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
        max = Math.max(max, dp[i][j]);
    }
}

5、最大子序和

for (int i = 1; i < nums.length; i++) {
    dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
    res = res > dp[i] ? res : dp[i];
}

4⃣️、编辑距离问题

1、判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
这道题其实是一个不连续的子序列问题,dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。如果最后相同的子序列的长度正好是字符串t的长度,那么就说明t是s的子序列。

// 如果s[i-1] == t[j-1], dp[i][j] = dp[i-1][j-1] + 1;
// 如果s[i-1] != t[j-1], 以i-1和j-1结尾的两者不相等,那么就要看以i-2和j-1结尾.dp[i][j] = dp[i][j-1];
if(s.charAt(i-1) == t.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = dp[i][j-1];

2、不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
这种子序列出现的个数就要涉及用不用某一个字符进行匹配了。

// s[i-1] == t[j-1], 就可以有两种情况:
// 第一就是用s[i-1]进行匹配,那么这时就有dp[i-1][j-1]种匹配情况。
// 第二是不用s[i-1]进行匹配,那么就要考虑以i-2结尾的字符串与t[j-1]的匹配,有dp[i-1][j]中匹配。
// 所以一共有:dp[i-1][j-1] + dp[i-1][j]种。
// s[i-1] != t[j-1], 那么就只有一种情况,那么就是如上第二种:dp[i-1][j]。
if(s.charAt(i-1) == t.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
else dp[i][j] = dp[i-1][j];

3、两个字符串的删除操作

给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。

// 跟上一个一样,如果s[i-1]==t[j-1],那么相同的字符一共有dp[i-1][j-1]+1个。
// s[i-1]!=t[j-1], 那么就要考虑是删哪个了,如果删word1,那么有dp[i-1][j]个相同的字符串,如果删word2,那么有dp[i][j-1]个相同的字符串。所以就是Math.max(dp[i-1][j],dp[i][j-1])
if(s.charAt(i-1) == t.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
// 最后结果就是有len1 + len2 - 2 * max个操作。

4、编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符、删除一个字符、替换一个字符

// 1、 w1[i-1]==w2[j-1]
// 	   不需要任何操作,所以操作数就是dp[i-1][j-1]
// 2、 w1[i-1]!=w2[j-1]:有如下三个操作:
//		2.1 删除w1:一共有dp[i-1][j]+1个操作
//		2.2 删除w2:(添加w1):一共有dp[i][j-1]+1个操作
// 		2.3 替换w1:(替换w2):一共有dp[i-1][j-1]+1个操作。
if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;

五、回文串问题

1、回文子串(连续的)

boolean[][] dp = new boolean[n][n];
int res = 0;
// 表示[i,j]的回文串的个数,如果s[i] == s[j],dp[i][j] = dp[i+1][j-1]
for (int i = n-1; i >= 0; i--) {
    for (int j = i; j < n; j++) {
    	// 情况1:两者不想等,直接跳过
        if (s.charAt(i) != s.charAt(j)) continue;
        // 情况2:两者相等
        // 2.1 i == j: 肯定是回文串
        // 2.2 i与j相差1,又想等,比如aa,肯定也是回文串
        // 2.3 比较i+1到j-1,如果其为true,那么就是true。
        if (j - i <= 1 || dp[i+1][j-1]) {
            res++;
            dp[i][j] = true;
        }
    }
}

2、最长回文子串(可不连续)

// 1、如果i == j,肯定是回文串而且长度为1
// 2、如果s.charAt(i) == s.charAt(j),那么回文串的长度就是+2,即两个端点。
// 3、如果s.charAt(i) != s.charAt(j),dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
int[][] dp = new int[len][len];
int max = 0;
for (int i = len-1; i >= 0; i--) {
    for (int j = i; j < len; j++) {
        if (i == j) dp[i][j] = 1;
        else if (s.charAt(i) == s.charAt(j)){
            dp[i][j] = dp[i+1][j-1] + 2;
        }else {
            dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
        }
        max = Math.max(dp[i][j], max);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值