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];
}
};