- 5. 最长回文子串 中等
- 10. 正则表达式匹配 困难
- 32. 最长有效括号 困难
- 62. 不同路径 中等
- 64. 最小路径和 中等
- 70. 爬楼梯 简单
- 72. 编辑距离 困难
- 139. 单词拆分 中等
- 152. 乘积最大子数组 中等
- 198. 打家劫舍 中等
- 221. 最大正方形 中等
- 279. 完全平方数 中等
- 300. 最长递增子序列 中等
- 309. 买卖股票的最佳时机含冷冻期 中等
- 312. 戳气球 困难
- 322. 零钱兑换 中等
- 337. 打家劫舍 III 中等
- 416. 分割等和子集 中等
- 494. 目标和 中等
5. 最长回文子串
解法1: 动态规划
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length(); // 获取输入字符串的长度
int maxlength = 1; // 用于记录最长回文子串的长度
int begin = 0; // 用于记录最长回文子串的起始位置
if (n < 2) return s; // 如果字符串长度小于2,直接返回原字符串,因为任何单字符都是回文
vector<vector<int>> d(n, vector<int>(n)); // 创建一个二维数组d,用于存储回文子串信息
// 给所有长度为1的子串都设置为回文子串
for (int i = 0; i < n; i++)
d[i][i] = true;
// 先遍历回文子串的长度,在遍历左边界,由长度和左边界可以确定右边界
for (int L = 2; L <= n; L++) {
for (int i = 0; i < n; i++) {
int j = i + L - 1; // 计算右边界的索引
// 如果右边界越界,跳出循环
if (j >= n) break;
if (s[i] != s[j]) {
d[i][j] = false; // 如果左右字符不相等,说明不是回文子串
} else {
if (j - i < 3) {
d[i][j] = true; // 如果左右字符相等且它们之间只有1个或2个字符,那么是回文子串
} else {
d[i][j] = d[i + 1][j - 1]; // 如果左右字符相等且它们之间有超过2个字符,判断它们是否是回文子串
}
}
// 如果从i到j是回文子串,且长度大于最大长度,就更新最大长度和起始位置
if (d[i][j] && j - i + 1 > maxlength) {
maxlength = j - i + 1;
begin = i;
}
}
}
// 返回最长回文子串,使用substr方法从原字符串中截取
return s.substr(begin, maxlength);
}
};
- 时间复杂度: O(n2)
- 空间复杂度: O(n2)
解法2: 中心扩展法
中心扩散法:从左向右遍历,以每个元素为一个中心,利用“回文串”中心对称的特点,左右扩散,看最多能扩散多远。
- 先看
当前元素是否与其相邻的右侧元素相同,若相同则 right 指针向右移动一位,回文串长度加 1 - 再看
当前元素是否与其相邻的左侧元素相同,若相同则 left 指针向左移动一位,回文串长度加 1 - 最后看当前 left 指针和 right 指针指向的元素是否
相同,若相同则 left 指针左移以及 right 指针右移,并且回文串长度加 2。 - 若没有满足以上三个条件的元素时,则判断当前得到的回文串长度
是否比之前的要长。若比之前长,则记录新的回文串的长度以及该回文串的开始和结尾时的下标(此时,需要将 left 指针右移一位以及 right 指针左移一位。因为,当进入该判断时,此时的两个指针所指向的元素并不满足回文串的条件,所以应将两指针均回移一位)。 - 将回文串长度
重新设置为1,继续下一个元素的遍历直到结束。
class Solution {
public:
int maxLen = 0; // 记录最长回文子串的长度
int begin = 0; // 记录最长回文子串的起始位置
void extend(string &s, int i, int j, int n) {
while (i >= 0 && j < n && s[i] == s[j]) {
if (j - i + 1 > maxLen) { // 如果找到更长的回文子串
maxLen = j - i + 1; // 更新最长回文子串的长度
begin = i; // 更新最长回文子串的起始位置
}
--i; // 向左移动指针
++j; // 向右移动指针
}
}
string longestPalindrome(string s) {
for (int i = 0; i < s.size(); i++) {
extend(s, i, i, s.size()); // 以字符为中心,向两边扩展查找回文子串
extend(s, i, i + 1, s.size()); // 以字符对之间为中心,向两边扩展查找回文子串
}
// 返回最长回文子串,使用substr方法从原字符串中截取
return s.substr(begin, maxLen);
}
};
- 时间复杂度: O(n2)
- 空间复杂度: O(1)
10. 正则表达式匹配
- 情况1:
例子: s="aa", p="a."
- 情况2:
s="aab",p="aab*"
class Solution {
public:
bool isMatch(string s, string p) {
int lens=s.size();
int lenp=p.size();
vector<vector<bool>> dp(lens+1,vector<bool>(lenp+1,false)); // 创建一个二维动态规划表 dp
// 初始化,当 s 和 p 都为空时匹配
dp[0][0]=true;//两个空字串
for(int j=1;j<lenp+1;j++)
{
if(p[j]=='*')
{
// 基础情况: s为空串, p不为空串
// 要想匹配,只可能是右端是星号,它干掉一个字符后,把 p 变为空串。
dp[0][j+1]=dp[0][j-1];
}
}
// 更新动态规划表
for(int i=1;i<lens+1;i++)
{
for(int j=1;j<lenp+1;j++)
{
if(s[i-1]==p[j-1]||p[j-1]=='.')
{
// 情况1:s[i−1] 和 p[j−1] 是匹配的符合,直接更新
dp[i][j]=dp[i-1][j-1];
}
else if(p[j-1]=='*')//情况2:考虑*的情况
{
if(s[i-1]==p[j-2]||p[j-2]=='.')
{
// 情况2.1: 分别是 * 让p[j-2]重复0次、重复一次、重复两次及以上
// 例子: s="aab",p="aab*"
dp[i][j]=dp[i][j-2]||dp[i-1][j-2]||dp[i-1][j];
}
else
{
// 情况2.2: p[j−1]=="∗",但 s[i−1]s[i-1]s[i−1] 和 p[j−2]p[j-2]p[j−2] 不匹配
// 例子: s="aab", p="aabb*"
dp[i][j]=dp[i][j-2];
}
}
}
}
return dp[lens][lenp];
}
};
32. 最长有效括号
解法1: 栈
class Solution {
public:
int longestValidParentheses(string s) {
int maxans = 0; // 用于记录最长有效括号子串的长度
stack<int> stk; // 使用栈来辅助处理括号匹配,栈中存储字符在字符串中的下标
stk.push(-1); // 初始化栈,将-1入栈表示起始位置
for (int i = 0; i < s.length(); i++) { // 遍历字符串的每个字符
if (s[i] == '(') { // 如果当前字符是左括号
stk.push(i); // 将当前字符的下标入栈
} else { // 如果当前字符是右括号
stk.pop(); // 弹出栈顶元素,表示与当前右括号匹配
if (stk.empty()) { // 如果栈为空
stk.push(i); // 将当前右括号的下标入栈,用于表示新的起始位置
} else {
maxans = max(maxans, i - stk.top()); // 计算当前有效括号子串的长度,并更新最长长度
}
}
}
return maxans; // 返回最长有效括号子串的长度
}
};
- 时间复杂度: O(N), n 是给定字符串的长度。我们只需要遍历字符串一次即可。
- 空间复杂度: O(N), 栈的大小在最坏情况下会达到 nnn,因此空间复杂度为 O(n)O(n)O(n) 。
解法2: 动态规划
class Solution {
public:
int longestValidParentheses(string s) {
int size = s.length(); // 获取字符串的长度
vector<int> dp(size, 0); // 创建一个数组 dp 用于记录每个位置的最长有效括号长度
// 用于记录最长有效括号子串的长度
int maxVal = 0;
// 从第二个字符开始遍历字符串
for(int i = 1; i < size; i++) {
// 如果当前字符是右括号
if (s[i] == ')') {
// 如果前一个字符是左括号
if (s[i - 1] == '(') {
// 至少可以组成一个 (),长度为2
dp[i] = 2;
// 如果前面还有字符
if (i - 2 >= 0) {
// 将之前的有效括号长度加上
dp[i] = dp[i] + dp[i - 2];
}
} else if (dp[i - 1] > 0) { // 如果前一个字符是右括号,且前一个位置的最长有效括号长度大于0
// 如果前一个位置的有效括号前面是左括号
if ((i - dp[i - 1] - 1) >= 0 && s[i - dp[i - 1] - 1] == '(') {
// 当前位置可以和前一个位置的有效括号连接,至少可以组成 (),长度为2
dp[i] = dp[i - 1] + 2;
// 如果前面还有字符
if ((i - dp[i - 1] - 2) >= 0) {
// 将之前的有效括号长度加上
dp[i] = dp[i] + dp[i - dp[i - 1] - 2];
}
}
}
}
// 更新最长有效括号子串的长度
maxVal = max(maxVal, dp[i]);
}
// 返回最长有效括号子串的长度
return maxVal;
}
};
- 时间复杂度: 遍历了一遍字符串,所以时间复杂度是:O(N)
- 空间复杂度:需要和字符串长度相同的数组保存每个位置的最长有效括号长度,所以空间复杂度是:O(N)
62. 不同路径
解法1: 动态规划
我们用 f(i,j) 表示从左上角走到 (i,j) 的路径数量,其中 i 和 j 的范围分别是 [0,m) 和 [0,n)。 由于我们每一步只能从向下或者向右移动一步,因此要想走到 (i,j),如果向下走一步,那么会从 (i−1,j) 走过来;如果向右走一步,那么会从 (i,j−1) 走过来。因此我们可以写出动态规划转移方程: f(i,j)=f(i−1,j)+f(i,j−1)
class Solution {
public:
int uniquePaths(int m, int n) {
// 创建一个二维数组f,用于存储不同路径的数量
vector<vector<int>> f(m, vector<int>(n));
for (int i = 0; i < m; ++i) {
// 第一列的所有格子只能向下移动,所以路径数量都为1
f[i][0] = 1;
}
for (int j = 0; j < n; ++j) {
// 第一行的所有格子只能向右移动,所以路径数量都为1
f[0][j] = 1;
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
// 动态规划转移方程,当前格子的路径数量等于上方格子和左方格子的路径数量之和
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
// 返回右下角格子的路径数量,即从左上角到右下角的不同路径数量
return f[m - 1][n - 1];
}
};
- 时间复杂度: O(mn)
- 空间复杂度: O(mn), 即为存储所有状态需要的空间。
解法2: 数学
class Solution {
public:
int uniquePaths(int m, int n) {
long long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y) {
ans = ans * x / y;
}
return ans;
}
};
- 时间复杂度: O(m)
- 空间复杂度: O(1)
64. 最小路径和
解法1: 动态规划
创建二维数组 dp,与原始网格的大小相同,dp[i][j] 表示从左上角出发到 (i,j) 位置的最小路径和。显然,dp[0][0]=grid[0][0]。对于 dp 中的其余元素,通过以下状态转移方程计算元素值。
-
当 i>0 且 j=0 时,dp[i][0]=dp[i−1][0]+grid[i][0]
-
当 i=0 且 j>0 时,dp[0][j]=dp[0][j−1]+grid[0][j]
-
当 i>0 且 j>0 时,dp[i][j]=min(dp[i−1][j],dp[i][j−1])+grid[i][j]
最后得到 dp[m−1][n−1] 的值即为从网格左上角到网格右下角的最小路径和。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0) {
return 0;
}
int rows = grid.size(), columns = grid[0].size();
auto dp = vector < vector <int> > (rows, vector <int> (columns));
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
};
- 时间复杂度: O(mn), 其中 m 和 n 分别是网格的行数和列数。需要对整个网格遍历一次,计算 dp 的每个元素的值
- 空间复杂度: O(mn), 其中 m 和 n 分别是网格的行数和列数。创建一个二维数组 dp,和网格大小相同。 空间复杂度可以优化,例如每次只存储上一行的 dp 值,则可以将空间复杂度优化到 O(n)。
70. 爬楼梯
解法1: 动态规划(滚动数组)
我们用 f(x) 表示爬到第 x 级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以我们可以列出如下式子: f(x)=f(x−1)+f(x−2), 可以用「滚动数组思想」把空间复杂度优化成 O(1)
class Solution {
public:
int climbStairs(int n) {
int p = 0, q = 0, r = 1;
for (int i = 1; i <= n; ++i) {
p = q;
q = r;
r = p + q;
}
return r;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
72. 编辑距离
解法1: 动态规划
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.length();
int m = word2.length();
// 有一个字符串为空串
if (n * m == 0) return n + m;
// DP 数组
vector<vector<int>> D(n + 1, vector<int>(m + 1));
// 边界状态初始化
for (int i = 0; i < n + 1; i++) {
D[i][0] = i;
}
for (int j = 0; j < m + 1; j++) {
D[0][j] = j;
}
// 计算所有 DP 值
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = D[i - 1][j] + 1;
int down = D[i][j - 1] + 1;
int left_down = D[i - 1][j - 1];
if (word1[i - 1] != word2[j - 1]) left_down += 1;
D[i][j] = min(left, min(down, left_down));
}
}
return D[n][m];
}
};
- 时间复杂度: O(mn)
- 空间复杂度: O(mn), 我们需要大小为 O(mn) 的 D 数组来记录状态值
139. 单词拆分
解法1: 动态规划
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
auto wordDictSet = unordered_set <string> ();
for (auto word: wordDict) {
wordDictSet.insert(word);
}
auto dp = vector <bool> (s.size() + 1);
dp[0] = true;
for (int i = 1; i <= s.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (dp[j] && wordDictSet.find(s.substr(j, i - j)) != wordDictSet.end()) {
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
- 时间复杂度: O(n2), 其中 n 为字符串 s 的长度。我们一共有 O(n) 个状态需要计算,每次计算需要枚举 O(n) 个分割点,哈希表判断一个字符串是否出现在给定的字符串列表需要 O(1) 的时间,因此总时间复杂度为 O(n2)
- 空间复杂度: O(n2), 其中 n 为字符串 s 的长度。我们需要 O(n) 的空间存放 dp 值以及哈希表亦需要 O(n) 的空间复杂度,因此总空间复杂度为 O(n)
152. 乘积最大子数组
解法1: 动态规划 (滚动数组版)
class Solution {
public:
int maxProduct(vector<int>& nums) {
int maxF = nums[0], minF = nums[0], ans = nums[0];
for (int i = 1; i < nums.size(); ++i) {
int mx = maxF, mn = minF;
maxF = max(mx * nums[i], max(nums[i], mn * nums[i]));
minF = min(mn * nums[i], min(nums[i], mx * nums[i]));
ans = max(maxF, ans);
}
return ans;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
198. 打家劫舍
解法1: 动态规划 (滚动数组版)
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) {
return 0;
}
int size = nums.size();
if (size == 1) {
return nums[0];
}
int first = nums[0], second = max(nums[0], nums[1]);
for (int i = 2; i < size; i++) {
int temp = second;
second = max(first + nums[i], second);
first = temp;
}
return second;
}
};
- 时间复杂度: O(n), 其中 n 是数组长度。只需要对数组遍历一次
- 空间复杂度: O(1), 使用滚动数组,可以只存储前两间房屋的最高总金额,而不需要存储整个数组的结果,因此空间复杂度是 O(1)
221. 最大正方形
解法1: 动态规划
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if (matrix.size() == 0 || matrix[0].size() == 0) {
return 0;
}
int maxSide = 0;
int rows = matrix.size(), columns = matrix[0].size();
vector<vector<int>> dp(rows, vector<int>(columns));
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
maxSide = max(maxSide, dp[i][j]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}
};
- 时间复杂度: O(mn)
- 空间复杂度: O(mn)
279. 完全平方数
解法1: 动态规划
class Solution {
public:
int numSquares(int n) {
vector<int> f(n + 1);
for (int i = 1; i <= n; i++) {
int minn = INT_MAX;
for (int j = 1; j * j <= i; j++) {
minn = min(minn, f[i - j * j]);
}
f[i] = minn + 1;
}
return f[n];
}
};
解法2: 数学
class Solution {
public:
// 判断是否为完全平方数
bool isPerfectSquare(int x) {
int y = sqrt(x);
return y * y == x;
}
// 判断是否能表示为 4^k*(8m+7)
bool checkAnswer4(int x) {
while (x % 4 == 0) {
x /= 4;
}
return x % 8 == 7;
}
int numSquares(int n) {
if (isPerfectSquare(n)) {
return 1;
}
if (checkAnswer4(n)) {
return 4;
}
for (int i = 1; i * i <= n; i++) {
int j = n - i * i;
if (isPerfectSquare(j)) {
return 2;
}
}
return 3;
}
};
300. 最长递增子序列
解法1: 动态规划
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = (int)nums.size();
if (n == 0) {
return 0;
}
vector<int> dp(n, 0);
for (int i = 0; i < n; ++i) {
dp[i] = 1;
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
- 时间复杂度: O(n2)
- 空间复杂度: O(n)
解法2: 二分查找
class Solution{
public:
int lengthOfLIS(vector<int>& nums)
{
int n=nums.size();
vector<int> ans;
ans.push_back(nums[0]);
for(int i=1;i<n;i++){
int len=ans.size();
if(nums[i]>ans[len-1]) ans.push_back(nums[i]);
else{
int l=0,r=len-1,L;
while(l<=r){
int mid=(l+r)/2;
if(ans[mid]>=nums[i]){
r=mid-1;
L=r;
}
else l=mid+1;
}
ans[L+1]=nums[i];
}
}
return ans.size();
}
};
- 时间复杂度: O(NlogN), 每个数组二分法需要O(logN)
- 空间复杂度: O(N), 列表占用线性大小额外空间
309. 买卖股票的最佳时机含冷冻期
解法1: 动态规划
// 空间优化前
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.empty()) {
return 0;
}
int n = prices.size();
// f[i][0]: 手上持有股票的最大收益
// f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
// f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
vector<vector<int>> f(n, vector<int>(3));
f[0][0] = -prices[0];
for (int i = 1; i < n; ++i) {
f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i]);
f[i][1] = f[i - 1][0] + prices[i];
f[i][2] = max(f[i - 1][1], f[i - 1][2]);
}
return max(f[n - 1][1], f[n - 1][2]);
}
};
// 空间优化后
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.empty()) {
return 0;
}
int n = prices.size();
int f0 = -prices[0];
int f1 = 0;
int f2 = 0;
for (int i = 1; i < n; ++i) {
int newf0 = max(f0, f2 - prices[i]);
int newf1 = f0 + prices[i];
int newf2 = max(f1, f2);
f0 = newf0;
f1 = newf1;
f2 = newf2;
}
return max(f1, f2);
}
};
- 时间复杂度: O(N), N为数组的长度
- 空间复杂度: O(N)
312. 戳气球
解法1: 动态规划
class Solution {
public:
int maxCoins(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
nums.insert(nums.begin(), 1);
nums.push_back(1);
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 2; j <= n + 1; j++) {
for (int k = i + 1; k < j; k++) {
dp[i][j] = max(
dp[i][j],
(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]));
}
}
}
return dp[0][n + 1];
}
};
- 时间复杂度: O(n3), 气球个数
- 空间复杂度: O(n3)
322. 零钱兑换
解法1:动态规划(自下而上)
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < (int)coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
- 时间复杂度: O(Sn), 气球个数
- 空间复杂度: O(S)
337. 打家劫舍 III
解法1:动态规划(优化空间前)
class Solution {
public:
unordered_map <TreeNode*, int> f, g;
void dfs(TreeNode* node) {
if (!node) {
return;
}
dfs(node->left);
dfs(node->right);
f[node] = node->val + g[node->left] + g[node->right];
g[node] = max(f[node->left], g[node->left]) + max(f[node->right], g[node->right]);
}
int rob(TreeNode* root) {
dfs(root);
return max(f[root], g[root]);
}
};
- 时间复杂度: O(n), n为二叉树节点
- 空间复杂度: O(n), n为二叉树节点
解法2:动态规划(优化空间后)
struct SubtreeStatus {
int selected;
int notSelected;
};
class Solution {
public:
SubtreeStatus dfs(TreeNode* node) {
if (!node) {
return {0, 0};
}
auto l = dfs(node->left);
auto r = dfs(node->right);
int selected = node->val + l.notSelected + r.notSelected;
int notSelected = max(l.selected, l.notSelected) + max(r.selected, r.notSelected);
return {selected, notSelected};
}
int rob(TreeNode* root) {
auto rootStatus = dfs(root);
return max(rootStatus.selected, rootStatus.notSelected);
}
};
- 时间复杂度: O(n), n为二叉树节点
- 空间复杂度: O(n), n为二叉树节点, 虽然优化过的版本省去了哈希表的空间,但是栈空间的使用代价依旧是 O(n),故空间复杂度不变
416. 分割等和子集
解法1: 动态规划(优化前)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
if (n < 2) {
return false;
}
int sum = accumulate(nums.begin(), nums.end(), 0);
int maxNum = *max_element(nums.begin(), nums.end());
if (sum & 1) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
vector<vector<int>> dp(n, vector<int>(target + 1, 0));
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
dp[0][nums[0]] = true;
for (int i = 1; i < n; i++) {
int num = nums[i];
for (int j = 1; j <= target; j++) {
if (j >= num) {
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
};
- 时间复杂度: O(n * target), 其中 nnn 是数组的长度,target是整个数组的元素和的一半。需要计算出所有的状态,每个状态在进行转移时的时间复杂度为 O(1)。
- 空间复杂度: O(n*target)
解法1: 动态规划(优化后)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
for (auto& num : nums) {
sum += num;
maxNum = max(maxNum, num);
}
if (sum & 1) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
vector<int> dp(target + 1, 0);
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
}
};
- 时间复杂度: O(n * target), 其中 n 是数组的长度,target是整个数组的元素和的一半。需要计算出所有的状态,每个状态在进行转移时的时间复杂度为 O(1)。
- 空间复杂度: O(target)其中 target 是整个数组的元素和的一半。空间复杂度取决于 dp 数组,在不进行空间优化的情况下,空间复杂度是 O(n×target),在进行空间优化的情况下,空间复杂度可以降到 O(target)。
494. 目标和
解法1: 回溯
数组 nums 的每个元素都可以添加符号 + 或 -,因此每个元素有 2 种添加符号的方法,n 个数共有 2^n种添加符号的方法,对应 2^n 种不同的表达式。当 n 个元素都添加符号之后,即得到一种表达式,如果表达式的结果等于目标数 target,则该表达式即为符合要求的表达式。 可以使用回溯的方法遍历所有的表达式,回溯过程中维护一个计数器 count,当遇到一种表达式的结果等于目标数 target 时,将 count 的值加 1。遍历完所有的表达式之后,即可得到结果等于目标数 target 的表达式的数目。
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) {
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]);
}
}
};
- 时间复杂度: O(2^n), 其中 nnn 是数组 nums 的长度。回溯需要遍历所有不同的表达式,共有 2^n种不同的表达式,每种表达式计算结果需要 O(1) 的时间,因此总时间复杂度是 O(2n)。
- 空间复杂度: O(n)
解法2: 动态规划(优化前)
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int& num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int n = nums.size(), neg = diff / 2;
vector<vector<int>> dp(n + 1, vector<int>(neg + 1));
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= neg; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= num) {
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[n][neg];
}
};
解法2: 动态规划(优化后)
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int& num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int neg = diff / 2;
vector<int> dp(neg + 1);
dp[0] = 1;
for (int& num : nums) {
for (int j = neg; j >= num; j--) {
dp[j] += dp[j - num];
}
}
return dp[neg];
}
};