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,即默认值
- 判断当前字符与s末尾是否相同
借助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 - 1 及 b = 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]; } };
本文作者:GreyWang
本文链接:https://www.cnblogs.com/GreyWang/p/17124732.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步