自学内容网 自学内容网

LeetCode 热题100之动态规划2

1.单词拆分

在这里插入图片描述
思路分析:动规五部曲

  • dp[i]:表示长度为i的字符串是否能被字典中的单词组成,其状态为dp[i];

  • 递推公式

    • 对于每个位置 i(1 到 s.size()),我们需要检查从 j 到 i-1 的子串 s[j…i-1] 是否在字典 wordDict 中,且前缀 s[0…j-1] 已经可以由字典中的单词拆分出来(即 dp[j] == true)。
    • 如果满足这些条件,则 dp[i] 可以被标记为 true,表示 s[0…i-1] 可以由字典中的单词拆分出来。
    • 递推公式为:dp[i] = dp[j] && (s.substr(j, i-j) in wordSet)
  • 初始化

    • dp[0] = true,完全是为了递推公式的进行,其实没有实际含义u(本题来说)
    • 其余的dp[i]都初始化为false,表示默认情况下,字符串的前缀不能由字典中的单词拆分出来。
  • 遍历顺序:可以看作背包问题,所以先遍历背包,再遍历物品

    • 外层循环遍历字符串 s 的每个位置 i,从 i = 1 到 i = s.size(),表示当前我们考虑字符串 s[0…i-1]。
    • 内层循环遍历每个可能的子串 s[j…i-1],从 j = 0 到 j = i-1,检查从 j 到 i-1 的子串是否在字典中,并且前缀 s[0…j-1] 是否可以被拆分。
    • 内层循环遍历每个可能的子串 s[j…i-1],从 j = 0 到 j = i-1,检查从 j 到 i-1 的子串是否在字典中,并且前缀 s[0…j-1] 是否可以被拆分。
  • 打印dp数组

    • dp 数组在整个过程中会逐步更新,最终我们关心的是 dp[s.size()] 的值。dp[s.size()] 表示整个字符串 s 是否能被字典中的单词拆分。
    • 如果 dp[s.size()] == true,则返回 true,表示字符串 s 可以由字典中的单词组成;否则返回 false,表示无法组成。

具体实现代码(详解版):

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        // 将字典转换为unordered_set以便快速查找
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        
        // dp[i] 表示 s[0...i-1] 是否能由字典中的单词组成
        vector<bool> dp(s.size() + 1, false);
        
        dp[0] = true;

        // 遍历字符串的每一个位置
        for (int i = 1; i <= s.size(); i++) {
            // 遍历子串 s[j...i-1]
            for (int j = 0; j < i; j++) {
                // 判断 s[j...i-1] 是否在字典中,并且 s[0...j-1] 能拆分
                string word = s.substr(j, i - j);
                if (wordSet.find(word) != wordSet.end() && dp[j]) {
                    dp[i] = true;
                }
            }
        }
        
        // 返回 dp[s.size()],即 s 是否能完全拆分
        return dp[s.size()];
    }
};

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

2.最长递增子序列

在这里插入图片描述
思路分析:动规五部曲

  • dp数组定义:dp[i] 表示以 nums[i] 为结尾的最长递增子序列的长度
    • 例如,对于数组 nums = [10, 9, 2, 5, 3, 7, 101, 18],dp[i] 存储的是以 nums[i] 结尾的递增子序列的最长长度。
    • 最终,我们的目标是找到数组中任意位置的 dp[i] 的最大值,表示整个数组的最长递增子序列
  • 递推公式
    • 对于每个元素 nums[i],我们需要遍历它之前的所有元素 nums[j](j < i),判断是否能够将 nums[i] 追加到 nums[j] 形成一个更长的递增子序列。如果满足 nums[i] > nums[j],那么就更新 dp[i] 的值:dp[i] = max(dp[i], dp[j] + 1);
    • 这里的含义是:如果 nums[i] 比 nums[j] 大,那么 nums[i] 可以接在以 nums[j] 结尾的递增子序列后面形成一个新的递增子序列,dp[i] 就是 dp[j] + 1 的最大值。
  • 初始化方式
    • 初始化 dp 数组时,每个位置的值都设为 1,因为每个元素至少能构成一个长度为 1 的递增子序列(即该元素本身)。
    • 初始时,dp[0] = 1,表示以第一个元素为结尾的最长递增子序列长度为 1
  • 遍历顺序
    • 外层循环遍历 i,从 i = 1 到 n - 1,表示当前我们考虑的元素是 nums[i],即以 nums[i] 为结尾的递增子序列。
    • 内层循环遍历 j,从 j = 0 到 i - 1,表示考虑 nums[i] 是否可以加到 nums[j] 后形成更长的递增子序列。
    • 如果 nums[i] > nums[j],我们就可以更新 dp[i]。
  • 打印dp数组
    • 在循环的过程中,dp[i] 会被逐渐更新,最终我们关心的是 dp 数组中的最大值,不是想当然的dp[n - 1],(因为最长的不一定就是以最后一个元素结尾的)它表示整个数组的最长递增子序列的长度。最终返回的 res 就是 dp 数组中的最大值,即最长递增子序列的长度。

具体实现代码(详解版):

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        int res = 1;  // 存储最终的结果,初始为1,表示最小的递增子序列长度

        // dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
        vector<int> dp(n + 1, 1);  // 初始化 dp 数组为 1

        for (int i = 1; i < n; i++) {  // 外层循环遍历每个元素
            for (int j = 0; j < i; j++) {  // 内层循环遍历元素 nums[i] 之前的所有元素
                if (nums[i] > nums[j]) {  // 如果 nums[i] > nums[j],说明可以扩展
                    dp[i] = max(dp[i], dp[j] + 1);  // 更新 dp[i],取最大值
                }
            }
            res = max(res, dp[i]);  // 更新结果,取所有 dp[i] 中的最大值
        }

        return res;  // 返回最长递增子序列的长度
    }
};

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

3.乘积最大子数组

在这里插入图片描述
思路分析:动规五部曲

  • dp数组定义
    • dp_max[i]:表示以 nums[i] 结尾的子数组的最大乘积。
    • dp_min[i]:表示以 nums[i] 结尾的子数组的最小乘积。
  • 递推公式
    • 对于每个元素 nums[i],它的最大乘积 dp_max[i] 和最小乘积 dp_min[i] 可以通过以下方式得到:
      • 当前元素 nums[i] 本身;
      • 当前元素与前一个子数组的最大乘积 dp_max[i-1] * nums[i];
      • 当前元素与前一个子数组的最小乘积 dp_min[i-1] * nums[i]。
    • dp_max[i] = max(nums[i], dp_max[i - 1] * nums[i], dp_min[i - 1] * nums[i]);
      dp_min[i] = min(nums[i], dp_max[i - 1] * nums[i], dp_min[i - 1] * nums[i]);
    • 这里需要注意的是,负数的处理。对于负数,如果 dp_max[i-1] 是正数,乘上负数可能会得到负值,因此我们交换 dp_max[i-1] 和 dp_min[i-1] 的值,保证最大乘积和最小乘积正确更新。
  • 初始化方式
    • dp_max[0] = nums[0] 和 dp_min[0] = nums[0],即从第一个元素开始,最大和最小乘积就是该元素本身。
  • 遍历方式
    • 从 i = 1 开始遍历数组,更新每个位置的最大和最小乘积。
  • 打印dp数组
    • 返回最大值即可。res = max(res,dp_max[i]);

具体实现代码(详解版):

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int n = nums.size();
        
        // 定义两个dp数组,dp_max和dp_min分别记录当前位置的最大乘积和最小乘积
        vector<int> dp_max(n, 0);
        vector<int> dp_min(n, 0);
        
        // 初始化第一个元素
        dp_max[0] = nums[0];
        dp_min[0] = nums[0];
        int res = nums[0];  // 记录最大乘积
        
        // 从第二个元素开始遍历
        for (int i = 1; i < n; ++i) {
            // 如果当前元素为负数,则交换最大和最小乘积
            if (nums[i] < 0) {
                swap(dp_max[i - 1], dp_min[i - 1]);
            }
            
            // 更新最大最小乘积
            dp_max[i] = max(nums[i], dp_max[i - 1] * nums[i]);
            dp_min[i] = min(nums[i], dp_min[i - 1] * nums[i]);
            
            // 更新最终结果
            res = max(res, dp_max[i]);
        }
        
        return res;
    }
};

可以注意到 dp_max[i] 和 dp_min[i] 只与前一个元素 dp_max[i-1] 和 dp_min[i-1] 有关。因此,可以将空间复杂度优化为 O(1),只使用常量的空间来存储前一个元素的最大和最小乘积。

具体实现代码(详解版):

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int n = nums.size();
        
        // 用来记录前一个位置的最大和最小乘积
        int dp_max = nums[0], dp_min = nums[0];
        int res = nums[0];  // 记录最大乘积
        
        for (int i = 1; i < n; ++i) {
            if (nums[i] < 0) {
                swap(dp_max, dp_min);  // 如果当前元素为负数,交换最大和最小乘积
            }
            
            // 更新当前的最大和最小乘积
            dp_max = max(nums[i], dp_max * nums[i]);
            dp_min = min(nums[i], dp_min * nums[i]);
            
            // 更新结果
            res = max(res, dp_max);
        }
        
        return res;
    }
};

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

4.分割等和子集

在这里插入图片描述
思路分析:这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。要明确本题中我们要使用的是01背包,因为元素我们只能用一次。

  • dp数组的定义:dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。本题中每一个元素的数值既是重量,也是价值。
  • 递推公式:相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。
    所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
  • 初始化
    • 从dp[j]的定义来看,首先dp[0]一定是0。
    • 本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。
    • 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200;总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了。所以vector dp(10001, 0);
  • 遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!保证物品不重复使用
  • 举例推导dp数组
    • 如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。

具体实现代码(详解版):

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;

        // 计算数组中所有元素的总和
        for(int i = 0; i < nums.size(); i++) {
            sum += nums[i];
        }

        // 如果总和是奇数,不可能将其分割成相等的两个子集,直接返回 false
        if (sum % 2) return false;

        // 初始化 dp 数组,dp[j] 表示是否可以得到和为 j 的子集
        vector<int> dp(10001, 0);

        // 初始化 dp[0] = 0,表示子集和为 0 是可能的(即为空集)
        dp[0] = 0;

        // 遍历数组中的每一个数(物品),每次尝试更新 dp 数组
        for (int i = 0; i < nums.size(); i++) { // 物品
            // 遍历容量从 sum / 2 到 nums[i] 的值,确保每个数只被使用一次(从后往前更新)
            for (int j = sum / 2; j >= nums[i]; j--) { // 背包
                // 更新 dp[j],选或不选 nums[i],取其中的最大值
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }

        // 如果 dp[sum / 2] 的值等于 sum / 2,则表示可以找到一个子集使其和等于 sum / 2
        return dp[sum / 2] == sum / 2;
    }
};

5.最长有效括号

在这里插入图片描述

这道题很难,浅浅记录一下给的思路
思路分析:动规五部曲

  • dp数组定义:dp[i] 表示以索引 i 为结尾的最长有效括号子串的长度。
  • 递推公式
    • 如果 s[i] == ‘(’:这是一个左括号,不能直接形成有效括号对子串。因此 dp[i] 不需要更新(默认是 0),继续处理下一个字符。
    • 如果 s[i] == ‘)’:这是一个右括号,我们需要判断它能否与前面的字符构成一个有效括号对。
      • s[i-1] == ‘(’
        • 如果 s[i-1] == ‘(’,那么 s[i-1] 和 s[i] 就构成了一个有效括号对子串,即 ()。
        • 此时,dp[i] 可以由 dp[i-2] 来推导出来(即:dp[i] = dp[i-2] + 2)。
          • dp[i-2] 表示在 i-2 位置之前的最长有效括号子串长度,而 +2 是因为当前 () 这一对括号增加了 2 的长度。
      • s[i-1] == ')
        • 如果 s[i-1] == ‘)’,我们需要进一步判断是否可以形成一个更长的有效括号子串。例如,s[i] 可能会与前面某个有效括号对子串形成一个新的有效括号对子串。
        • 假设 dp[i-1] 是以 i-1 为结尾的有效括号子串的长度。我们首先检查 s[i - dp[i-1] - 1] 是否是 ‘(’,如果是,那么可以通过扩展 dp[i-1] 来得到新的有效括号子串。
          • 如果 s[i - dp[i-1] - 1] == ‘(’,那么 dp[i] 就应该更新为:dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2],即:
            • dp[i-1] + 2 是当前的 ) 和前面的有效括号对子串 () 合起来形成的新的有效括号子串。
            • dp[i - dp[i-1] - 2] 是在 i - dp[i-1] - 2 位置之前的最长有效括号子串(如果有的话)。
  • 初始化:dp 数组大小为 n,初始化为 0。maxLength 用于记录最长有效括号子串的长度。
  • 遍历顺序:从左往右一次遍历字符
  • 打印dp:最后返回maxLength = max(maxLength, dp[i]);

具体实现代码(详解版):

class Solution {
public:
    int longestValidParentheses(string s) {
        int n = s.size();
        vector<int> dp(n, 0);  // dp[i] 表示以第 i 个字符为结尾的最长有效括号子串的长度
        int maxLength = 0;  // 记录最大有效括号子串的长度

        for (int i = 1; i < n; ++i) {
            if (s[i] == ')') {
                // 如果当前字符是 ')'
                if (i - 1 >= 0 && s[i - 1] == '(') {
                    // 如果前一个字符是 '(',则构成一个有效的括号对
                    dp[i] = (i - 2 >= 0 ? dp[i - 2] : 0) + 2;
                } else if (i - 1 >= 0 && s[i - 1] == ')') {
                    // 如果前一个字符是 ')',我们需要判断 dp[i - 1] 的有效性
                    // 需要检查 s[i - dp[i - 1] - 1] 是否是 '(',如果是,则可以构成一个有效括号对
                    if (i - dp[i - 1] - 1 >= 0 && s[i - dp[i - 1] - 1] == '(') {
                        dp[i] = dp[i - 1] + 2 + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0);
                    }
                }
                // 更新最大长度
                maxLength = max(maxLength, dp[i]);
            }
        }

        return maxLength;
    }
};

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

原文地址:https://blog.csdn.net/qq_43060884/article/details/143680468

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!