LeetCode 动态规划问题

@

1. 背包问题

474. 一和零

给定 m 个数字 0 和 n 个数字 1,以及一些由 0 1 构成的字符串,求利用这些数字最多可以构成多少个给定的字符串,字符串只可以构成一次。

示例

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4,其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和1 的数量。可利用空间压缩将三维空间压缩到二维。

class Solution {
public:
    pair<int, int> count(const string& str) {
        int count0 = str.length(), count1 = 0;
        for (int i = 0; i < str.length(); i++) {
            if (str[i] == '1') {
                --count0;
                ++count1;
            }
        }
        return make_pair(count0, count1);
    }
    int findMaxForm(vector<string>& strs, int m, int n) {
        int num = strs.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (const string& str : strs) {
            auto [count0, count1] = count(str);
            for (int i = m; i >=count0; i--) { // 逆序遍历背包的容量
                for (int j = n; j >= count1; j--) {
                	// 递归最大组合数 
                    dp[i][j] = max(dp[i][j], dp[i - count0][j - count1] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

使用三维空间不压缩的情况

int findMaxForm(vector<string>& strs, int m, int n) {
    int num = strs.size();
    vector<vector<vector<int>>> dp(num + 1, vector<vector<int>>(m + 1, vector<int>(n + 1, 0)));
    for (int i = 1; i <= num; i++) {
        const string& str = strs[i - 1];
        auto [count0, count1] = count(str);
        // 此处的 0-1 背包问题每种物品的体积可以为 0,所以从 0 开始遍历
        for (int j = 0; j <= m; j++) {
            for (int k = 0; k <= n; k++) {
                if (j >= count0 && k >= count1) {
                    // 是否取第i串的最优化选择
                    // 0-1 背包问题,有没有取当前 i 都需要考虑 i-1 时的情况
                    // 完全背包问题,当前 i 可能取多次,所以考虑 i-1 和 i 时的情况
                    dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - count0][k - count1] + 1);
                } else {
                    dp[i][j][k] = dp[i - 1][j][k];
                }
            }
        }
    }
    return dp[num][m][n];
}

322. 零钱兑换

一个整数数组 coins,表示不同面额的硬币;一个整数 amount 表示总金额。
计算并返回可以凑成总金额所需的最少的硬币个数 。
如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if (coins.empty()) return -1;
        // amount + 2 可以看作是一个极大值,为了方便取最小运算
        vector<int> dp(amount + 1, amount + 2);
        dp[0] = 0; // 边界条件置 0 ,否则无法执行
        for (int coin : coins) {
        	// 完全背包问题,体积正向遍历
            for (int j = coin; j <= amount; j++) {
                dp[j] = min(dp[j], dp[j - coin] + 1);
            }
        }
        // 判断是否有解
        return dp[amount] == amount + 2 ? -1 : dp[amount];
    }
};

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

  • 环状排列意味着第一个房子和最后一个房子只能偷一个,因此可以把环状排列房间约化为两个单排排列房间的子问题:
    • 在不偷第一个房子情况下的最大金额,即nums[1 : end]
    • 在不偷最后一个房子情况下的最大金额,即nums[0 : end - 1]
  • 比较以上两种情况的最大值作为最终结果输出
class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if (n == 1) return nums[0];
        // 返回两个单排排列房间子问题的最大值
        return max(rob1(nums, 1, n), rob1(nums, 0, n - 1));

    }
    int rob1(vector<int>& nums, int start, int end) {
        int pre2 = 0, pre1 = 0, cur;
        for (int i = start; i < end; i++) {
        	// pre2 前两个房间的最大金额,此时可抢当前房间
        	// pre1 前一个房间的最大金额,此时不可抢当前房间
        	// 默认会抢第一个房间
            cur = max(pre2 + nums[i], pre1);
            pre2 = pre1;
            pre1 = cur;
        }
        return cur;
    };
};

494. 目标和

给你一个整数数组 nums 和一个整数 target 。向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个表达式,返回通过上述方法构造的、运算结果等于 target 的不同表达式的数目。

  • 回溯
    • 每个元素都可以添加 + 或 - 号,使用回溯的方式遍历所有的表达式,过程中维护一个计数器 conut,遇到符合要求的表达式就将计数器 + 1
class Solution {
public:
    int count = 0;
    int findTargetSumWays(vector<int>& nums, int target) {
        backtrack(nums, target, 0, 0);
        return count;
    }
    void backtrack(vector<int>& nums, int target, int index, int sum) {
    	// index 不合法时说明数组中的元素遍历完毕
        if (index == nums.size()) {
            if (sum == target) {
                count++;
            }
        }else {
            backtrack(nums, target, index + 1, sum - nums[index]); // 当前元素选择 -
            backtrack(nums, target, index + 1, sum + nums[index]); // 当前元素选择 +
        }
    }
};
  • 动态规划:转换为背包问题
    • 假设选取若干个元素添加 - 号,这些元素的和为 neg,则添加 + 号元素的和为 sum - neg,有 sum - neg - neg = target,则 neg = (sum - target) / 2。
    • 数组dp[i][j]表示前 i 个数中选取元素,其和为 j 的方案数,可知边界条件有dp[0][0] = 1 dp[0][j] = 0 ( j = 1, 2, ... , neg )

遍历数组中的元素,假设当前元素为 num,遍历 j = 0 - neg,

  • j < num,不能选择当前元素,dp[i][j] = dp[i-1][j]
  • j >= num,可选可不选,dp[i][j] = dp[i-1][j] + dp[i-1][j - num]

由于 dp 的每一行的计算只与上一行有关,可以使用滚动数组的方式,去掉 dp 的第一个维度,这样内层循环采取倒序遍历的方式,保证转移来的是dp[i-1][]中的元素值。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        int diff = sum - target;
        // 判断能否得到结果
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int neg = diff / 2;
        vector<int> dp(neg + 1, 0);
        dp[0] = 1; // 边界条件
        for (int i = 0; i < nums.size(); i++) {
        	// 逆序遍历得到方法数
            for (int j = neg; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[neg];
    }
};

416. 分割等和子集

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

  • 转换为0-1背包问题
    • 1、等和子集,假设和为 a,则有数组总和为 2a
    • 2、选择一部分元素使其和为 a
    • 3、选择的元素不能重复,即0-1背包问题
  • 剪枝条件:数组总和不为偶数,故不能划分为 2a
  • 状态转移:物品正向遍历、背包容量逆向遍历,假设数组为 3 2 1
  • 滚动数组,转移方程为 dp[j] = dp[j] || dp[j - num]; 不能为 dp[j] = dp[j - num],例如
    • 物品 第一层循环:{true, false, false, true} dp[j] = dp[j - 3];
    • 物品 第二层循环:{true, false, true, false} dp[j] = dp[j - 2];
    • 第一层循环时,dp[3] 为 true,但是第二层循环时被覆盖,得到的错误的结果
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % 2 != 0) return false;
        int target = sum / 2;
        int i = 0, j = 0;
        int n = nums.size();
        sum = 0;
        vector<bool> dp(target + 1);
        dp[0] = true; // 初始状态
        for (int i = 0; i < n; i++) { // 物品循环
            int num = nums[i];
            for (int j = target; j >= num; j--) { // 背包逆向循环
            	// 逆向循环时,dp[j - num] 为上一个物品的计算结果,相当于使用了上一轮的结果进行更新
            	// 上一轮的结果假设为 {T, F, F, T}
            	// 本轮需要用到上一轮的结果,因此需要逆向,因为后面的更新了不会被本轮用到
                dp[j] = dp[j] || dp[j - num]; // 滚动数组
            }
        }
        return dp[target];
    }
};

2. 字符串编辑

72. 编辑距离

给定两个字符串,已知你可以删除、替换和插入任意字符串的任意字符,求最少编辑几步可以将两个字符串变成相同

dp[i][j]表示将第一个字符串到位置 i 为止,第二个字符串到位置 j 位置,最少需要几步编辑,位置 i 表示第 i 个字符,i = 0 表示空字符

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.length(), n = word2.length();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                if (i == 0) {
                    dp[i][j] = j; // 可以考虑为第一个字符串为空时
                }else if (j == 0) {
                    dp[i][j] = i; // 同上
                }else {
                	// 位置 i 插入字符,插入字符肯定与第二个字符串位置 j 相同,这与删除位置 j 的字符是等价的
                	// 因此其代价是令字符串位置 i 为止和位置 j-1 为止相同所需的最少编辑步数 +1
                	// 注意这里加法 + 优先级高于 ==,因此需要用括号
                    dp[i][j] = min(dp[i - 1][j - 1] + ((word1[i - 1] == word2[j - 1]) ? 0 : 1),
                                   min(dp[i][j - 1] + 1, dp[i - 1][j] + 1));
                }
            }
        }
        return dp[m][n];
    }
};

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

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

  • 只允许删除操作,判断情况减少为 2 种:
    • 删除 word1 中第 i 个字符 dp[i - 1][j] + 1
    • 删除 word2 中第 j 个字符 dp[i][j - 1] + 1
  • 若当前字符相同,则有dp[i][j] = dp[i - 1][j - 1]
class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.length(), n = word2.length();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                if (i == 0) {
                    dp[i][j] = j; // word1 为空字符串的编辑距离
                }else if (j == 0) {
                    dp[i][j] = i; // word2 为空字符串的编辑距离
                }else {
                	// i j 表示第几个字符,用于索引时需要 -1
                    if (word1[i - 1] == word2[j - 1]) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }else {
                        dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
                    }
                }
            }
        }
        return dp[m][n];
    }
};

650. 只有两个键的键盘

给定一个字母A,已知你可以每次选择复制全部字符,或者粘贴之前复制的字符,求最少需要几次操作可以把字符串延展到指定长度。

dp[i]表示将字符串延展到长度i所需要的操作次数

class Solution {
public:
    int minSteps(int n) {
        vector<int> dp(n + 1);
        // dp[0] dp[1] 都是 0
        for (int i = 2; i <= n; i++) {
            dp[i] = i; // 复制单个字符一直粘贴需要次数最多 CPPP
            for (int j = 2; j * j <= i; j++) {
            	// j * j <= i, 则有 i / j * j >= 1, => i / j >= j
                if (i % j == 0) { // 可以整除,说明有 i / j 个 j
                	// 可以将 j 个字符视为一个整体,这样就转换为了 1 -> i / j的问题,该问题可以进一步优化
                	// j -> i 的最少操作次数并非 i / j,如 2 -> 16 最少需要CPCPCP 6次,而不是8次
                    dp[i] = dp[j] + dp[i / j];
                    break;
                }
            }
        }
        return dp[n];
    }
};

10. 正则表达式匹配

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

'.' 匹配任意单个字符
'*' 匹配零个或多个前一个字符

  • 当前字符为'*':
    • Case1:不匹配字符,将该组合扔掉,不再进行匹配,dp[i][j] = dp[i][j - 2]
    • Case2:匹配s末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配,dp[i][j] = dp[i - 1][j]
    • 前一个字符与s末尾不同,只能选择 Case1
    • 前一个字符与s末尾相同,Case1 Case2均可以选择,取或
  • 当前字符不为'*':
    • 判断当前字符与s末尾是否相同
      • 相同,两者都往前一步,取dp[i - 1][j - 1]的结果
      • 不同,直接false,即默认值

借助lambda表达式将'.'的判断隐含在了前一个字符的判断中,只需要在遇到'*'号时判断前一个字符与s末尾的关系,罗列状态转移关系即可。

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.length(), n = p.length();
        vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
        dp[0][0] = true;
		// lambda 表达式
        auto match = [&] (int i, int j) {
            if (i == 0) {
                return false;
            }
            if (p[j - 1] == '.') {
                return true;
            }
            return s[i - 1] == p[j - 1];
        };
        for (int i = 0; i <= m; i++) { // i 从 0 开始循环
            for (int j = 1; j <= n; j++) {
                if (p[j - 1] == '*') {
                    dp[i][j] = dp[i][j - 2]; // 不匹配字符,将该组合扔掉
                    if (match(i, j - 1)) { // 判断前一个字符与s末尾是否相同
                        dp[i][j] = dp[i - 1][j] || dp[i][j - 2]; 
                    }
                }else {
                	// 判断当前两个字符是否相同,因为默认是 false,所以不需要加 false 操作
                    if (match(i, j)) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                }
            }
        }
        return dp[m][n];
    }
};

3. 股票交易

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

给定一段时间内每天的股票价格,已知你只可以买卖各 k 次,且每次只能拥有一支股票,求最大的收益。

  • 算法实际为二维空间压缩后的情况,dp[j]表示第 i 天第 j 种情况下买入或卖出所获得的最大收益,第 j 种情况指的是这一天所处于的买入卖出状态。
  • 假如前一天是第一次买入,第二天没有操作,则第二天就沿用前一天的第一次买入状态。所以可以将dp[i][j]这样的二维空间压缩为一维的dp[j]。
  • buy[j] = max(buy[j], sell[j - 1] - prices[i]) equals to buy[i][j] = max(buy[i - 1][j], sell[i-1][j-1] - prices[i]);
class Solution {
public:
    int maxProfitUnlimited(vector<int>& prices) {
        int maxProfit = 0;
        for (int i = 1; i < prices.size(); i++) {
            if (prices[i] > prices[i - 1])
                maxProfit += prices[i] - prices[i - 1];
        }
        return maxProfit;
    };
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        // 无法交易
        if (n < 2) {
            return 0;
        }
        // 只要有利润就能交易
        if (k >= n) {
            return maxProfitUnlimited(prices);
        }
        // 普遍情况
        vector<int> buy(k + 1, INT_MIN), sell(k + 1, 0);
        for (int i = 0; i < n; i++) {
        	// buy sell 会对每个价格进行 k 次循环,更新前 i 天中第 j 次买入或卖出的最大收益
            for (int j = 1; j <= k; j++) {
            // 相当于 buy[i][j] = max(buy[i - 1][j], sell[i - 1][j - 1] - prices[i])
                buy[j] = max(buy[j], sell[j - 1] - prices[i]); // buy[j] 沿用前一天的买入状态, sell[j - 1] - prices[i] 当天第 j 次买入的收益情况
                sell[j] = max(sell[j], buy[j] + prices[i]);// 第 j 次卖出时的最大收益
            }
        }
        return sell[k];
    }
};

309. 最佳买卖股票时机含冷冻期

给定一段时间内每天的股票价格,已知每次卖出之后必须冷却一天,且每次只能拥有一支股票,求最大的收益。不限制买卖次数


  • 可以将第 i 天分为三种状态
    • 状态1:持有股票,记dp[i][0]
    • 状态2:不持有股票,未冻结,记dp[i][1]
    • 状态3:不持有股票,冻结,记dp[i][2]
  • 三种情况的状态转移方程为
    • 状态1: 买入第 i 天的股票或者未处理,买入第 i 天的股票需要前一天不持有股票且未冻结,即前一天属于状态2,则有dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i])
    • 状态2:前一天属于状态3或者今天卖出,则有dp[i][1] = max(dp[i - 1][0] + prices[i], dp[i - 1][2])
    • 状态3:前一天属于状态2,则有dp[i][2] = dp[i - 1][1];
  • 边界条件为
    • dp[0][0] = -prices[0]
    • dp[0][1] = 0
    • dp[0][2] = 0

最后一天两种未持有股票状态的最大值即为最大利润

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(3));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = 0;

        for (int i = 1; i < n; i++) {
            // 持有股票:1、前一天买了,今天未操作;2、前一天冻结,今天买了
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
            // 未持有且未冻结:1、前一天买了,今天卖出了;2、前一天卖出,今天未操作
            dp[i][1] = max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
            // 未持有且冻结:只能是前一天卖了
            dp[i][2] = dp[i - 1][1];
        }

        // 最大收益:1、最后一天卖了;2、倒数第二天卖了
        return max(dp[n - 1][1], dp[n - 1][2]);
    }
};

4. 分割问题

343. 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

  • dp[i]表示正整数按条件拆分后,这些正整数的乘积最大值,假设 $i$ 拆分出来的第一个整数是 $j$ $(1 \leq j < i )$,有以下两种方案:
    • 将 $i$ 拆分为 $j$ 和 $i - j$ 的和,且 $i - j$ 不再继续拆分,此时的乘积是$j \times (i - j)$
    • 将 $i$ 拆分为 $j$ 和 $i - j$ 的和,且 $i - j$ 继续拆分,此时的乘积是$j \times dp[i - j]$

在求解 dp[i] 时,仅需要将 j 从 1 遍历至 i - 1 并把所有结果取最大值即可得到。

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1);
        dp[0] = dp[1] = 0;
        for (int i = 2; i <= n; i++) {
        	// 求解正整数 i 拆分成若干正整数求和的最大乘积
            int curMax  = 0;
            for (int j = 1; j < i; j++) {
                curMax = max(curMax, max(j * (i - j), j * dp[i - j]));
            }
            // 需放在循环外面,否则 dp[i - j] 的值可能在循环中被修改
            dp[i] = curMax;
        }
        return dp[n];
    }
};
  • 数学推论:将数字 n 尽可能以因子 3 等分时,乘积最大。
  • 拆分规则:
    • 最优:余数为 0,可以用 3 的幂次表示
    • 次优:余数为 2,保留 2,不再继续拆分
    • 最差:余数为 1,把一份 3 + 1 拆分为 2 + 2,因为 $2 \times 2 > 3 \times 1$
class Solution {
public:
    int integerBreak(int n) {
        if (n <= 3) return n - 1;
        int num = n / 3, res = n % 3; // 求 3 等分的个数以及余数
        int max;
        switch(res) {
            case 0: max = pow(3, num); break;
            case 1: max = pow(3, num - 1) * 4; break; // 拿出一个 3 拆分
            case 2: max = pow(3, num) * 2; break;
        }
        return max;
    }
};

279. 完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

  • 动态规划思路,将 n 拆分为完全平方数求和的子问题
  • dp[n] = dp[n - 1] + 1; dp[n] = dp[n - 2 * 2] + 1; dp[n] = dp[n - 3 * 3] + 1;
  • 可以得到状态转移方程为 dp[i] = min(dp[i], dp[i - j*j] + 1);,确保i > j*j 即可
class Solution {
public:
    int numSquares(int n) {
    // 初值设为 n + 2 是为了取最小值时能够工作
        vector<int> dp(n + 1, n + 2);
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
        // 	根据状态转移求的最小的代价,保存结果去计算最终的 n 
            for (int j = i / j; j * j <= i; j++) {
                dp[i] = min(dp[i], dp[i - j *j] + 1);
            }
        }
        return dp[n];
    }
};

5. 子序列问题

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

子序列可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。求 nums 中作为 摆动序列最长子序列的长度。

  • 某个序列被称为「上升摆动序列」,当且仅当该序列是摆动序列,且最后一个元素呈上升趋势。
  • 某个序列被称为「下降摆动序列」,当且仅当该序列是摆动序列,且最后一个元素呈下降趋势。
  • 动态规划
    • $up$ 表示以前 $i -1$ 个元素中的某一个为结尾的最长的「上升摆动序列」的长度。
    • $down$ 表示以前 $i - 1$ 个元素中的某一个为结尾的最长的「下降摆动序列」的长度。
    • $nums[i] > nums[i -1]$,上升趋势,$up = down + 1$
    • $nums[i] < nums[i -1]$,下降趋势,$down = up + 1$
    • $nums[i] = nums[i -1]$,保持不变
    • 初始化 $up = 1, down = 1$
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if (n < 2) return n;
        int up = 1, down = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] > nums[i - 1]) {
                up = down + 1;
            }else if (nums[i] < nums[i - 1]) {
                down = up + 1;
            }
        }
        return max(up, down);
    }
};
  • 贪心算法
    • 计算当前元素 $i$ 与前一个元素 $i - 1$ 的差值,与 $i - 1$ 和 $i - 2$ 的差值对比看是否异号,异号则计数加 1,否则跳过。
    • 根据 $nums[1], nums[0]$ 的差值情况确定计数起点为 2 还是 1。
    • 计数完毕后更新前一个点的差值情况。
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if (n < 2) return n;
        int prev = nums[1] - nums[0];
        int cnt = prev != 0 ? 2 : 1; // 确定计数起点为 2 还是 1
        for (int i = 2; i < n; i++) {
            int diff = nums[i] - nums[i - 1];
            // prev 的判断包含 0 是为了覆盖开头差值为 0 的情况
            if ((diff > 0 && prev <= 0) || (diff < 0 && prev >= 0)) {
                cnt++;
                prev = diff; // 更新为当前的差值作为下一次判断的依据
            }
        }
        return cnt;
    }
};

6. 排列组合问题

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
有效括号组合需满足:左括号必须以正确的顺序闭合。

  • 思路:
    • n 对括号序列可以表示为 ( a ) + b 的形式,其中 a + b = n - 1,因为 a 已经被 1 对括号包围
    • 因此遍历 a + b = n - 1 的所有组合即可得到 n 对括号序列的全部合法结果
  • 递归思路:自上而下
    • 递归计算 a = 0, 1 , 2 , ... , n - 1b = n - 1 - a 的结果
    • 将所有结果组合起来
    • 利用哈希表存储计算结果减少重复计算
class Solution {
public:
    unordered_map<int, vector<string>> hash; // 哈希表存储计算结果
    vector<string> generateParenthesis(int n) {
        vector<string> res;
        if (hash.count(n)) {
            return hash[n];
        }
        if (n == 0) {
            return vector<string>{""}; // 返回空字符串
        }
        for (int i = 0; i < n; i++) {
            vector<string> lefts = generateParenthesis(i); // 递归调用
            vector<string> rights = generateParenthesis(n - 1 - i);
            for (const string& left : lefts) {
                for (const string& right : rights) {
                    res.push_back("(" + left + ")" + right); // 拼接的结果
                }
            }
        }
        hash[n] = res; // 存储计算结果
        return res;
    }
};
  • 动态规划:自下而上
    • 要求 n 对括号的结果,先计算0, 1, 2, ..., n - 1 的结果
class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<vector<string>> dp(n + 1);
        dp[0] = {""};
        dp[1] = {"()"}; // 边界条件
        for (int i = 2; i <= n; i++) { // 自下而上计算
            for (int j = 0; j < i; j++) {
                for (const string& l : dp[j]) {
                    for (const string& r : dp[i - j - 1]) {
                        dp[i].push_back("(" + l + ")" + r);
                    }
                }
            }
        }
        return dp[n];
    }
};
posted @ 2023-02-15 21:16  GreyWang  阅读(30)  评论(0编辑  收藏  举报