八、动态规划(Dynamic Programming)

news2024/11/19 15:12:59

文章目录

  • 一、理论基础
  • 二、题目分类
    • (一)基础题目
      • 2.[70.爬楼梯](https://leetcode.cn/problems/climbing-stairs/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 3.[746. 使用最小花费爬楼梯](https://leetcode.cn/problems/min-cost-climbing-stairs/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 4.[62.不同路径](https://leetcode.cn/problems/unique-paths/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 5.[63.不同路径2](https://leetcode.cn/problems/unique-paths-ii/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 6.[343.整数拆分](https://leetcode.cn/problems/integer-break/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
    • (二)背包理论基础
      • 1.[416.分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/description/)
        • (1)思路
        • (2)代码
    • (三)打家劫舍
    • (四)股票
    • (五)子序列
      • 1. [300.最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 2. [674. 最长连续递增序列](https://leetcode.cn/problems/longest-continuous-increasing-subsequence/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 3. [18. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/)
        • (1)思路
        • (2)代码
      • 4. [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/description/)
        • (3)复杂度分析
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 5. [1035.不相交的线](https://leetcode.cn/problems/uncrossed-lines/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 6. [53.最大子数组和](https://leetcode.cn/problems/maximum-subarray/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 7. [392. 判断子序列](https://leetcode.cn/problems/is-subsequence/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 8. [115. 不同的子序列](https://leetcode.cn/problems/distinct-subsequences/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 9.[583. 两个字符串的删除操作](https://leetcode.cn/problems/delete-operation-for-two-strings/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 10.[72. 编辑距离](https://leetcode.cn/problems/edit-distance/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 11.[647. 回文子串](https://leetcode.cn/problems/palindromic-substrings/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析
      • 12.[516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/description/)
        • (1)思路
        • (2)代码
        • (3)复杂度分析

一、理论基础

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的!
对于动态规划问题,拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。

二、题目分类

(一)基础题目

2.70.爬楼梯

(1)思路
  1. 确定dp数组以及下标的含义
    dp[i]: 爬到第i层楼梯,有dp[i]种方法

  2. 确定递推公式
    如何可以推出dp[i]呢?
    从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。
    首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。
    还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。
    那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!
    所以dp[i] = dp[i - 1] + dp[i - 2] 。
    在推导dp[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。

这体现出确定dp数组以及下标的含义的重要性!

  1. dp数组如何初始化
    再回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]种方法。
    那么i为0,dp[i]应该是多少呢,这个可以有很多解释,但基本都是直接奔着答案去解释的。

例如强行安慰自己爬到第0层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶。
但总有点牵强的成分。

那还这么理解呢:我就认为跑到第0层,方法就是0啊,一步只能走一个台阶或者两个台阶,然而楼层是0,直接站楼顶上了,就是不用方法,dp[0]就应该是0.

其实这么争论下去没有意义,大部分解释说dp[0]应该为1的理由其实是因为dp[0]=1的话在递推的过程中i从2开始遍历本题就能过,然后就往结果上靠去解释dp[0] = 1。

从dp数组定义的角度上来说,dp[0] = 0 也能说得通。

需要注意的是:题目中说了n是一个正整数,题目根本就没说n有为0的情况。

所以本题其实就不应该讨论dp[0]的初始化!

我相信dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。
所以我的原则是:不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。

  1. 确定遍历顺序
    从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的

  2. 举例推导dp数组
    举例当n为5的时候,dp table(dp数组)应该是这样的

(2)代码
版本一:
class Solution {
public:
    int climbStairs(int n) {
    if(n <= 1)
    return 1;
    vector<int> dp(n + 1);
	dp[1] = 1;
	dp[2] = 2;
	for (int i = 3; i <= n; i++) {
		dp[i] = dp[i - 1] + dp[i - 2];
	}
	return dp[n];
    }
};
// 版本二
class Solution {
public:
    int climbStairs(int n) {
        if (n <= 1) return n;
        int dp[3];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            int sum = dp[1] + dp[2];
            dp[1] = dp[2];
            dp[2] = sum;
        }
        return dp[2];
    }
};
(3)复杂度分析

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)

3.746. 使用最小花费爬楼梯

(1)思路
  1. 确定dp数组以及下标的含义
    dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。
  2. 可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2], 所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
  3. dp数组如何初始化
    所以初始化 dp[0] = 0,dp[1] = 0;
  4. 确定遍历顺序
    最后一步,递归公式有了,初始化有了,如何遍历呢?
    本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。
    因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。
  5. 举例推导dp数组
    示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:
(2)代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size() + 1);
        dp[0] = 0; // 默认第一步都是不花费体力的
        dp[1] = 0;
        for (int i = 2; i <= cost.size(); i++) {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[cost.size()];
    }
};
(3)复杂度分析

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)

4.62.不同路径

(1)思路
(2)代码
class Solution {
public:

    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m + 1,vector<int>(n + 1));
        // 1. 含义:到第m行n列共有几条路。
        // 2. 公式:dp[i - 1][j] + dp[i][j - 1]
        // 3. 初始化:  for j in n dp[0][j] = 1;   for i in m dp[i][0] = 1;  
        // 4. 方向:从前向后 
        // 5. 打印:
        dp[0][0] = 1;
        for (int j = 1; j < n; j++) {
            dp[0][j] = 1;
        }
        for (int i = 1; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
};
(3)复杂度分析

时间复杂度: O ( m × n ) O(m × n) O(m×n)
空间复杂度: O ( n ) O(n) O(n)

5.63.不同路径2

(1)思路

// 1. 含义:到第m行n列共有几条路。
// 2. 公式:dp[i - 1][j] + dp[i][j - 1]
// 3. 初始化: for j in n dp[0][j] = 1; for i in m dp[i][0] = 1; // 注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理
// 4. 方向:从前向后
// 5. 打印:

(2)代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        vector<vector<int>> dp(m + 1,vector<int>(n + 1));
        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
            return 0;
        // 1. 含义:到第m行n列共有几条路。
        // 2. 公式:dp[i - 1][j] + dp[i][j - 1]
        // 3. 初始化:  for j in n dp[0][j] = 1;   for i in m dp[i][0] = 1;  
        // 4. 方向:从前向后 
        // 5. 打印:
        dp[0][0] = 1;
        for (int j = 1; j < n && obstacleGrid[0][j] == 0; j++) { // 注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理
            dp[0][j] = 1;
        }
        for (int i = 1; i < m && obstacleGrid[i][0] == 0; i++) {
            dp[i][0] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] == 1) continue;
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
};
(3)复杂度分析

时间复杂度: O ( m × n ) O(m × n) O(m×n)
空间复杂度: O ( n ) O(n) O(n)

6.343.整数拆分

(1)思路

看到这道题目,都会想拆成两个呢,还是三个呢,还是四个…

我们来看一下如何使用动规来解决。

(2)代码
class Solution {
public:
    int integerBreak(int n) {
    // 1.确定dp数组(dp table)以及下标的含义 dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。
    // 2.公式:一个是j * (i - j) 直接相乘。一个是j * dp[i - j],相当于是拆分(i - j)。
    // 3.dp的初始化 只初始化dp[2] = 1
    // 4.dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
    // 5.
        vector<int> dp(n + 1);
        dp[2] = 1;
        for (int i = 3; i <= n ; i++) {
            for (int j = 1; j <= i / 2; j++) {
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
            }
        }
        return dp[n];
    }
};
贪心:
class Solution {
public:
    int integerBreak(int n) {
        if (n == 2) return 1;
        if (n == 3) return 2;
        if (n == 4) return 4;
        int result = 1;
        while (n > 4) {
            result *= 3;
            n -= 3;
        }
        result *= n;
        return result;
    }
};
(3)复杂度分析

时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)

(二)背包理论基础

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
在这里插入图片描述
二维dp数组01背包
依然动规五部曲分析一波。

  1. 确定dp数组以及下标的含义
    对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
  2. 确定递推公式
    再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

那么可以有两个方向推出来dp[i][j],

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
    所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
  1. dp数组如何初始化
    关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。

首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:

1.416.分割等和子集

(1)思路

看到这道题目,都会想拆成两个呢,还是三个呢,还是四个…

我们来看一下如何使用动规来解决。

(2)代码

(三)打家劫舍

(四)股票

(五)子序列

1. 300.最长递增子序列

(1)思路
  1. dp[i]的定义
    本题中,正确定义dp数组的含义十分重要。

dp[i]示i之前包括i的以nums[i]结尾的最长递增子序列的长度
为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在 做 递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么如何算递增呢。

  1. 状态转移方程
    位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。

所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。

  1. dp[i]的初始化
    每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.

  2. 确定遍历顺序
    dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。

j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。

  1. 举例推导
(2)代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(),1);
        int result = 0;
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j])
                    dp[i] = max(dp[i],dp[j] + 1);
            }
            if (dp[i] > result) result = dp[i]; // 取长的子序列
        }
        return result;
    }
};
(3)复杂度分析

时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)

2. 674. 最长连续递增序列

(1)思路
  1. 确定dp数组(dp table)以及下标的含义,dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]。
  2. 确定递推公式
    如果 nums[i] > nums[i - 1],那么以 i 为结尾的连续递增的子序列长度 一定等于 以i - 1为结尾的连续递增的子序列长度 + 1 。即:dp[i] = dp[i - 1] + 1;因为本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。
  3. 确定递推公式 以下标i为结尾的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素。
  4. 确定遍历顺序
    从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。
  5. 举例推导dp数组
    已输入nums = [1,3,5,4,7]为例,dp数组状态如下:
(2)代码
class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        // 1. dp[i]的定义,以i结尾的最长连续递增子序列长度
        // 2. 方程  if (nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
        // 3. 初始  dp[0] = 1; 
        // 4. 顺序  从左到右
        // 5. 打印 
        if (nums.size() == 0) return 0;
        int result = 1;
        vector<int> dp(nums.size() ,1);
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] > nums[i - 1]) { // 连续记录
                dp[i] = dp[i - 1] + 1;
            }
            if (dp[i] > result) result = dp[i];
        }
        return result;

    }
};
(3)复杂度分析

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)

3. 18. 最长重复子数组

(1)思路

// 1. dp[i][j] 表示,nums1以i - 1结尾,nums2以j - 1结尾的最长重复子数组的长度
// 2. 方程:根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始! dp[i][j] = dp[i - 1][j - 1] + 1;
// 3. 初始化 所以dp[i][0] 和dp[0][j]初始化为0。
// 4. 顺序 外层for循环遍历A,内层for循环遍历B。
// 5. 打印

(2)代码
class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        // 1. dp[i][j] 表示,nums1以i - 1结尾,nums2以j - 1结尾的最长重复子数组的长度
        // 2. 方程: dp[i][j] = dp[i - 1][j - 1] + 1;
        // 3. 初始化 所以dp[i][0] 和dp[0][j]初始化为0。
        // 4. 顺序 外层for循环遍历A,内层for循环遍历B。
        // 5. 打印
        vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        int result = 0;
        for (int i = 1; i <= nums1.size(); i++) {
            for (int j = 1; j <= nums2.size(); j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                if (dp[i][j] > result) result = dp[i][j];
            }
        }
        return result;
    }
};

4. 1143. 最长公共子序列

(3)复杂度分析

时间复杂度: O ( n ∗ m ) O(n * m) O(nm)
空间复杂度: O ( n ∗ m ) O(n * m) O(nm)

(1)思路

// 1. dp[i][j] 表示,长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
// 2. 方程: if (text1[i] == text2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
// else dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])
// 3. 初始化 if (text1[0] == text2[0]) dp[0][0] = 1; else dp[0][0] = 0;
// 4. 顺序 外层for循环遍历A,内层for循环遍历B。
// 5. 打印

(2)代码
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        // 1. dp[i][j] 表示,长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
        // 2. 方程: if (text1[i] == text2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
             // else dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])
        // 3. 初始化 if (text1[0] == text2[0]) dp[0][0] = 1; else dp[0][0] = 0;
        // 4. 顺序 外层for循环遍历A,内层for循环遍历B。
        // 5. 打印
        vector<vector<int>> dp(text1.size() + 1,vector<int>(text2.size() + 1, 0));
        for (int i = 1; i <= text1.size(); i++) {
            for (int j = 1; j <= text2.size(); j++) {
                 if (text1[i - 1] == text2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else {
                    dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]);
                }
            }
        }
          return dp[text1.size()][text2.size()];
    }
};
(3)复杂度分析

时间复杂度: O ( n ∗ m ) O(n * m) O(nm)
空间复杂度: O ( n ∗ m ) O(n * m) O(nm)

5. 1035.不相交的线

(1)思路

和1143. 最长公共子序列思路一样!

(2)代码
class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        // 1. dp[i][j]  nums1以i 结尾 nums2 以j结尾的最长公共子序列的长度!
         vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        for (int i = 1; i <= nums1.size(); i++) {
            for (int j = 1; j <= nums2.size(); j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[nums1.size()][nums2.size()];
    }
};
(3)复杂度分析

时间复杂度: O ( n ∗ m ) O(n * m) O(nm)
空间复杂度: O ( n ∗ m ) O(n * m) O(nm)

6. 53.最大子数组和

(1)思路
   // 1. dp[i] 包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
    // 2. dp[i] = max(nums[i],dp[i - 1] + nums[i]);  dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
    // nums[i],即:从头开始计算当前连续子序列和
    // 3.  从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。
    // 4. 1 - n
    // 5. 方程
(2)代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        // 1. dp[i] 包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
        // 2. dp[i] = max(nums[i],dp[i - 1] + nums[i]);  dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
        // nums[i],即:从头开始计算当前连续子序列和
        // 3.  从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。
        // 4. 1 - n
        // 5. 方程
        if (nums.size() == 0) return 0;
     
        vector<int> dp(nums.size(),0);
        
        dp[0] = nums[0];
        int result = dp[0];
        for (int i = 1; i < nums.size(); i++) {
            dp[i] = max(dp[i - 1] + nums[i],nums[i]);
            if (dp[i] > result) result = dp[i];
        }
        return result;
    } 
};
(3)复杂度分析

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)

7. 392. 判断子序列

(1)思路
  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
  2. 确定递推公式
    在确定递推公式的时候,首先要考虑如下两种操作,整理如下:
    if (s[i - 1] == t[j - 1])
    t中找到了一个字符在s中也出现了
    if (s[i - 1] != t[j - 1])
    相当于t要删除元素,继续匹配
  3. dp数组如何初始化
    从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],所以dp[0][0]和dp[i][0]是一定要初始化的。
  4. 确定遍历顺序
    同理从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],那么遍历顺序也应该是从上到下,从左到右
  5. 举例推导dp数组
(2)代码
class Solution {
public:
    bool isSubsequence(string s, string t) {
        // 1. dp[i][j] 表示 s 以 i - 1 结尾,t 以 j - 1 结尾, 相同子序列的长度为dp[i][j]
        // 2. if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1
        // if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];
        // 3. 初始化
        // 4. 同理从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],那么遍历顺序也应该是从上到下,从左到右
        vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
         for (int i = 1; i <= s.size(); i++) {
            for (int j = 1; j <= t.size(); j++) {
                if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = dp[i][j - 1];
            }
        }
       if (dp[s.size()][t.size()] == s.size()) return true;
        return false;
    }
};
(3)复杂度分析

时间复杂度: O ( n ∗ m ) O(n * m) O(nm)
空间复杂度: O ( n ∗ m ) O(n * m) O(nm)

8. 115. 不同的子序列

(1)思路
  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
  2. 确定递推公式
    这一类问题,基本是要分析两种情况
    s[i - 1] 与 t[j - 1]相等
    s[i - 1] 与 t[j - 1] 不相等
(2)代码
(3)复杂度分析

9.583. 两个字符串的删除操作

(1)思路
(2)代码
(3)复杂度分析

10.72. 编辑距离

(1)思路
(2)代码
(3)复杂度分析

11.647. 回文子串

(1)思路
  1. 布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
  2. 公式:
    当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
    当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
  • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
  • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
  • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
  1. dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹配上了。
    所以dp[i][j]初始化为false。
  2. 确定遍历顺序
    所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
  3. 举例推导dp数组
(2)代码
(3)复杂度分析

12.516. 最长回文子序列

(1)思路
(2)代码
(3)复杂度分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1059418.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

正点原子嵌入式linux驱动开发——U-boot顶层Makefile详解

在学习uboot源码之前&#xff0c;要先看一下顶层Makefile&#xff0c;分析gcc版本代码的时候一定是先从顶层Makefile开始的&#xff0c;然后再是子Makefile&#xff0c;这样通过层层分析Makefile即可了解整个工程的组织结构。顶层Makefile也就是uboot根目录下的Makefile文件&am…

怪兽智能推出3D数字人虚拟主播,实时动作捕捉赋能直播营销,打造全新营销场景

怪兽智能科技推出了数字人直播新功能——3D数字人虚拟主播&#xff0c;通过实时动作捕捉技术&#xff0c;为直播营销场景注入了全新的活力和创意。这一功能将为企业带来更加生动、鲜活的营销体验&#xff0c;助力品牌在竞争激烈的市场中占据优势。 传统的直播营销方式往往依赖于…

论文笔记:Contrastive Trajectory Similarity Learning withDual-Feature Attention

ICDE 2023 1 intro 1.1 背景 轨迹相似性&#xff0c;可以分为两类 启发式度量 根据手工制定的规则&#xff0c;找到两条轨迹之间基于点的匹配学习式度量 通过计算轨迹嵌入之间的距离来预测相似性值上述两种度量的挑战&#xff1a; 无效性&#xff1a; 具有不同采样率或含有噪…

spring的面向切面编程

如果您觉得本博客的内容对您有所帮助或启发&#xff0c;请关注我的博客&#xff0c;以便第一时间获取最新技术文章和教程。同时&#xff0c;也欢迎您在评论区留言&#xff0c;分享想法和建议。谢谢支持&#xff01; 一、介绍什么是面向切面编程&#xff08;AOP&#xff09; 1.…

lv7 嵌入式开发-网络编程开发 05 字节序及IP地址转换

目录 1 主机字节序和网络字节序 1.1 什么是字节序&#xff1f; 1.2 查看主机字节序 2 字节序转换函数 3 IP地址字节序转换函数 4 练习 1 主机字节序和网络字节序 1.1 什么是字节序&#xff1f; 字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序&a…

Qt creator+cmake编译并安装

1、qt creator打开项目中的CMakeLists.txt 2、修改“构建设置“-“Cmake”-”Current Configuration“&#xff0c;其中&#xff0c;安装路径为CMAKE_INSTALL_PREFIX 3、修改“构建设置“-“构建的步骤”-”目标“&#xff0c;勾选"all"和"install" 4、构…

acwing198反素数(题解)

对于任何正整数 x&#xff0c;其约数的个数记作 g(x)&#xff0c;例如 g(1)1、g(6)4&#xfffd;(1)1、&#xfffd;(6)4。 如果某个正整数 x满足&#xff1a;对于任意的小于 x 的正整数 i&#xff0c;都有 g(x)>g(i)&#xff0c;则称 x为反素数。 例如&#xff0c;整数 1…

孔雀东南飞:经济高质量发展与人才流动(数据复现)

数据简介&#xff1a;人才是经济高质量发展的动力源泉。中国ZF一直高度重视人才培养&#xff0c;积极发挥人才作用。“人才是第一资源”,“深入实施……人才强国战略”,“坚持……人才引领驱动”。与此同时&#xff0c;地方ZF大力引进人才&#xff0c;不断推出各类人才优待政策…

提升您的工作效率:TechSmith Snagit for Mac:强大的屏幕截图软件

在当今数字化的时代&#xff0c;屏幕截图已成为我们日常生活和工作中必不可少的一部分。无论是为了保存重要的信息、分享有趣的内容&#xff0c;还是为了制作教程和演示文稿&#xff0c;一款优秀的屏幕截图软件都能极大地提升我们的效率。而在所有的屏幕截图软件中&#xff0c;…

开源python双屏图片浏览器软件

源代码 需要安装pyqt5这个库 # -*- coding: utf-8 -*-from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton, QFileDialog, QAction, QSlider, QHBoxLayout, QWidget from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt, QS…

UWB高精度定位系统:引领精准定位技术的新纪元

在现代社会中&#xff0c;精准定位技术对于各行各业都至关重要。为了满足对高精度定位的需求&#xff0c;超宽带&#xff08;Ultra-Wideband, UWB&#xff09;技术应运而生。UWB高精度定位系统以其出色的定位精度和多样化的应用领域而备受关注。本文将深入探讨UWB高精度定位系统…

国庆day5---QT实现TCP服务器客户端搭建的代码,现象

ser.h #ifndef SER_H #define SER_H#include <QWidget> #include<QTcpServer> //服务器头文件 #include<QTcpSocket> //客户端头文件 #include<QMessageBox> //消息对话框 #include<QList> //链表头文件QT_BEGIN_NAMESPACE nam…

pycharm中添加固定的作者的信息

一. pycharm中添加作者信息,日期,等 如图所示 里面还可以添加一些固定的信息 #专业 计算机科学与技术 #姓名 小明

Java泛型理解

什么是泛型&#xff1f; 我们都知道 Java 中有形参和实参之分&#xff0c;是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数&#xff0c;其本身没有确定的值。在调用函数时&#xff0c;实参将赋值给形参。 而泛型是一种参数化的类型&#xff08…

【云备份】

文章目录 [toc] 1 :peach:云备份的认识:peach:1.1 :apple:功能了解:apple:1.2 :apple:实现目标:apple:1.3 :apple:服务端程序负责功能:apple:1.4 :apple:服务端功能模块划分:apple:1.5 :apple:客户端程序负责功能:apple:1.6 :apple:客户端功能模块划分:apple: 2 :peach:环境搭建…

Cocos Creator3.8 实战问题(四)巧用九宫格图像拉伸

一、为什么要使用九宫格图像拉伸 相信做过前端的同学都知道&#xff0c;ui &#xff08;图片&#xff09;资源对包体大小和内存都有非常直接的影响。 通常ui 资源都是图片&#xff0c;也是最占资源量的资源类型&#xff0c;游戏中的ui 资源还是人机交互的最重要的部分&#xff…

【Java】main方法的深入理解

目录 深入理解 main 方法 public static void main(String[] args) ​编辑示例代码&#xff1a; 编译运行&#xff08;String[] args&#xff09;&#xff1a; main 方法的注意事项 示例代码&#xff1a; 深入理解 main 方法 public static void main(String[] args) mai…

HTTP协议,请求响应

、概述 二、HTTP请求协议 三、HTTP响应协议 四、请求数据 1.简单实体参数 RequestMapping("/simpleParam")public String simpleParam(RequestParam(name "name" ,required false ) String username, Integer age){System.out.println (username "…

基于Java的火车高铁订票购票系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…