Leecode Hot100 刷题笔记本-动态规划(C++版)

165 阅读10分钟
  1. 5. 最长回文子串 中等
  2. 10. 正则表达式匹配 困难
  3. 32. 最长有效括号 困难
  4. 62. 不同路径 中等
  5. 64. 最小路径和 中等
  6. 70. 爬楼梯 简单
  7. 72. 编辑距离 困难
  8. 139. 单词拆分 中等
  9. 152. 乘积最大子数组 中等
  10. 198. 打家劫舍 中等
  11. 221. 最大正方形 中等
  12. 279. 完全平方数 中等
  13. 300. 最长递增子序列 中等
  14. 309. 买卖股票的最佳时机含冷冻期 中等
  15. 312. 戳气球 困难
  16. 322. 零钱兑换 中等
  17. 337. 打家劫舍 III 中等
  18. 416. 分割等和子集 中等
  19. 494. 目标和 中等

5. 最长回文子串

Screen Shot 2023-08-21 at 7.01.27 PM.png

解法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: 中心扩展法

中心扩散法:从左向右遍历,以每个元素为一个中心,利用“回文串”中心对称的特点,左右扩散,看最多能扩散多远。

  1. 先看当前元素是否与其相邻的右侧元素相同,若相同则 right 指针向右移动一位,回文串长度加 1
  2. 再看当前元素是否与其相邻的左侧元素相同,若相同则 left 指针向左移动一位,回文串长度加 1
  3. 最后看当前 left 指针和 right 指针指向的元素是否相同,若相同则 left 指针左移以及 right 指针右移,并且回文串长度加 2。
  4. 若没有满足以上三个条件的元素时,则判断当前得到的回文串长度是否比之前的要长。若比之前长,则记录新的回文串的长度以及该回文串的开始和结尾时的下标(此时,需要将 left 指针右移一位以及 right 指针左移一位。因为,当进入该判断时,此时的两个指针所指向的元素并不满足回文串的条件,所以应将两指针均回移一位)。
  5. 将回文串长度重新设置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. 正则表达式匹配

Screen Shot 2023-08-23 at 4.03.52 PM.png

  • 情况1: Screen Shot 2023-08-23 at 5.35.01 PM.png 例子: s="aa", p="a."
  • 情况2: Screen Shot 2023-08-23 at 5.52.09 PM.png 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. 最长有效括号

Screen Shot 2023-08-23 at 7.01.37 PM.png

解法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: 动态规划

Screen Shot 2023-08-23 at 7.45.14 PM.png Screen Shot 2023-08-23 at 7.45.46 PM.png

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. 不同路径

Screen Shot 2023-08-23 at 8.05.27 PM.png

解法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: 数学

Screen Shot 2023-08-23 at 8.27.13 PM.png

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. 最小路径和

Screen Shot 2023-08-23 at 8.29.57 PM.png

解法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. 爬楼梯

Screen Shot 2023-08-23 at 9.14.05 PM.png

解法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. 编辑距离

Screen Shot 2023-08-23 at 9.26.44 PM.png

解法1: 动态规划

Screen Shot 2023-08-24 at 7.52.38 AM.png

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. 单词拆分

Screen Shot 2023-08-24 at 7.55.11 AM.png

解法1: 动态规划

Screen Shot 2023-08-24 at 8.13.21 AM.png

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. 乘积最大子数组

Screen Shot 2023-08-24 at 8.15.43 AM.png

解法1: 动态规划 (滚动数组版)

Screen Shot 2023-08-24 at 8.28.15 AM.png

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. 打家劫舍

Screen Shot 2023-08-24 at 8.29.49 AM.png

解法1: 动态规划 (滚动数组版)

Screen Shot 2023-08-24 at 8.41.55 AM.png

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. 最大正方形

Screen Shot 2023-08-24 at 8.48.51 AM.png

解法1: 动态规划

Screen Shot 2023-08-24 at 9.10.19 AM.png

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. 完全平方数

Screen Shot 2023-08-24 at 9.16.34 AM.png

解法1: 动态规划

Screen Shot 2023-08-24 at 9.30.14 AM.png

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

Screen Shot 2023-08-24 at 9.24.22 AM.png

解法2: 数学

Screen Shot 2023-08-24 at 9.30.37 AM.png

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

Screen Shot 2023-08-24 at 9.31.39 AM.png

300. 最长递增子序列

Screen Shot 2023-08-24 at 9.33.52 AM.png

解法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. 买卖股票的最佳时机含冷冻期

Screen Shot 2023-08-24 at 4.15.40 PM.png

解法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. 戳气球

Screen Shot 2023-08-24 at 5.55.22 PM.png

解法1: 动态规划

Screen Shot 2023-08-25 at 9.12.57 AM.png

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. 零钱兑换

Screen Shot 2023-08-25 at 9.31.16 AM.png

解法1:动态规划(自下而上)

Screen Shot 2023-08-25 at 9.34.43 AM.png

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

Screen Shot 2023-08-25 at 9.40.48 AM.png

解法1:动态规划(优化空间前)

Screen Shot 2023-08-25 at 9.42.34 AM.png

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:动态规划(优化空间后)

Screen Shot 2023-08-25 at 9.56.05 AM.png

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. 分割等和子集

Screen Shot 2023-08-25 at 5.42.12 PM.png Screen Shot 2023-08-25 at 6.04.49 PM.png

解法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. 目标和

Screen Shot 2023-08-25 at 6.10.21 PM.png

解法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: 动态规划(优化前)

Screen Shot 2023-08-25 at 6.27.10 PM.png

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

Screen Shot 2023-08-25 at 6.31.43 PM.png