codetop标签动态规划大全C++讲解(上)!!动态规划刷穿地心!!学吐了家人们o(╥﹏╥)o

news2024/11/23 16:56:12

主要供自己回顾学习,会持续更新,题源codetop动态规划+近半年

  • 1.零钱兑换
  • 2.零钱兑换II
  • 3.面试题08.11.硬币
  • 4.单词拆分
  • 5.最长递增子序列
  • 6.最长递增子序列的个数
  • 7.得到山形数组的最少删除次数
  • 8.最长公共子序列
  • 9.最长重复子数组
  • 10.最长等差数列
  • 11.最大子数组和
  • 12.最长的斐波那契子序列的长度
  • 13.最大正方形
  • 14.最长有效括号
  • 15.乘积最大子数组
  • 16.可被三整除的最大和
  • 17.回文子串数目
  • 18.最长回文子序列
  • 19.最长回文子串
  • 20.买卖股票的最佳时机
  • 21.买卖股票的最佳时机含手续费
  • 22.买卖股票的最佳时机III
  • 23.买卖股票的最佳时机IV
  • 24.打家劫舍
  • 25.打家劫舍II
  • 26.不同路径
  • 27.不同路径II
  • 28.最小路径和
  • 29.三角形的最小路径和
  • 30.两个字符串的删除操作
  • 31.编辑距离
  • 32.一和零
  • 33.目标和
  • 34.分割等和子集
  • 35.完全平方数
  • 36.比特位计数
  • 37.石子游戏
  • 38.预测赢家
  • 39.不同的二叉搜索树
  • 40.解码方法
  • 41.鸡蛋掉落
  • 42.正则表达式匹配
  • 43.通配符匹配
  • 44.交错字符串

1.零钱兑换

给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

dp[n]的意义为凑成总金额n,最少需要dp[n]枚硬币
amount怎么来的?撤回一个硬币+1来的,dp[i] = min(dp[i], dp[i - coin] + 1),其中是coin的原因是,到底撤回面值多少的硬币,还需要穷举
另外,要取min所以dp应该初始化为一个较大值,不能是INT_MAX,因为递推式是dp+1,会溢出

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, amount + 1);
        dp[0] = 0;
        for(int i = 0; i <= amount; i ++){
            for(int coin : coins){
                if(i - coin < 0) continue;
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }
        return dp[amount] == amount + 1 ? -1 : dp[amount];
    }
};

还有一种视作完全背包的写法,一样的初始化和递推,将硬币视为物品,面值视为重量和价值,背包容量是amount
其中dp[j - coins[i]] != INT_MAX必不可少,因为如果其为INT_MAX的话,后序计算dp会+1,就会溢出

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);
        dp[0] = 0;
        for(int i = 0; i < coins.size(); i ++){//遍历物品
            for(int j = coins[i]; j <= amount; j ++){//遍历背包
                if(dp[j - coins[i]] != INT_MAX) dp[j] = min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        if(dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

2.零钱兑换II

和零钱兑换一样,不一样的是这里要求返回硬币组合数。
可以视作完全背包做

如果求组合数就是外层for循环遍历物品,内层for遍历背包
如果求排列数就是外层for遍历背包,内层for循环遍历物品

这个题求的是组合,所以外层遍历背包,内存遍历物品。求组合数量的递推公式:dp[j]+=dp[j - coins[i]]
从这个递推式来看,初始化一定要有不为0的
dp[0] = 1,其他初始化为0

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        for(int i = 0; i < coins.size(); i ++){
            for(int j = coins[i]; j <= amount; j ++){
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};

3.面试题08.11.硬币

给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)

dp[i]的含义是i分有几种表示方法,由于5,1和1,5不用特别区分,所以求的是组合数,外层遍历物品,内层遍历背包容量

当前硬币有取或者不取,取得话就是dp[i - coin],不

class Solution {
public:
    const int mod = 1e9 + 7;
    int waysToChange(int n) {
        vector<int> dp(n + 1, 0);
        vector<int> coins = {1, 5, 10, 25};
        dp[0] = 1;
        for (int coin : coins) {
            for (int i = coin; i <= n; i++) {
                dp[i] = (dp[i] + dp[i - coin]) % mod;
            }
        }
        return dp[n];
    }
};

4.单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
在这里插入图片描述
单词就是物品,字符串就是背包,因为物品可以重复填背包,所以这是个完全背包问题!
dp[i]:表示下标为0~i - 1的字符串可以被拆分为一个或多个在字典中出现的单词。
如果dp[j] == true且[j,i)出现在字典中了,那么dp[i] 也为 true,这句话记住基本就做出来了
本题求的是排列数,因为applepen是apple+pen的组合而不是pen+apple的组合

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size(), false);
        unordered_set<string> word(wordDict.begin(), wordDict.end());
        dp[0] = true;
        for(int i = 1; i <= s.size(); i ++){
            for(int j = 0; j < i; j ++){
                if(dp[j] && word.find(s.substr(j, i - j)) != word.end()) dp[i] = true;
            }
        }
        return dp[s.size()];
    }
};

5.最长递增子序列

输入一个无序的整数数组,请你找到其中最长的严格递增子序列的长度。
注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。

dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
那么base case就是dp[0] = 1
以下是完整代码,其中res初始化为 1 至关重要。
如果 res 初始化为 0,而数组是递减的,那么 res 将永远保持 0,因为没有任何 dp[j] 会大于 1,所以内部循环的条件 if(res < dp[j]) 永远不会满足。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        //dp[i]是到nums[i]截止的
        vector<int> dp(nums.size() + 1, 1);
        int result = 1;
        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(result < dp[i]) result = dp[i];
            }
        }
        return result;
    }
};

6.最长递增子序列的个数

给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。

dp[i]:最长递增序列的长度
cnt[i]:最长递增序列的个数
设nums的最长上升子序列的长度为maxLen,那么答案为所有满足dp[i] = maxLen的 i 对应的cnt[i]之和
我们从小到大计算 dp数组的值,在计算 dp[i]之前,我们已经计算出 dp[0…i−1]的值,则状态转移方程为:
dp[i]=max⁡(dp[j])+1,其中 0≤j<i 且 num[j]<num[i]
即考虑往 dp[0…i−1]中最长的上升子序列后面再加一个 nums[i]。
由于 dp[j] 代表 nums[0…j] 中以 nums[j] 结尾的最长上升子序列,所以如果能从 dp[j] 这个状态转移过来,那么 nums[i] 必然要大于 nums[j],才能将 nums[i] 放在 nums[j] 后面以形成更长的上升子序列。

对于 cnt[i],其等于所有满足 dp[j] + 1 = dp[i] 的 cnt[j]之和。在代码实现时,我们可以在计算 dp[i]的同时统计 cnt[i]的值。

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n), cnt(n);
        int ans = 0, maxLen = 0;
        for(int i = 0; i < n; ++i) {
            dp[i] = 1; // 初始化为1
            cnt[i] = 1; // 初始化为1
            for(int j = 0; j < i; ++j) {
                if(nums[i] > nums[j]) {
                    if(dp[j] + 1 > dp[i]) {  //相当于 dp[i] = max(dp[j]+1, dp[i]);
                        dp[i] = dp[j] + 1;
                        cnt[i] = cnt[j];
                    }else if(dp[j] + 1 == dp[i]){ //dp[j]+1 == dp[i] 有相同的长度
                        cnt[i] += cnt[j];
                    }
                }
            }
            if(dp[i] > maxLen) {
                maxLen = dp[i];
                ans = cnt[i];
            } else if(dp[i] == maxLen) {
                 ans += cnt[i];
            }
        }
        cout << ans << endl;
        return ans;
    }
};

7.得到山形数组的最少删除次数

我们定义 arr 是 山形数组 当且仅当它满足:

  • arr.length >= 3
  • 存在某个下标 i (从 0 开始) 满足 0 < i < arr.length - 1 且:

arr[0] < arr[1] < … < arr[i - 1] < arr[i]
arr[i] > arr[i + 1] > … > arr[arr.length - 1]

给你整数数组 nums​ ,请你返回将 nums 变成 山形状数组 的​ 最少 删除次数。

可以两部分处理这个问题:

  1. 寻找所有可能的递增序列的最长长度。
  2. 寻找所有可能的递减序列的最长长度。

1可以使用类似于最长递增子序列(LIS)的算法来实现。
2可以使用类似于最长递减子序列(LDS)的算法来实现。
对于每个位置 i,可以计算它作为山峰的情况下的最长山形子序列的长度,并求取最大的长度。然后,从总长度中减去这个最大长度就是最少删除次数。

class Solution {
public:
    int minimumMountainRemovals(vector<int>& nums) {
        int n = nums.size();
        if(n < 3) return 0;
        //记录上升子序列
        vector<int> dp(n, 1);
        for(int j = 1; j < n; j ++){
            for(int i = 0; i < j; i ++){
                if(nums[j] > nums[i]) dp[j] = max(dp[j], dp[i] + 1);
            }
        }
        //记录下降子序列
        vector<int> dp2(n, 1);
        for(int j = n - 2; j >= 0; j --){
            for(int i = n - 1; i > j; i --){
                if(nums[j] > nums[i]) dp2[j] = max(dp2[j], dp2[i] + 1);
            }
        }
        //找到了最长的山行子序列的长度
        int maxLen = 0;
        //以中心为基准
        for(int i = 1; i < n - 1; i ++){
            if(dp[i] > 1 && dp2[i] > 1) maxLen = max(maxLen, dp[i] + dp2[i] - 1);
        }
        return n - maxLen;
    }
};

8.最长公共子序列

text1和text2,返回最长处公共子序列的长度

dp[i][j]表示text1下标0~i-1和text2下标0 ~ j-1最长公共子序列的长度
所以dp的长度需要是len+1
text1[i - 1] == text2[j - 1],dp[i][j] = dp[i - 1][j - 1] + 1
text1[i - 1] != text2[j - 1],dp[i][j] = max(dp[i][j - 1],dp[i - 1][j])

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int len1 = text1.size();
        int len2 = text2.size();
        vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
        int res = 0;
        for(int i = 1; i <= len1; i ++){
            for(int j = 1; j <= len2; j ++){
                if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);

                if(res < dp[i][j]) res = dp[i][j];
            }
        }
        return res;

    }
};

9.最长重复子数组

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。

dp[i][j] 的含义是,nums1以下标i - 1结尾的子数组和nums2以下标j - 1结尾的子数组重复的最长子数组长度

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int len1 = nums1.size();
        int len2 = nums2.size();
        int result = 0;
        vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
        for(int i = 1; i <= len1; i ++){
            for(int j = 1; j <= len2; j ++){
                if(nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                if(result < dp[i][j]) result = dp[i][j];
            }
        }
        return result;
    }
};

10.最长等差数列

给你一个整数数组 nums,返回 nums 中最长等差子序列的长度。

dp[i][j]:以nums[i]结尾的子序列中,差值是j的最长等差数列长度
nums[i] - nums[j] = diff
dp[i][diff] = dp[j][diff] + 1
res实时变化

另外,由于0 <= nums[i] <= 500,所以diff在-500到500之间,又由于索引不可为负数,所以diff应该加500,变成0到1000间

class Solution {
public:
    int longestArithSeqLength(vector<int>& nums) {
        int n = nums.size();
        vector<vector<int>> dp(n, vector<int>(1001, 1));
        int res = dp[0][0];
        for(int i = 0; i < n; i ++){
            for(int j = 0; j < i; j ++){
                int diff = nums[i] - nums[j] + 500;
                dp[i][diff] = dp[j][diff] + 1;

                res = max(res, dp[i][diff]);
            }
        }
        return res;
    }
};

11.最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。

注意初始化,res需要初始为dp[0]

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        int res = dp[0];
        if(nums.size() <= 1) return dp[0];
        for(int i = 1; i < nums.size(); i ++){
            dp[i] = max(nums[i], dp[i - 1] + nums[i]);
            if(dp[i] > res) res = dp[i];
        }
        return res;
    }
};

12.最长的斐波那契子序列的长度

如果序列 X_1, X_2, …, X_n 满足下列条件,就说它是 斐波那契式 的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}

给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
eg:
输入: arr = [1,2,3,4,5,6,7,8]
输出: 5
解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
dp[i][j] 表示以 arr[i] 和 arr[j] 作为最后两个数字的最长斐波那契式子序列的长度。

class Solution {
public:
    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();
        unordered_map<int, int> indexMap;
        for (int i = 0; i < n; i++) {
            indexMap[arr[i]] = i;
        }

        vector<vector<int>> dp(n, vector<int>(n, 2));
        int maxLen = 0;

        for (int j = 1; j < n; j++) {
            for (int i = 0; i < j; i++) {
                int k = indexMap.find(arr[j] - arr[i]) != indexMap.end() ? indexMap[arr[j] - arr[i]] : -1;
                if (k >= 0 && k < i) {
                    dp[i][j] = dp[k][i] + 1;
                    maxLen = max(maxLen, dp[i][j]);
                }
            }
        }

        return maxLen > 2 ? maxLen : 0;
    }
};

13.最大正方形

在一个由0和1组成的二维矩阵内,找到只包含1的最大正方形,并返回其面积

前缀和做法:
和美团2024春招第一题平衡矩阵很像,给一个和平衡矩阵一样的做法
需要注意的是,在小美的平衡矩阵中,输入数组都是下标1开始,这里的matrix是从0开始的,而前缀和又必须从1开始,所以前缀和s要加的matrix[i - 1][j - 1] - ‘0’
左上角i,j的起始,已经是在前缀和s里面看了,所以从1开始而不是0

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        if (matrix.empty()) return 0;
        int m = matrix.size(), n = matrix[0].size();
        vector<vector<int>> s(m+1, vector<int>(n+1, 0));
        int res = 0;
        
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                s[i][j] = (matrix[i-1][j-1] - '0') + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
            }
        }
        
        // i 和 j 是左上角, l 和 r 是右下角
        // len 是正方形边长
        for (int len = 1; len <= min(m, n); len++) {
            for (int i = 1; i <= m - len + 1; i++) {
                for (int j = 1; j <= n - len + 1; j++) {
                    int l = i + len - 1;
                    int r = j + len - 1;
                    int total = s[l][r] - s[l][j - 1] - s[i - 1][r] + s[i - 1][j - 1];
                    if (total == len * len) res = max(res, len * len);
                }
            }
        }
        return res;
    }
};

动态规划做法:

  1. dp[i][j] 代表(i,j)为右下角,且只包含1的正方形的边长最大值。
  2. 递推公式:
  • 如果该位置是0,则dp[i][j] = 0,因为当前位置不可能由1组成的正方形中
  • 如果该位置是1,则dp[i][j]的值由其上方、左方和左上方的三个相邻位置的dp值决定。具体而言,当前位置的元素值等于三个相邻位置的元素中的最小值加 1,状态转移方程如下:dp(i, j)=min(dp(i−1, j), dp(i−1, j−1), dp(i, j−1))+1
  1. 如果 i 和 j 中至少有一个为0,则位置(i,j)为右下角的最大正方形的边长只能是1,dp[i][j] = 1
class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        if(matrix.empty()) return 0;
        int m = matrix.size();
        int n = matrix[0].size();
        vector<vector<int>> dp(m, vector<int>(n, 0));
        int len = 0;
        for(int i = 0; i < m; i ++){
            for(int j = 0; j < n; j ++){
                if(matrix[i][j] == '1'){
                    if(i == 0 || j == 0) dp[i][j] = 1;
                    else dp[i][j] = min(dp[i - 1][j], min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
                    if(dp[i][j] > len) len = dp[i][j];
                }
            }
        }
        return len * len;
    }
};

14.最长有效括号

给一个字符串只包含 ‘(’ 和 ‘)’ ,找出最长有效括号子串。
在这里插入图片描述
dp[i]的含义是以下标1结尾的字符串最长有效括号子串的长度
所以上图dp[1] = 2, dp[3] = 4

  1. 如果当前遍历到了‘(’,那么一定是非法序列,dp[i] = 0
  2. 如果当前遍历到了‘)’,那么分2种情况
  • )的前面是(,那么dp[i] = dp[i - 2] + 2
  • )的前面是),那么需要检查i - dp[i - 1] - 1,即前一个合法序列的前一个位置是不是左括号,类似于图中的dp[7],index = 7 的时候,此时 index - 1 也是右括号,我们需要知道 i - dp[i - 1] - 1 = 7 - dp [ 6 ] - 1 = 4 位置的括号的情况。而刚好 index = 4 的位置是左括号,此时 dp [ i ] = dp [ i - 1 ] + dp [ i - dp [ i - 1] - 2 ] + 2 (当前位置的前一个合法序列的长度,加上匹配的左括号前边的合法序列的长度,加上新增的长度 2)
class Solution {
public:
    int longestValidParentheses(string s) {
        vector<int> dp(s.size(), 0);
        int res = 0;
        for(int i = 1; i < s.size(); i ++){
            if(s[i] == ')'){
                if(s[i - 1] == '('){
                    dp[i] = (i - 2 >= 0 ? dp[i - 2] : 0) + 2;
                }else if(i > 0 && i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] == '('){
                    dp[i] = dp[i - 1] + (i - dp[i - 1] >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
                }
            }
            res = max(res, dp[i]);
        }
        return res;
    }
};

15.乘积最大子数组

给你一个整数数组nums,请你找出数组中乘积最大的非空连续子数组,并将乘积返回
dpMax[i] 表示以第 i 个元素的结尾的子数组,乘积最大的值

  1. 当nums[i] >= 0 并且dpMax[i - 1] > 0,dpMax[i] = dpMax[i - 1] * nums[i]
  2. 当nums[i] >= 0 并且dpMax[i - 1] < 0,dpMax[i] = nums[i]
  3. 当nums[i] < 0时,dpMax需要分情况讨论
  • 当dpMin[i - 1] < 0,dpMax[i] = dpMin[i - 1] * nums[i]
  • 当dpMin[i - 1] >= 0,dpMax[i] = nums[i]

上面引入了dpMin数组,怎么求dpMin和dpMax其实是一样的。
首先
dpMax[i] = max(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i]);
求 dpMin[i] 同理
dpMin[i] = min(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i], nums[i]);
由于都是上一个dpMax或者上一个dpMin推导的,所以可以不设数组
dpMax = max(dpMin * nums[i], max(dpMax * nums[i], (double)nums[i]));
dpMin = min(dpMin * nums[i], min(preMax * nums[i], (double)nums[i]));
如果是int的话会有一个测试用例过不了

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) {
            return 0;
        }
        double dpMax = nums[0];
        double dpMin = nums[0];
        double maxProd = nums[0];
        for (int i = 1; i < n; i++) {
        	// 更新dpMin的时候需要dpMax之前的信息,所以先保存起来
            double preMax = dpMax;
            dpMax = max(dpMin * nums[i], max(dpMax * nums[i], (double)nums[i]));
            dpMin = min(dpMin * nums[i], min(preMax * nums[i], (double)nums[i]));
            maxProd = max(maxProd, dpMax);
        }
        return (int)maxProd; // 将结果转换回int,假设最终结果适合int类型。
    }
};

16.可被三整除的最大和

给你一个整数数组 nums,请你找出并返回能被三整除的元素 最大和。

dp[i][j]:处理前i个元素,余数为j的最大和。j有三种可能0,1,2按照定义,最终答案就是dp[n][0]
对于nums[i - 1],可选可不选

  1. 如果nums[i - 1]%3 = 0,那么说明加入当前nums[i - 1]不会改变其余数,所以:
  • dp[i][0] = max{dp[i - 1][0],dp[i - 1][0] + nums[i - 1]}
  • dp[i][1] = max{dp[i - 1][1],dp[i - 1][1] + nums[i - 1]}
  • dp[i][2] = max{dp[i - 1][2],dp[i - 1][2] + nums[i - 1]}
  1. 如果nums[i - 1]%3 = 1,那么说明加入当前nums[i - 1]之后,原来余数0会变成1,1变成2,2变成0
  • dp[i][0] = max{dp[i - 1][0],dp[i - 1][2] + nums[i - 1]}
  • dp[i][1] = max{dp[i - 1][1],dp[i - 1][0] + nums[i - 1]}
  • dp[i][2] = max{dp[i - 1][2],dp[i - 1][1] + nums[i - 1]}
  1. 如果nums[i − 1]%3 = 2,那么说明加入当前nums[ i − 1]之后,原来的余数0会变成2,1变成 0,2变成1:
  • dp[i][0] = max{dp[i - 1][0],dp[i - 1][1] + nums[i - 1]}
  • dp[i][1] = max{dp[i - 1][1],dp[i - 1][2] + nums[i - 1]}
  • dp[i][2] = max{dp[i - 1][2],dp[i - 1][0] + nums[i - 1]}
class Solution {
public:
    int maxSumDivThree(vector<int>& nums) {
        int n = nums.size();
        int dp[n + 1][3];// dp[i][j]代表下标0~i-1的数中,÷3余数为j的最大和

        memset(dp,0,sizeof dp);
        dp[0][1] = -1e9 , dp[0][2] = -1e9;// dp[0][0]是前0个数。÷3余数为0的最大和,是0

        for(int i = 1;i <= n;i++){
            int r = nums[i - 1] % 3;
            if(r == 0){
                dp[i][0] = max(dp[i - 1][0] , dp[i - 1][0] + nums[i - 1]);
                dp[i][1] = max(dp[i - 1][1] , dp[i - 1][1] + nums[i - 1]);
                dp[i][2] = max(dp[i - 1][2] , dp[i - 1][2] + nums[i - 1]);
            }
            else if(r == 1){
                dp[i][0] = max(dp[i - 1][0] , dp[i - 1][2] + nums[i - 1]);
                dp[i][1] = max(dp[i - 1][1] , dp[i - 1][0] + nums[i - 1]);
                dp[i][2] = max(dp[i - 1][2] , dp[i - 1][1] + nums[i - 1]);
            }
            else{
                dp[i][0] = max(dp[i - 1][0] , dp[i - 1][1] + nums[i - 1]);
                dp[i][1] = max(dp[i - 1][1] , dp[i - 1][2] + nums[i - 1]);
                dp[i][2] = max(dp[i - 1][2] , dp[i - 1][0] + nums[i - 1]);
            }
        }
        return dp[n][0];
    }
};

17.回文子串数目

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

如果设置 dp[i] 的意义是指以下标 i 结尾的字符串有 dp[i] 个回文串,那就会很难想,因为dp[i] 和 dp[i-1] ,dp[i + 1] 看上去都没啥关系
所以应该这样设置:bool类型dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
假设s[i] != s[j],不是回文子串
s[i] == s[j],j - i <= 1,是回文串(a或aa的情况),如果 i 和 j 差距大于1,就撤回一步看dp[i + 1][j - 1]是不是回文子串

注意初始化,第二个for的j要从i开始不是i+1,差一步都不行

class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int res = 0;
        for(int i = s.size() - 1; i >= 0; i --){
            for(int j = i; j < s.size(); j ++){
                if(s[i] == s[j]){
                    if(j - i <= 1){
                        res++;
                        dp[i][j] = true;
                    }else{
                        if(dp[i + 1][j - 1]){
                            res++;
                            dp[i][j] = true;
                        }
                    }
                }
            }
        }
        return res;
    }
};

18.最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。
子序列,所以可以不连续

大致逻辑和3一样
在s[i] == s[j]时,dp[i][j] = dp[i + 1][j - 1] + 2
在s[i] != s[j]时,加入s[j]的回文子序列长度为dp[i + 1][j]。加入s[i]的回文子序列长度为dp[i][j - 1]。

从递推式可以看出来,i == j的情况没有覆盖,所以需要初始化为1

另外加入s长度只有一个的话,无法进入for循环,这个时候应该输出1,所以res也需要初始化为1

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        int res = 1;
        for(int i = 0; i < s.size(); i ++) dp[i][i] = 1;
        for(int i = s.size() - 1; i >= 0; i --){
            for(int j = i + 1; j < s.size(); j ++){
                if(s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
                else dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

                if(dp[i][j] > res) res = dp[i][j];
            }
        }
        return res;
    }
};

19.最长回文子串

给你一个字符串 s,找到 s 中最长的 回文子串。
和3差不多

class Solution {
public:
    string longestPalindrome(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int l = 0, maxLen = -1;
        for(int i = s.size() - 1; i >= 0; i --){
            for(int j = i; j < s.size(); j ++){
                if(s[i] == s[j]){
                    if(j - i <= 1) dp[i][j] = true;
                    else if(dp[i + 1][j - 1]) dp[i][j] = true;
                }
                if(dp[i][j] && j - i + 1 > maxLen){
                    maxLen = j - i + 1;
                    l = i;
                }
            }
        }        
        return s.substr(l, maxLen);
    }
};

20.买卖股票的最佳时机

给prices数组,只能买卖一次!
0持有,1不持有
注意:只能买一次,状态不能从dp[i-1][1]转移,所以只有-prices[i],如果不限次数就可以写dp[i - 1][1] - prices[i]了!!!

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
        dp[0][0] = -prices[0];
        for(int i = 1; i < prices.size(); i ++){
            dp[i][0] = max(- prices[i], dp[i - 1][0]);
            dp[i][1] = max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
        }
        return dp[prices.size() - 1][1];
    }
};

21.买卖股票的最佳时机含手续费

无限次买卖,卖出同时需要付一笔手续费

因为是无限次买卖,所以状态从dp[i-1][1]转移,不限次数写成dp[i - 1][1] - prices[i]
买的时候减去fee

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
        //0持有,1不持有
        dp[0][0] = -prices[0];
        for(int i = 1; i < prices.size(); i ++){
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
        }
        return dp[prices.size() - 1][1];
    }
};

22.买卖股票的最佳时机III

限制最多可以完成两笔交易

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //0第一次持有,1第一次不持有,2第二次持有,3第二次不持有
        vector<vector<int>> dp(prices.size(), vector<int>(4, 0));
        dp[0][0] = -prices[0];
        dp[0][2] = -prices[0];
        for(int i = 1; i < prices.size(); i ++){
            dp[i][0] = max(dp[i - 1][0], -prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
        }
        return dp[prices.size() - 1][3];
    }
};

23.买卖股票的最佳时机IV

最多可以完成k笔交易,也就是说,最多可以买k次,卖k次

k从1开始,分奇偶

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        vector<vector<int>> dp(prices.size(), vector<int>(2*k + 1, 0));
        //奇数持有,偶数不持有
        for(int i = 1; i < 2*k + 1; i ++){
            if(i % 2 == 1) dp[0][i] = -prices[0];
        }
        for(int i = 1; i < prices.size(); i ++){
            for(int j = 1; j <= 2*k - 1; j += 2){
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] + prices[i]);
            }
        }
        return dp[prices.size() - 1][2 * k];
    }
};

24.打家劫舍

给定一个代表每个房屋存放金额的非负整数数组,相邻房屋装有相互连通的防盗系统,求最多偷的金额。
关键函数: dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);

class Solution {
public:
    int rob(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);
        if(nums.size() == 0) return 0;
        if(nums.size() == 1) return nums[0];
        if(nums.size() == 2) return max(nums[0], nums[1]);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for(int i = 2; i < nums.size(); i ++){
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[nums.size() - 1];
    }
};

25.打家劫舍II

房屋围城一圈,最后一个房屋和第一个房屋紧邻,偷相邻房间会自动报警

  1. 考虑不包含首尾元素
  2. 考虑包含首元素不包含尾元素
  3. 考虑不包含首元素包含尾元素
    考虑是包含但不一定要选,所以情况23包含了1
    简单点来说,考虑偷第一个不偷第二个和偷第二个不偷第一个的情况
    两个情况取最值,单领一个情况的计算就和I一样
class Solution {
public:
    int rob(vector<int>& nums) {
        int len = nums.size();
        if(len == 0) return 0;
        if(len == 1) return nums[0];
        if(len == 2) return max(nums[0], nums[1]);
        int result1 = robRange(nums, 0, len - 2);
        int result2 = robRange(nums, 1, len - 1);
        int result = max(result1, result2);
        return result;
    }
    int robRange(vector<int>& nums, int start, int end){
        vector<int> dp(nums.size() + 1, 0);
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        for(int i = start + 2; i <= end; i ++){
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[end];
    }
};

26.不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
关键代码:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for(int i = 0; i < m; i ++) dp[i][0] = 1;
        for(int i = 0; i < n; i ++) dp[0][i] = 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];
    }
};

27.不同路径II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。

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, 0));
        for(int i = 0; i < m && obstacleGrid[i][0] == 0; i ++) dp[i][0] = 1;
        for(int j = 0; j < n && obstacleGrid[0][j] == 0; j ++) dp[0][j] = 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];
    }
};

28.最小路径和

给定一个包含非负整数的m * n网格grid,找出一条从左上角到右下角的路径,使得路径上的数字总和最小。

注意边界问题,第一行第一列需要初始化,所有for都是从1开始的

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        vector<vector<int>> dp(m, vector<int>(n, 0));
        dp[0][0] = grid[0][0];
        for(int i = 1; i < m; i ++) dp[i][0] = dp[i - 1][0] + grid[i][0];
        for(int j = 1; j < n; j ++) dp[0][j] = dp[0][j - 1] + grid[0][j];
        for(int i = 1; i < m; i ++){
            for(int j = 1; j < n; j ++){
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[m - 1][n - 1];
    }
};

29.三角形的最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点
在这里插入图片描述
题目的意思,在相邻节点的条件下,求最小路径,与(i, j)点相邻的结点为(i + 1, j)和 (i + 1, j + 1)

第 i 行的第 j 个元素从哪里来?可以从第 i - 1 行第 j 或第 j - 1 个元素下落过来。
dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle[i][j]

注意边界!!!dp[i][0]必须初始化,dp[0][i]不用,因为dp[0][i]一定只有一个,毕竟三个三角形

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int m = triangle.size();
        vector<vector<int>> dp(m, vector<int>(m, INT_MAX));
        dp[0][0] = triangle[0][0];
        for(int i = 1; i < m; i ++) dp[i][0] = dp[i - 1][0] + triangle[i][0];
        for(int i = 1; i < m; i ++){
            for(int j = 1; j < triangle[i].size(); j ++){
                dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle[i][j];
            }
        }
        int res = INT_MAX;
        for(int i = 0; i < m; i ++){
            res = min(res, dp[m - 1][i]);
        }
        return res;
    }
};

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

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

dp[i][j]:以i-1为结尾的字符串word1,和以j-1为结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

  1. 当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1]
  2. 当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
  • 删word1[i - 1],最少操作次数为dp[i - 1][j] + 1
  • 删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
  • 删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2
class Solution {
public:
    int minDistance(string word1, string word2) {
        int len1 = word1.size();
        int len2 = word2.size();
        vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
        for(int i = 0; i <= len1; i ++) dp[i][0] = i;
        for(int i = 0; i <= len2; i ++) dp[0][i] = i;
        for(int i = 1; i <= len1; i ++){
            for(int j = 1; j <= len2; j ++){
                if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] =  min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
            }
        }
        return dp[len1][len2];
    }
};

31.编辑距离

可以对一个单词进行增加,删除和替换,现有两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

dp[i][j]表示以下标i-1结尾的字符串word1,和以下标j-1结尾的字符串word2,最近编辑距离为dp[i][j],所以i,j最大都可以达到len

  1. word[i - 1] == word2[j - 1]的时候不操作,dp[i][j]=dp[i-1][j-1]
  2. word1[i - 1] != word2[j - 1]的时候操作,分三种情况,增删换
  • 增加/删除:word1增加一个元素,相当于word2减少一个元素。比如word1 = “a”,word2 = “ab”,word1增加一个元素b和word2删除一个元素b的操作数是一样的;word1减少一个元素,相当于word2增加一个元素,比如word1 = “ab”,word2 = “a”,word1减少一个b和word2增加一个b用的操作数是一样的。所以dp[i][j] = dp[i][j - 1] + 1
  • 替换:dp[i][j] = dp[i - 1][j - 1] + 1
class Solution {
public:
    int minDistance(string word1, string word2) {
        int len1 = word1.size();
        int len2 = word2.size();
        vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
        for(int i = 0; i <= len1; i ++) dp[i][0] = i;
        for(int i = 0; i <= len2; i ++) dp[0][i] = i;
        for(int i = 1; i <= len1; i ++){
            for(int j = 1; j <= len2; j ++){
                if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1));
            }
        }
        return dp[len1][len2];
    }
};

32.一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

背包有2个维度,m个0和n个1属于背包,strs数组是物品,由于物品只能选一次,所以这是个01背包问题
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i] [j]。
递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for(string str : strs){//遍历物品
            int zoreNums = 0;
            int oneNums = 0;
            for(int i = 0; i < str.size(); i ++){
                if(str[i] == '0') zoreNums ++;
                else oneNums ++;
            }
            for(int j1 = m; j1 >= zoreNums; j1 --){//遍历背包,两个维度
                for(int j2 = n; j2 >= oneNums; j2 --){
                    dp[j1][j2] = max(dp[j1][j2], dp[j1 - zoreNums][j2 - oneNums] + 1);
                }
            }
        }
        return dp[m][n];

    }
};

33.目标和

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

sum_pos - sum_neg = sum
sum_pos + sum_neg = target
所以:sum_pos = (target + sum) % 2
01背包问题

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i ++) sum += nums[i];
        if((target + sum) % 2 != 0) return 0;
        if(abs(target) > sum) return 0;
        int sum_pos = (target + sum) / 2;
        // 背包是sum_pos,nums中的值是物品,只能使用一次,是0-1背包
        vector<int> dp(sum_pos + 1, 0);
        dp[0] = 1;
        for(int i = 0; i < nums.size(); i ++){
            for(int j = sum_pos; j >= nums[i]; j --){
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[sum_pos];
    }
};

34.分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

一个子集的大小就除2,这就是背包容量,nums里面的元素是物品,由于每个元素只能用一次,所以是01背包

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i ++){
            sum += nums[i];
        }
        if(sum % 2 != 0) return false;
        int target = sum / 2;
        //初始化
        vector<int> dp(target + 1, 0);

        for(int i = 0; i < nums.size(); i ++){
            for(int j = target; j >= nums[i]; j --){
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        if(dp[target] == target) return true;
        return false;
    }
};

35.完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例:

  • 输入:n = 12
  • 输出:3
  • 解释:12 = 4 + 4 + 4

平方数就是物品,并且可以无限次使用,n是背包容量,现在问凑满这个背包最少需要多少物品
dp[j] = min(dp[j],dp[j - i * i] + 1)

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;
        for(int i = 1; i * i<= n; i ++){//物品
            for(int j = i * i; j <= n; j ++){//背包
                dp[j] = min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
};

36.比特位计数

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

有一个极其巧妙的做法:
偶数的二进制1个数超级简单,因为偶数相当于是被更小的某个数乘2,这个乘2在二进制中的体现就是整体左移,比如数字1的二进制是1,左移是10,就是2。所以1的个数是不变的,只是多了个0,dp[i] = dp[i / 2]
奇数就更简单了,奇数由不大于数的偶数+1,偶数+1在二进制上会发生什么?就是某位加1,所以dp[i] = dp[i / 2] + 1

class Solution {
public:
    vector<int> countBits(int n) {
        int i = 1;
        vector<int> dp(n + 1, 0);
        for(int i = 0; i <= n; i ++){
            if(i % 2 == 0) dp[i] = dp[i / 2];
            else dp[i] = dp[i - 1] + 1;
        }
        return dp;
    }
};

37.石子游戏

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行;每堆都有 正 整数颗石子,数目为 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的 总数 是 奇数 ,所以没有平局。

Alice 和 Bob 轮流进行,Alice 先开始 。 每回合,玩家从行的 开始 或 结束 处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜 。

假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false 。

用二维数组 dp[i][j] 记录 piles[i] ~ piles[j] 序列中,先选择的人可以获得比对手多的石子数量。

  1. 若选择piles[i],则对手可选的范围变为piles[i + 1] ~ piles[j],选择piles[i]后,先手变成后手,对手可选择的石子数量为dp[i + 1][j],则我们比对手多的石子的数量记为dp[i][j] = piles[i] - dp[i + 1][j]。
  2. 若选择piles[j],则对手可选的范围变为 piles[i]~piles[j-1] ,选择piles[j]后,我们变为后手,所以对手可选择的比我们多的石子的数量为 dp[i][j-1],则我们比对手多的石子的数量记为dp[i][j] = piles[j]- dp[i][j-1]。

所以 dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1])
遍历是从小范围到大范围,为了保证每次计算依赖的值已经被计算过,应该先遍历数组长度len,再遍历l和r

class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        int n = piles.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for(int i = 0; i < n; i++) {
            dp[i][i] = piles[i];
        }
        
        for (int len = 2; len <= n; len++) {
            for (int l = 0; l <= n - len; l++) {
                int r = l + len - 1;
                dp[l][r] = max(piles[l] - dp[l + 1][r], piles[r] - dp[l][r - 1]);
            }
        }

        return dp[0][n - 1] > 0;
    }
};

38.预测赢家

给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。

玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0] 或 nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。

如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。

和石子游戏区别不大

class Solution {
public:
    bool predictTheWinner(vector<int>& nums) {
        int n = nums.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for (int i = 0; i < n; i++) dp[i][i] = nums[i];
        
        for (int len = 2; len <= n; len++) {
            for (int l = 0; l <= n - len; l++) {
                int r = l + len - 1; // r是右端点
                dp[l][r] = max(nums[l] - dp[l + 1][r], nums[r] - dp[l][r - 1]);
            }
        }
        return dp[0][n - 1] >= 0;
    }
};

39.不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

dp[i]表示i个节点的二叉搜索树共有多少种
确定递推公式,先观察dp[3]怎么得出来的
在这里插入图片描述
dp[3],就是元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量

  • 元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
  • 元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
  • 元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

有2个元素的搜索树数量就是dp[2]。
有1个元素的搜索树数量就是dp[1]。
有0个元素的搜索树数量就是dp[0]。

所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

所以dp[i] += dp[j - 1] * dp[i - j]

初始化,空树也是二叉搜索树,dp[0] = 1

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        for(int i = 1; i <= n; i ++){
            for(int j = 1; j <= i; j ++){
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
};

40.解码方法

在这里插入图片描述
dp[i]表示以第i个字符为结尾,解码方法的总数

  • s[i] 和 s[i - 1] 是单独为一个字符形成两个数字,那么dp[i]的值就是dp[i-1]的值;
  • 如果 s[i] 和 s[i-1] 合并为一个字符形成为一个数字,那么 dp[i] 的值就是 dp[i-2] 的值。因为 s[i] 和 s[i-1] 都形成一个数字了,再 dp[i] 往前就是就是 dp[i-2] 了。
  • 因为单独一个0不能解码,所以当s[i]和s[i-1]是单独为一个字符时,若s[i]==‘0’,dp[i] = 0
  • 如果形成的2位数数字不在[10,26]区间内的话,所以当s[i]和s[i-1]合并为一个字符时,若超出这个范围了,dp[i] = 0

所以状态转移方程包含dp[i-1]和dp[i-2]
那么需要初始化dp[0]和dp[1]
dp[0]只需要判断他是不是0,即可得出结论
dp[1]的话有两种情况,合为一个数;分开成两个数。合为一个数需要判断形成的这一个数是否在[10, 26]这个范围里面;分别为两个数只需要判断s[0]和s[1]是不是都不为’0’。

class Solution {
public:
    int numDecodings(string s) {
        int n = s.size();
        vector<int> dp(n, 0);
        if(s[0] != '0') dp[0] = 1;
        if(n == 1) return dp[0];
        if(s[0] != '0' && s[1] != '0') dp[1] = 1;
        int count = (s[0] - '0') * 10 + (s[1] - '0');
        if(count >= 10 && count <= 26) dp[1] ++;

        for(int i = 2; i < n; i ++){
            if(s[i] != '0') dp[i] += dp[i - 1];
            int count = (s[i - 1] - '0') * 10 + (s[i] - '0');
            if(count >= 10 && count <= 26) dp[i] += dp[i-2];
        }
        return dp[n - 1];
    }
};

41.鸡蛋掉落

给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。

每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
在这里插入图片描述
没有鸡蛋个数的限制的话,肯定是二分法效率最高
有鸡蛋限制的话,先留一个鸡蛋,其他二分法或者是多分法,找到鸡蛋碎的区间,在拿最后一个鸡蛋逐步试楼层

dp[k][n] 表示有 k 个鸡蛋和 n 层楼时,最少的扔鸡蛋次数。
状态转移方程:对于每个楼层 i,我们考虑两种情况:
鸡蛋碎了,问题转化为 k-1 个鸡蛋和 i-1 层楼的问题。
鸡蛋没碎,问题转化为 k 个鸡蛋和 n-i 层楼的问题。
取两者中的最大值,然后加 1,表示在第 i 层扔鸡蛋所需的最少次数。

class Solution {
public:
    int superEggDrop(int K, int N) {
        // dp[i][j] 表示 i 个鸡蛋,j 层楼时的最少尝试次数
        vector<vector<int>> dp(K + 1, vector<int>(N + 1, 0));

        // 初始化 base case
        // 如果只有一个鸡蛋,只能线性扫描,每层楼都要试,最坏情况的次数就是最少次数
        for (int j = 1; j <= N; j++) {
            dp[1][j] = j;
        }

        for (int i = 2; i <= K; i++) {
            for (int j = 1; j <= N; j++) {
                int left = 1, right = j;
                int res = INT_MAX;

                // 二分搜索,优化线性扫描
                while (left <= right) {
                    int mid = (left + right) / 2;
                    int broken = dp[i - 1][mid - 1];
                    int notBroken = dp[i][j - mid];
                    // 在最坏情况下的最少尝试次数
                    if (broken > notBroken) {
                        right = mid - 1;
                        res = min(res, broken + 1);
                    } else {
                        left = mid + 1;
                        res = min(res, notBroken + 1);
                    }
                }
                dp[i][j] = res;
            }
        }

        // 答案在 dp[K][N] 中
        return dp[K][N];
    }
};

42.正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘※’ 的正则表达式匹配。

  • ‘.’匹配任意单个字符
  • ‘※’匹配零个或多个前面的那一个元素
    在这里插入图片描述
    s 和 p 相互匹配的过程大致是,两个指针 i, j 分别在 s 和 p 上移动,如果最后两个指针都能移动到字符串的末尾,那么就匹配成功,反之则匹配失败。
    其实.很好匹配,遇到.无脑匹配就可以了,但是 ※ 很厉害了,他可以让之前的那个字符重复任意次数,包括0次,意味着这个是空字符串也没事。“.a※b”可以匹配文本“zaaab”,也可以匹配“cb”。而模式串“ .※ ”更厉害了,他可以匹配任何文本,包括空字符串。

如果i到了s的末尾,j没有到p的末尾,这时候就要考虑p剩余的元素能否匹配空字符串。什么能匹配空字符串呢?

  • ‘.’不行,因为 ‘.’ 自身并不意味着可以选择不匹配任何字符。它总是匹配一个字符,无论是字母、数字还是符号。因此,. 不能单独用来表示“匹配空字符串”的意思。
  • ‘ * ’用于指定其前一个字符可以出现 0 次、1 次或多次。它的关键在于 * 可以让前面的字符出现次数为零,这就意味着完全可以不匹配任何字符。

现在开始一步一步写代码,如果不考虑※通配符,面对两个待匹配字符s[i]和p[j],我们唯一能做的就是考虑她俩是否匹配。

bool isMatch(string s, string p){
	int i = 0, j = 0;
	while(i < s.size() && j < p.size()){
		if(s[i] == p[j] || p[j] == '.'){
			i ++; j ++;
		}else{
			return false;
		}
	}
	return i == j;
}

如果加入 ※ 匹配符,局面就会复杂一些。
当p[j + 1] 为 ※ 通配符时,分情况讨论:

  1. 如果s[i] == p[j],那么有两种情况:
  • p[j] 有可能会匹配多个字符,比如 s = “aaa”, p = “a*”,那么 p[0] 会通过 * 匹配 3 个字符 “a”。
  • p[i] 也有可能匹配 0 个字符,比如 s = “aa”, p = “a*aa”,由于后面的字符可以匹配 s,所以 p[0] 只能匹配 0 次。
  1. 如果 s[i] != p[j],只有一种情况:
  • p[j] 只能匹配 0 次,然后看下一个字符是否能和 s[i] 匹配。比如说 s = “aa”, p = “b*aa”,此时 p[0] 只能匹配 0 次。
if(s[i] == p[j] || p[j] == '.'){
	//匹配
	if(j < p.size() - 1 && p[j + 1] == '*'){
		// 有 * 通配符,可以匹配 0 次或多次
	} else {
		// 无 * 通配符,老老实实匹配 1 次
		i ++; j ++;
	}
}else{
	// 不匹配
	if(j < p.size() - 1 && p[j + 1] == '*'){
		// 有 * 通配符,只能匹配0
	}else{
		// 无 * 通配符,匹配无法进行下去了
		return false;
	}
}

定义dp函数bool dp(string s, int i, string p, int j)
若 dp(s, i, p, j) = true,则表示 s[i…] 可以匹配 p[j…];若 dp(s, i, p, j) = false,则表示 s[i…] 无法匹配 p[j…]。

根据这个定义,我们想要的答案就是 i = 0, j = 0 时 dp 函数的结果:

bool dp(string s, int i, string p, int j) {
    if (s[i] == p[j] || p[j] == '.') {
        // 匹配
        if (j < p.size() - 1 && p[j + 1] == '*') {
            // 1.1 通配符匹配 0 次或多次
            // 先执行第一个再执行第二个
            return dp(s, i, p, j + 2) || dp(s, i + 1, p, j);
        } else {
            // 1.2 常规匹配 1 次
            return dp(s, i + 1, p, j + 1);
        }
    } else {
        // 不匹配
        if (j < p.size() - 1 && p[j + 1] == '*') {
            // 2.1 通配符匹配 0 次
            return dp(s, i, p, j + 2);
        } else {
            // 2.2 无法继续匹配
            return false;
        }
    }
}

解析:

  1. 通配符匹配0次或多次dp(s, i, p, j + 2)
    将j加2,i不变,含义就是直接跳过p[j]和之后的通配符,即通配符匹配0次。
    即使s[i] == p[j],依然可能出现匹配0次这种情况,如下图:
    在这里插入图片描述
  2. 匹配多次的情况dp(s, i + 1, p, j)
    将i加1,j不变,含义就是p[j]匹配了s[i],但p[j]还可以继续匹配,即通配符匹配多次的情况。
    在这里插入图片描述
  3. 常规匹配1次。无 * 的常规匹配。如果 s[i] == p[j],就是 i 和 j 分别加一。
  4. 不等。通配符匹配0次。将j加2,i不变。
    在这里插入图片描述
  5. 不等且无*,匹配失败
    完整代码:

考这个我就去死(╯>д<)╯⁽˙³˙⁾

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size();
        int n = p.size();
        // dp[i][j] 表示 s 的前 i 个字符和 p 的前 j 个字符是否匹配
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        // 空字符串和空模式是匹配的
        dp[0][0] = true;

        // 处理 p 开头是 '*' 的情况
        for (int j = 1; j < n; j += 2) {
            if (p[j] == '*') {
                dp[0][j + 1] = dp[0][j - 1];
            } else {
                break;
            }
        }

        // 填充 dp 表格
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p[j - 1] == '*') {
                    // '*' 可以匹配零个或多个字符
                    dp[i][j] = dp[i][j - 2] || (dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'));
                } else {
                    // 逐字符匹配,或者 '.' 可以匹配任意字符
                    dp[i][j] = dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.');
                }
            }
        }

        // 最终结果
        return dp[m][n];
    }
};

43.通配符匹配

给你一个输入字符串 (s) 和一个字符模式 ( p) ,请你实现一个支持 ’ ? ’ 和 ’ * ’ 匹配规则的通配符匹配:

  • ’ ? ’ 可以匹配任何单个字符。
  • ’ * ’ 可以匹配任意字符序列(包括空字符序列)。

判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。
在这里插入图片描述
唯一需要注意的是本题的测试用例可能出现很多 * 连续出现的情况,很容易看出连续多个 * 和一个 * 的通配效果是一样的,所以我们可以提前删除连续的 * 以便提升一些效率。

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size();
        int n = p.size();

        // dp[i][j] 表示 s 的前 i 个字符和 p 的前 j 个字符是否匹配
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));

        // 空字符串和空模式是匹配的
        dp[0][0] = true;

        // 处理 p 开头是 '*' 的情况
        for (int j = 1; j <= n; ++j) {
            if (p[j - 1] == '*') {
                dp[0][j] = dp[0][j - 1];
            }
        }

        // 填充 dp 表格
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p[j - 1] == s[i - 1] || p[j - 1] == '?') {
                    // 单个字符匹配,或 '?' 匹配任意一个字符
                    dp[i][j] = dp[i - 1][j - 1];
                } else if (p[j - 1] == '*') {
                    // '*' 可以匹配零个或多个字符
                    dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
                }
            }
        }

        // 最终结果
        return dp[m][n];
    }
};

44.交错字符串

给定三个字符串s1,s2,s3,请你帮忙验证s3是否是由s1与s2交错组成的。

实则就是一个使用双指针技巧合并两个字符串的过程。
但本题跟普通的数组/链表双指针技巧不同的是,这里需要穷举所有情况。比如 s1[i], s2[j] 都能匹配 s3[k] 的时候,到底应该让谁来匹配,才能完全合并出 s3 呢?回答这个问题很简单,我不知道让谁来,那就都来试一遍好了。
所以本题肯定需要一个递归函数来穷举双指针的匹配过程,然后用一个备忘录消除递归过程中的重叠子问题,也就能写出自顶向下的递归的动态规划解法了。

class Solution {
private:
    vector<vector<int>> memo;
    bool dp(string& s1, int i, string& s2, int j, string& s3){
        // base case
        int k = i + j;
        if(k == s3.size()) return true;

        if(memo[i][j] != -1) return memo[i][j] == 1;
        bool res = false;
        if(i < s1.size() && s1[i] == s3[k]) res = dp(s1, i + 1, s2, j, s3);
        if(j < s2.size() && s2[j] == s3[k]) res = res || dp(s1, i, s2, j + 1, s3);
        memo[i][j] = res ? 1 : 0;
        return res;
    }
public:
    bool isInterleave(string s1, string s2, string s3) {
        int m = s1.size(), n = s2.size();
        if(m + n != s3.size()) return false; // 长度不对的话,不可能对
        memo = vector<vector<int>>(m + 1, vector<int>(n + 1, -1));
        return dp(s1, 0, s2, 0, s3);
    }
};

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

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

相关文章

智能优化算法-海鸥优化算法(SOA)(附源码)

目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1.内容介绍&#xff1a; 海鸥优化算法 (Seagull Optimization Algorithm, SOA) 是一种基于群体智能的元启发式优化算法&#xff0c;它模拟了海鸥的觅食、飞行和社会交互行为&#xff0c;用于解决复杂的优化问题。 SOA的工…

wxpython Scintilla styledtextctrl滚动条拖到头文本内容还有很多的问题

wxpython Scintilla styledtextctrl滚动条拖到头文本内容还有很多的问题 使用wxpython Scintilla styledtextctrl&#xff0c;滚动条不自动更新 滚动条拖到头文本内容还有很多&#xff0c;如下&#xff1a; 以下是拖到最后的状态&#xff1a; 明显看出下图的滚动条的格子比…

书生.浦江大模型实战训练营——(十一)LMDeploy 量化部署进阶实践

最近在学习书生.浦江大模型实战训练营&#xff0c;所有课程都免费&#xff0c;以关卡的形式学习&#xff0c;也比较有意思&#xff0c;提供免费的算力实战&#xff0c;真的很不错&#xff08;无广&#xff09;&#xff01;欢迎大家一起学习&#xff0c;打开LLM探索大门&#xf…

超声波清洗机哪个品牌好?专业测评师推荐四款高质量眼镜清洗机

近年来&#xff0c;越来越多的用户在使用超声波清洗机清洗小物件&#xff0c;因为超声波清洗机不仅能清洗眼镜&#xff0c;还能清洗各种各样的小饰品、餐具、茶具、剃须刀、金属制品等&#xff0c;有一个智能超声波清洗机在家里&#xff0c;对于生活质感的提升还是挺大的&#…

第一个NIO开发演示

文章目录 Channel简介Buffer简介第一个NIO程序分析 上一篇文章 介绍了传统网络编程在处理高并发和大规模应用时通常面临性能瓶颈和资源管理问题&#xff0c;而 NIO 通过非阻塞 I/O 和选择器机制提供了更高效的解决方案。虽然 NIO 的 API 更复杂&#xff0c;但在高负载和高性能需…

先从路径优化开始学习FastPlanner之B样条曲线平滑路径(一):从拉格朗日插值到B样条曲线

参考B站视频学习 注&#xff1a;我会列出学习他人的博客&#xff0c;但我不涉及具体推导&#xff0c;原理讲解&#xff0c;旨在于理解必须概念后写代码出效果。 给若干点如何获得一条平滑的曲线&#xff1f; 两个方法插值、拟合 插值要经过给定点&#xff0c;拟合不用经过。 经…

Hostease的Windows虚拟主机如何设置错误页面

404错误设置主要用于定义当访问网站上不存在的页面时服务器应该如何响应。通常&#xff0c;404错误表示请求的页面或资源不存在。在Plesk面板中&#xff0c;你可以通过404错误设置来配置服务器对这种情况的处理方式。下面我就介绍如何在Hostease的Windows虚拟主机中设置404错误…

探索数据结构:图(一)之邻接矩阵与邻接表

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;数据结构与算法 贝蒂的主页&#xff1a;Betty’s blog 1. 图的定义 **图&#xff08;Graph&#xff09;**是数学和计算机科学中…

Mybatis-plus 创建自定义 FreeMarker 模板详细教程

FreeMarker 自定义模板官方步骤 网址&#xff1a;https://baomidou.com/reference/new-code-generator-configuration/#%E6%A8%A1%E6%9D%BF%E9%85%8D%E7%BD%AE-templateconfig &#xff08;页面往最下面拉为自定义模板相关内容&#xff09; 创建自定义FreeMarker 模板及使用…

案例分享—优秀ui设计作品赏析

多浏览国外优秀UI设计作品&#xff0c;深入分析其设计元素、色彩搭配、布局结构和交互方式&#xff0c;以理解其背后的设计理念和趋势。 在理解的基础上&#xff0c;尝试将国外设计风格中的精髓融入自己的设计中&#xff0c;同时结合国内用户的审美和使用习惯&#xff0c;进行创…

趣味算法------试用 6 和 9 组成的最大数字

目录 ​编辑 题目描述 解题思路 具体代码 总结 题目描述 给你一个仅由数字 6 和 9 组成的正整数 num。 你最多只能翻转一位数字&#xff0c;将 6 变成 9&#xff0c;或者把 9 变成 6 。 请返回你可以得到的最大数字。 输入格式 一个整数 输出格式 一个整数 输入输出…

【机器学习】决策树------迅速了其基本思想,Sklearn的决策树API及构建决策树的步骤!!!

目录 &#x1f354; 案例剖析 &#x1f354; 通过sklearn实现决策树分类并进一步认识决策树 &#x1f354; 基于规则构建决策树 &#x1f354; 构建决策树的三个步骤 &#x1f354; 小结 学习目标 &#x1f340; 了解决策树算法的基本思想 &#x1f340; 了解Sklearn的决策…

Linux WPA/WPA2/WPA3/IEEE 802.1X Supplicant

参考&#xff1a;wpa_supplicant 终端连接 wifi — Linux latest 文档 (gnu-linux.readthedocs.io) 为了管理无线网卡驱动&#xff0c;并且能正常连接到无线网络&#xff0c;你需要一个无线连接管理工具。 如何选择一个最佳的管理方法&#xff0c;将依赖于下面几个因素&#xf…

【数据结构】—— 树和二叉树

1、树的概念2、树的相关术语3、树的常见表示方法4、树的实际应用5、二叉树的相关概念和性质6、二叉树的顺序存储&#xff08;堆&#xff09;6.1 堆的概念6.2 堆的结构和接口6.3 堆的初始化和销毁6.4 堆的插入6.5 堆的删除6.5 取堆顶数据6.6 获取有效节点个数6.7 判空6.8 源代码…

免费SSL证书申请流程开启HTTPS,以及3个月到期解决方法

阿里云免费SSL证书申请流程2024年最新申请教程&#xff0c;阿里云免费SSL证书品牌是Digicert&#xff0c;免费单域名证书&#xff0c;一个阿里云账号可以免费申请20张SSL免费证书&#xff0c;免费时长为3个月&#xff08;之前是一年免费时长&#xff09;&#xff0c;免费SSL证书…

【Java并发】变量的内存存储、线程安全分析

要理解原因&#xff0c;首先要清楚局部变量是什么&#xff1f;局部变量的存储方式是什么&#xff1f; 局部变量&#xff0c;从名字上就可以知道&#xff0c;它是只在特定作用域内可见并且只能在该作用域内使用的变量。也就意味着不同作用域的局部变量是不共享的。在多线程环境下…

Apache Tomcat与反向代理

Apache Tomcat 是一个开源的 Java Servlet 容器&#xff0c;主要用于部署和运行基于 Java 的 Web 应用程序。Tomcat 提供了一个环境&#xff0c;让开发者能够使用 Java 编写的 Web 应用程序在 Web 服务器上运行。下面是对 Tomcat 的详细介绍&#xff1a; Tomcat 的历史 Tomca…

C++学习笔记——三角形面积

一、题目描述 二、代码 #include <iostream> #include <bits/stdc.h> using namespace std; int main() {double a0,b0,c0;cin >> a >> b >> c;double p(abc)/2;double dp * (p-a)* (p-b)* (p-c);cout<< fixed << setprecision(3)&l…

QT 简易网页信息抓取程序模板基础代码

有些网页爬不了&#xff0c;只是一个简单的代码。 项目结构 NetBugBaseCode.pro #------------------------------------------------- # # Project created by QtCreator 2024-08-26T15:13:10 # #-------------------------------------------------QT core gui netw…

抖音小红书爆款预定,Tiktok爆火的短视频玩法,Ai生成宝宝走秀视频,萌翻全场

大家好&#xff0c;我是方知有&#xff0c;每天分享一个互联网副业&#xff0c;喜欢的朋友可以关注~ 今天给大家分享在Tiktok爆火的短视频玩法&#xff0c;现在抖音小红书制作这类型视频的人数还不多&#xff0c;大家可以赶快操作起来&#xff0c;这个玩法就是用Ai生成宝宝走秀…