动态规划力扣题型总结

 
Category: DSA

C++ snippets

ostream& operator<<(ostream& os, const vector<int>& v) {
    for (auto i : v) os << i << " ";
    return os << endl;
}

ostream& operator<<(ostream& os, const vector<vector<int>>& v) {
    for (auto i : v) os << i;
    return os;
}

入门

[Overlapping Subproblems Property in Dynamic Programming DP-1 - GeeksforGeeks](https://www.geeksforgeeks.org/overlapping-subproblems-property-in-dynamic-programming-dp-1/);
  1. 剑指 Offer 10- I. 斐波那契数列 - 力扣(LeetCode);

    class Solution {
    public:
        int fib(int n) {
            const int MOD = 1e9 + 7;
            int a{}, b{1};
            for (int i{}; i < n; ++i) tie(a, b) = pair(b, a % MOD + b % MOD);
            return a % MOD;
        }
    };
    // 玩个骚的
    class Solution {
    public:
        int fib(int n) {
            int a{}, b{1};
            while (n--) tie(a, b) = pair{b, (a + b) % 1000000007};
            return a % 1000000007;
        }
    };
    
  2. 509. 斐波那契数 - 力扣(LeetCode);

    vector<int> a(31, -1);
    class Solution {
    public:
        int fib(int n) {
            if (a[n] == -1) {
                if (n <= 1)
                    a[n] = n;
                else
                    a[n] = fib(n - 1) + fib(n - 2);
            }
            return a[n];
        }
    };
    // 滚动数组优化
    class Solution {
    public:
        int fib(int n) {
            int a{}, b{1};
            for (int i{}; i < n; ++i) {
                int tmp = b;
                b += a;
                a = tmp;
            }
            return a;
        }
    };
    
  3. 70. 爬楼梯 - 力扣(LeetCode);剑指 Offer 10- II. 青蛙跳台阶问题

    class Solution {
    public:
        int climbStairs(int n) {
            if (n < 3) return n;
            vector<int> dp(n);
            dp[0] = 1, dp[1] = 2;
            for (int i{2}; i < n; ++i) dp[i] = dp[i - 1] + dp[i - 2];
            return dp[n - 1];
        }
    };
    // 滚动数组
    class Solution {
    public:
        int climbStairs(int n) {
            if (n < 3) return n;
            int a{1}, b{2};
            for (int i{2}; i < n; ++i) tie(a, b) = pair(b, a + b);
            return b;
        }
    };
    // 终极优化
    class Solution {
    public:
        int climbStairs(int n) {
            if (n < 3) return n;
            int a{1}, b{2}, tmp{};
            for (int i{2}; i < n; ++i) tmp = b, b += a, a = tmp;
            return b;
        }
    };
    
  4. 746. 使用最小花费爬楼梯 - 力扣(LeetCode);剑指 Offer II 088. 爬楼梯的最少成本 - 力扣(LeetCode);

    class Solution {
    public:
        int minCostClimbingStairs(vector<int> &cost) {
            int n = cost.size();
            vector<int> dp(n + 1);
            for (int i{2}; i <= n; ++i)
                dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
            return dp[n];
        }
    };
    // 滚动数组
    class Solution {
    public:
        int minCostClimbingStairs(vector<int> &cost) {
            int n = cost.size(), a{}, b{}, tmp{};
            for (int i{}; i < n - 1; ++i)
                tmp = b, b = min(b + cost[i + 1], a + cost[i]), a = tmp;
            return b;
        }
    };
    
  5. 343. 整数拆分 - 力扣(LeetCode);(还有$O(1)$的数学做法)

    class Solution {
    public:
        int integerBreak(int n) {
            vector<int> dp(n + 1);
            dp[2] = 1;
            for (int i{3}; i <= n; ++i)
                // 这里进行了优化, 原来是j<i, 浪费时间
                // 优化的依据是满足最大乘积的拆分数字尽可能接近
                for (int j{1}; j < 1 + i / 2; ++j)
                    dp[i] = max({dp[i], j * dp[i - j], j * (i - j)});
            return dp[n];
        }
    };
    

路径问题

  1. 62. 不同路径 - 力扣(LeetCode);(经典的二维DP问题)

    class Solution {
    public:
        int uniquePaths(int m, int n) {
            vector<vector<int>> dp(m, vector<int>(n));
            for (int i{}; i < n; ++i) dp[0][i] = 1;
            for (int j{1}; j < m; ++j) dp[j][0] = 1;
            for (int i{1}; i < m; ++i)
                for (int j{1}; j < n; ++j) dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            return dp[m - 1][n - 1];
        }
    };
    

    滚动数组优化:

    class Solution {
    public:
        int uniquePaths(int m, int n) {
            vector<int> dp(n, 1);
            for (int i{1}; i < m; ++i)
                for (int j{1}; j < n; ++j) dp[j] += dp[j - 1];
            return dp[n - 1];
        }
    };
    

    组合数学公式方法也可以:(注意这里ans=ans*...是必须的, 因为如果写成ans*=...会导致除法舍入)

    class Solution {
    public:
        int uniquePaths(int m, int n) {
            long ans{1};
            for (int i{}; i < m - 1; ++i) ans = ans * (n + i) / (i + 1);
            return ans;
        }
    };
    
  2. 63. 不同路径 II - 力扣(LeetCode); 先写一个记忆化搜索

    class Solution {
    public:
        int uniquePathsWithObstacles(vector<vector<int>>& g) {
            int m = g.size(), n = g[0].size();
            map<pair<int, int>, int> st; // unordered_map不能用 pair 作为键
            if (g[m - 1][n - 1] || g[0][0]) return 0;
            function<int(int, int)> f = [&](int x, int y) {
                int& ans = st[{x, y}];
                if (ans) return ans;
                if (x >= m || y >= n || g[x][y]) // 有石头
                    return 0;
                if (x == m - 1 && y == n - 1) return 1;
                ans = f(x + 1, y) + f(x, y + 1);
                return ans;
            };
            return f(0, 0);
        }
    };
    

    然后翻译成递推

    class Solution {
    public:
        int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
            int m = obstacleGrid.size(), n = obstacleGrid[0].size();
            if (obstacleGrid[0][0]) return 0; // 注意初始值为障碍物, 后面不需要考虑
         
            vector<vector<int>> dp(m, vector<int>(n));
            int i{}, j{1};
            for (; i < n && obstacleGrid[0][i] != 1; ++i) dp[0][i] = 1;
            for (; j < m && obstacleGrid[j][0] != 1; ++j) dp[j][0] = 1;
            for (i = 1; i < m; ++i)
                for (j = 1; j < n; ++j)
                    if (obstacleGrid[i][j] == 0)
                        dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            return dp[m - 1][n - 1];
        }
    };
    

    同样可以滚动数组优化:(有点技巧, 参考了官方题解)

    class Solution {
    public:
        int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
            int m = obstacleGrid.size(), n = obstacleGrid[0].size();
            vector<int> dp(n);
            dp[0] = obstacleGrid[0][0] != 1;
            for (int i{}; i < m; ++i)
                for (int j{}; j < n; ++j) {
                    if (obstacleGrid[i][j]) {
                        dp[j] = 0;
                        continue;
                    }
                    if (j && obstacleGrid[i][j - 1] == 0) dp[j] += dp[j - 1];
                }
            return dp[n - 1];
        }
    };
    
  3. 64. 最小路径和 - 力扣(LeetCode);

  4. 980. 不同路径 III - 力扣(LeetCode);

  5. 1824. 最少侧跳次数 - 力扣(LeetCode);

  6. 剑指 Offer 47. 礼物的最大价值 - 力扣(LeetCode);(二维dp的经典问题, 路径求和找最大即可)

    class Solution {
    public:
        int maxValue(vector<vector<int>>& grid) {
            int m = grid.size(), n = grid[0].size();
            // dp
            vector<vector<int>> dp(m, vector<int>(n));
            // init
            for (int i{}; i < n; ++i)
                dp[0][i] += grid[0][i] + (i ? dp[0][i - 1] : 0);
         
            for (int i{1}; i < m; ++i) dp[i][0] += grid[i][0] + dp[i - 1][0];
         
            for (int i{1}; i < m; ++i)
                for (int j{1}; j < n; ++j)
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
         
            return dp[m - 1][n - 1];
        }
    };
    // 使用本地数组
    class Solution {
    public:
        int maxValue(vector<vector<int>>& grid) {
            int m = grid.size(), n = grid[0].size();
            // init
            for (int i{1}; i < n; ++i) grid[0][i] += grid[0][i - 1];
            for (int i{1}; i < m; ++i) grid[i][0] += grid[i - 1][0];
         
            for (int i{1}; i < m; ++i)
                for (int j{1}; j < n; ++j)
                    grid[i][j] = max(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
            return grid[m - 1][n - 1];
        }
    };
    

买卖股票系列

  1. 121. 买卖股票的最佳时机;剑指 Offer 63. 股票的最大利润;
    class Solution {
    public:
        int maxProfit(vector<int>& prices) {
            int n = prices.size(), mn{prices[0]};
            vector<int> dp(n);
            for (int i{1}; i < n; ++i) {
                if (prices[i] < mn)
                    mn = prices[i], dp[i] = dp[i - 1];
                else
                    dp[i] = max(dp[i - 1], prices[i] - mn);
            }
            return dp[n - 1];
        }
    };
    // 空间优化
    class Solution {
    public:
        int maxProfit(vector<int>& prices) {
            int n = prices.size(), mn{prices[0]}, ans{}, tmp{};
            for (int i{1}; i < n; ++i) {
                if (prices[i] < mn)
                    mn = prices[i], ans = tmp;
                else
                    ans = max(tmp, prices[i] - mn);
                tmp = ans;
            }
            return ans;
        }
    };
    
  2. 122. 买卖股票的最佳时机 II; (贪心最快, 有利润就买卖, DP 需要考虑两种状态: 目前是否持有)

    class Solution {
    public:
        int maxProfit(vector<int>& prices) {
            int n = prices.size(), dp[n][2]; // 0 表示不持有, 1 表示持有
            dp[0][0] = 0, dp[0][1] = -prices[0];
            for (int i{1}; i < n; ++i) {
                dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
                dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            }
            return dp[n - 1][0];
        }
    };
    // 滚动数组
    class Solution {
    public:
        int maxProfit(vector<int>& prices) {
            int n = prices.size();
            int t0{}, t1{-prices[0]}, a0{}, a1{}; // 0 表示不持有, 1 表示持有
            for (int i{1}; i < n; ++i) {
                a0 = max(t0, t1 + prices[i]);
                a1 = max(t1, t0 - prices[i]);
                t0 = a0, t1 = a1;
            }
            return a0;
        }
    };
    
  3. 714. 买卖股票的最佳时机含手续费;(上一题的改进版)

    class Solution {
    public:
        int maxProfit(vector<int>& prices, int fee) {
            int n = prices.size(), ans0{}, ans1{}, t0{},
                t1{}; // 0 表示不持有, 1 表示持有
            t0 = 0, t1 = -prices[0];
            for (int i{1}; i < n; ++i) {
                ans0 = max(t0, t1 + prices[i] - fee);
                ans1 = max(t1, t0 - prices[i]);
                t0 = ans0, t1 = ans1;
            }
            return ans0;
        }
    };
    
  4. 123. 买卖股票的最佳时机 III; cpp

  5. 188. 买卖股票的最佳时机 IV;
 ```cpp
 
 ```
  1. 309. 最佳买卖股票时机含冷冻期;

  2. 给定初始资金, 买卖股票, 一个例子:

    例如:初始资金M= 10000元 连续N=7天的价格分别是(单位:元)1.0, 2.0,1.0, 2.0,2.0, 3.0,2.0 最大交易次数K为2 最大收益为:50000元 注解:第一天1元买入,得10000股,第二天2元卖出得20000元,第三天1元买入, 得20000股,第六天卖出得60000元,最终盈利50000元。

    #include <vector>
    #include <algorithm>
    #include <iostream>
    #include <cstring>
    using namespace std;
          
    class Solution {
    public:
        double maxProfit(double origin, vector<double>& prices, int k) {
            // cache + dfs
            // int n = prices.size(), cache[n][k+1][2];
            // function<int(int, int, int)> dfs = [&](int i, int j, int hold) -> int
            // {
            //     if ( i<0 )
            //         return hold ? -INT_MAX/2 : origin;
            //     if ( j<0 )
            //         return -INT_MAX/2;
            //     if ( cache[i][j][hold]!=-1 )
            //         return cache[i][j][hold];
            //     if ( hold )
            //         return cache[i][j][hold] = max(dfs(i-1, j-1,
            //         false)/prices[i], dfs(i-1, j, true));
            //     return cache[i][j][hold] = max(dfs(i-1, j, true)*prices[i],
            //     dfs(i-1, j, false));
            // };
          
            // // iteration
            // int n = prices.size();
            // double f[n+1][k+2][2];
            // memset(f, -(double)0x3f, sizeof(f));
            // for ( int j=0;j<=k+1;++j ) f[0][j][false] = origin;
            // for ( int i=0;i<n;++i ) {
            //     for ( int j=0;j<k+1;++j ) {
            //         f[i+1][j+1][true] = max(f[i][j][false]/prices[i],
            //         f[i][j+1][true]); f[i+1][j+1][false] =
            //         max(f[i][j+1][true]*prices[i], f[i][j+1][false]);
            //     }
            // }
            // return f[n][k+1][false]-origin;
          
            // space optimized
            int n = prices.size();
            double f[k + 2][2];
            memset(f, -(double)0x3f, sizeof(f));
            for (int j = 0; j <= k + 1; ++j) f[j][false] = origin;
            for (int i = 0; i < n; ++i) {
                for (int j = k; j >= 0; --j) {
                    f[j + 1][true] = max(f[j][false] / prices[i], f[j + 1][true]);
                    f[j + 1][false] =
                        max(f[j + 1][true] * prices[i], f[j + 1][false]);
                }
            }
            return f[k + 1][false] - origin;
        }
    };
          
          
    int main(int argc, char const* argv[]) {
        Solution s;
        vector<double> v{1, 2, 1, 2, 2, 3, 2};
        cout << s.maxProfit(10000, v, 2);
        return 0;
    }
    

打家劫舍系列

  1. 198. 打家劫舍;剑指 Offer II 089. 房屋偷盗;
 ```cpp
 class Solution {
 public:
     int rob(vector<int>& nums) {
         int n = nums.size(), dp[n + 1];
         dp[0] = 0, dp[1] = nums[0];
         for (int i{2}; i <= n; ++i) {
             dp[i] = max(dp[i - 2] + nums[i - 1], dp[i - 1]);
         }
         return dp[n];
     }
 };
 // 
 class Solution {
 public:
     int rob(vector<int>& nums) {
         int n = nums.size(), ans{nums[0]}, a{}, b{ans};
         for (int i{2}; i <= n; ++i)
             ans = max(a + nums[i - 1], b), a = b, b = ans;
         return ans;
     }
 };
 ```
  1. 213. 打家劫舍 II; (考虑首尾) cpp class Solution { public: int rob(vector<int>& nums) { auto f = [&](int l, int r) { int ans{nums[l]}, a{}, b{ans}; for (int i{l + 2}; i <= r + 1; ++i) ans = max(a + nums[i - 1], b), a = b, b = ans; return ans; }; int n = nums.size(); if (n < 2) return nums[0]; return max(f(0, n - 2), f(1, n - 1)); } };

  2. 337. 打家劫舍 III; cpp

  3. 2560. 打家劫舍 IV; cpp

子序列/子数组

子序列

  1. 674. 最长连续递增序列 - 力扣(LeetCode);

    DP做法:

    class Solution {
    public:
        int findLengthOfLCIS(vector<int>& nums) {
            int n = nums.size(), dp[n], ans{1};
            memset(dp, 0, sizeof(dp));
            dp[0] = 1;
            for (int i{1}; i < n; ++i) {
                dp[i] = dp[i - 1] + (nums[i - 1] < nums[i]);
                ans = max(dp[i], ans);
            }
            return ans;
        }
    };
    

    贪心也能做:(最优方法, 空间复杂度$O(1)$)

    class Solution {
    public:
        int findLengthOfLCIS(vector<int>& nums) {
            int n = nums.size(), ans{1}, tmp{1};
            for (int i{1}; i < n; ++i) {
                if (nums[i - 1] < nums[i])
                    ++tmp;
                else
                    tmp = 1;
                ans = max(ans, tmp);
            }
            return ans;
        }
    };
    
  2. 300. 最长递增子序列 - 力扣(LeetCode);(贪心也可以做)

    class Solution {
    public:
        int lengthOfLIS(vector<int>& nums) {
            int n = nums.size();
            vector<int> dp(n, 1);
            for (int i{1}; i < n; ++i)
                for (int j{}; j < i; ++j)
                    if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1);
            return *max_element(dp.begin(), dp.end());
        }
    };
    
  3. 1143. 最长公共子序列 - 力扣(LeetCode);剑指 Offer II 095. 最长公共子序列 - 力扣(LeetCode);

    // 二维数组
    class Solution {
    public:
        int longestCommonSubsequence(string text1, string text2) {
            int m = text1.size(), n = text2.size();
            int dp[m + 1][n + 1];
            memset(dp, 0, sizeof(dp));
            for (int i{1}; i <= m; ++i)
                for (int j{1}; j <= n; ++j)
                    dp[i][j] = (text1[i - 1] == text2[j - 1])
                                   ? dp[i - 1][j - 1] + 1
                                   : max(dp[i][j - 1], dp[i - 1][j]);
            return dp[m][n];
        }
    };
    // 两个数组优化
    class Solution {
    public:
        int longestCommonSubsequence(string text1, string text2) {
            int m = text1.size(), n = text2.size();
            int dp[2][n + 1];
            memset(dp, 0, sizeof(dp));
            for (int i{}; i < m; ++i)
                for (int j{}; j < n; ++j)
                    dp[(i + 1) % 2][j + 1] =
                        (text1[i] == text2[j])
                            ? dp[i % 2][j] + 1
                            : max(dp[(i + 1) % 2][j], dp[i % 2][j + 1]);
            return dp[m % 2][n];
        }
    };
    // 一维数组优化
    class Solution {
    public:
        int longestCommonSubsequence(string text1, string text2) {
            int n = text2.size(), dp[n + 1];
            memset(dp, 0, sizeof(dp));
            for (char c : text1)
                for (int j{}, pre{}; j < n; ++j) {
                    int tmp = dp[j + 1];
                    dp[j + 1] = (c == text2[j]) ? pre + 1 : max(dp[j], dp[j + 1]);
                    pre = tmp;
                }
            return dp[n];
        }
    };
    
  4. 1035. 不相交的线 - 力扣(LeetCode);(和LCS类似, 转变一下思路, 直接拿来LCS的代码用)

    class Solution {
    public:
        int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
            int n1 = nums1.size(), n2 = nums2.size();
            int dp[n1 + 1][n2 + 1];
            memset(dp, 0, sizeof(dp));
            for (int i{1}; i <= n1; ++i) {
                for (int j{1}; j <= n2; ++j) {
                    if (nums1[i - 1] == nums2[j - 1])
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    else 
                        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
            return dp[n1][n2];
        }
    };
    
  5. 392. 判断子序列 - 力扣(LeetCode);(双指针也能做, DP普适性好, 编辑距离入门)

    class Solution {
    public:
        bool isSubsequence(string s, string t) {
            int n1 = s.size(), n2 = t.size();
            int dp[n1 + 1][n2 + 1]; // [0, i], [0, j]内s与t公共的子序列长度
            memset(dp, 0, sizeof(dp));
            for (int i{1}; i <= n1; ++i) {
                for (int j{1}; j <= n2; ++j) {
                    if (s[i - 1] == t[j - 1])
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    else 
                        dp[i][j] = dp[i][j - 1];
                }
            }
            return dp[n1][n2] == n1;
        }
    };
    // 剪枝一下
    class Solution {
    public:
        bool isSubsequence(string s, string t) {
            if (s.empty()) return true; // 需要考虑空字符串情况
            int n1 = s.size(), n2 = t.size();
            int dp[n1 + 1][n2 + 1]; // [0, i], [0, j]内s(与t公共)的子序列长度
            memset(dp, 0, sizeof(dp));
            for (int j{1}; j <= n2; ++j) {
                for (int i{1}; i <= n1; ++i) {
                    if (s[i - 1] == t[j - 1])
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    else
                        dp[i][j] = dp[i][j - 1];
                }
                if (dp[n1][j] == n1) return true;
            }
            return false;
        }
    };
    // 滚动数组
    class Solution {
    public:
        bool isSubsequence(string s, string t) {
            int n1 = s.size(), n2 = t.size();
            int dp[n2 + 1]; // [0, i], [0, j]内s与t公共的子序列长度
            memset(dp, 0, sizeof(dp));
            for (int i{1}; i <= n1; ++i) {
                for (int j{1}, pre{}; j <= n2; ++j) {
                    int tmp{dp[j]};
                    if (s[i - 1] == t[j - 1])
                        dp[j] = pre + 1;
                    else 
                        dp[j] = dp[j - 1];
                    pre = tmp;
                }
            }
            return dp[n2] == n1;
        }
    };
    
  6. 115. 不同的子序列 - 力扣(LeetCode);剑指 Offer II 097. 子序列的数目;

    class Solution {
    public:
        int numDistinct(string s, string t) {
            int m = s.size(), n = t.size();
            int dp[m + 1][n + 1];
            memset(dp, 0, sizeof(dp));
            for (int i{0}; i <= m; ++i) dp[i][0] = 1;
            for (int i{1}; i <= m; ++i)
                for (int j{1}; j <= n; ++j)
                    if (s[i - 1] == t[j - 1])
                        dp[i][j] = 1ll * dp[i - 1][j - 1] + dp[i - 1][j];
                    else
                        dp[i][j] = dp[i - 1][j];
            return dp[m][n];
        }
    };
    // 优化, 减少判断
    class Solution {
    public:
        int numDistinct(string s, string t) {
            int n1 = s.size(), n2 = t.size();
            int dp[n1 + 1][n2 + 1]; // [0, i]中t取[0, j]的子序列个数
            memset(dp, 0, sizeof(dp));
            for (int i{}; i <= n1; ++i) dp[i][0] = 1;
            for (int i{1}; i <= n1; ++i)
                for (int j{1}; j <= n2; ++j)
                    dp[i][j] = 1ll * (s[i - 1] == t[j - 1]) * dp[i - 1][j - 1] +
                               dp[i - 1][j];
         
            return dp[n1][n2];
        }
    };
    // 滚动数组
    class Solution {
    public:
        int numDistinct(string s, string t) {
            int m = s.size(), n = t.size();
            int dp[n + 1];
            memset(dp, 0, sizeof(dp));
            dp[0] = 1;
            for (int i{1}; i <= m; ++i)
                for (int j{1}, pre{1}; j <= n; ++j) { // pre 初始化为1
                    int tmp{dp[j]};
                    dp[j] += 1ll * (s[i - 1] == t[j - 1]) * pre;
                    pre = tmp;
                }
            return dp[n];
        }
    };
    
  7. 583. 两个字符串的删除操作 - 力扣(LeetCode);

    class Solution {
    public:
        int minDistance(string s, string t) {
            int m = s.size(), n = t.size();
            int dp[m + 1][n + 1]; // 使s[0:i]和t[0:j]相同的最少删除次数
            memset(dp, 0, sizeof(dp));
            // init
            for (int i{1}; i <= m; ++i) dp[i][0] = i;
            for (int i{1}; i <= n; ++i) dp[0][i] = i;
            // calc
            for (int i{1}; i <= m; ++i)
                for (int j{1}; j <= n; ++j) {
                    if (s[i - 1] != t[j - 1]) // 都要删
                        dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
                    else
                        dp[i][j] = dp[i - 1][j - 1];
                }
            return dp[m][n];
        }
    };
    // 滚动数组
    class Solution {
    public:
        int minDistance(string s, string t) {
            int m = s.size(), n = t.size(), i, j, pre, tmp;
            int dp[n + 1]; // 使s[0:i]和t[0:j]相同的最少删除次数
            memset(dp, 0, sizeof(dp));
            // init
            for (i = 1; i <= n; ++i) dp[i] = i;
            // calc
            for (i = 1; i <= m; ++i)
                // dp[0] = i初始化一定要加
                for (j = 1, pre = i - 1, dp[0] = i; j <= n; ++j) {
                    tmp = dp[j];
                    if (s[i - 1] != t[j - 1]) // 都要删
                        dp[j] = min(dp[j], dp[j - 1]) + 1;
                    else
                        dp[j] = pre;
                    pre = tmp;
                }
            return dp[n];
        }
    };
    
  8. 72. 编辑距离 - 力扣(LeetCode);(终极boss)

    class Solution {
    public:
        int minDistance(string word1, string word2) {
            int m = word1.size(), n = word2.size();
            int dp[m + 1][n + 1];
            memset(dp, 0, sizeof(dp));
            for (int i{1}; i <= m; ++i) dp[i][0] = i;
            for (int j{1}; j <= n; ++j) dp[0][j] = j;
            for (int i{1}; i <= m; ++i)
                for (int j{1}; j <= n; ++j)
                    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], dp[i - 1][j - 1]}) + 1;
            return dp[m][n];
        }
    };
    // 一维
    class Solution {
    public:
        int minDistance(string s, string t) {
            int m = s.size(), n = t.size();
            int dp[n + 1];
            memset(dp, 0, sizeof(dp));
            // init
            for (int j{1}; j <= n; ++j) dp[j] = j;
            // calc
            for (int i{1}; i <= m; ++i) {
                dp[0] = i;
                for (int j{1}, pre{i - 1}; j <= n; ++j) {
                    int tmp{dp[j]};
                    if (s[i - 1] == t[j - 1])
                        dp[j] = pre;
                    else
                        dp[j] = min(dp[j - 1], min(dp[j], pre)) + 1;
                    pre = tmp;
                }
            }
            return dp[n];
        }
    };
    
  9. $\bigstar$1092. 最短公共超序列;(难, 需要考虑从后往前遍历, 并且自己构造字符串)

    class Solution {
    public:
        string shortestCommonSupersequence(string s, string t) {
            int m = s.size(), n = t.size();
            // 存长度
            int dp[m + 1][n + 1];
            memset(dp, 0, sizeof(dp));
            // init
            for (int j{1}; j <= n; ++j) dp[0][j] = j;
            for (int i{1}; i <= m; ++i) dp[i][0] = i;
            // calc
            for (int i{1}; i <= m; ++i)
                for (int j{1}; j <= n; ++j)
                    if (s[i - 1] == t[j - 1])
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    else
                        dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
            /* str1 = "abac", str2 = "cab"
            0   1   2   3
            1   2   2   4
            2   3   4   3
            3   4   4   5
            4   4   5   6
            */
            // for (int i{}; i <= m; ++i) {
            //     for (int j{}; j <= n; ++j)
            //         cout<< dp[i][j]<<"\t";
            //     cout<<endl;
            // }
            // 构造字符串
            string ans{};
            int i{m}, j{n};
            while (i && j) {
                if (s[i - 1] == t[j - 1])
                    ans = s[--i] + ans, --j;
                else if (dp[i][j] == dp[i - 1][j] + 1)
                    ans = s[--i] + ans;
                else
                    ans = t[--j] + ans;
            }
            // 可不计s和t前面的顺序
            return t.substr(0, j) + s.substr(0, i) + ans;
        }
    };
    
  10. 1638. 统计只差一个字符的子串数目;

    class Solution {
    public:
        int countSubstrings(string s, string t) {
            int m = s.size(), n = t.size(), ans{};
            int dpl[m + 1][n + 1], dpr[m + 1][n + 1];
            memset(dpl, 0, sizeof(dpl)), memset(dpr, 0, sizeof(dpr));
            // 中心扩展, 找相等字符
            for (int i{m - 1}; i >= 0; --i)
                for (int j{n - 1}; j >= 0; --j)
                    dpr[i][j] = s[i] == t[j] ? dpr[i + 1][j + 1] + 1 : 0;
          
            for (int i{}; i < m; ++i)
                for (int j{}; j < n; ++j)
                    if (s[i] == t[j])
                        dpl[i + 1][j + 1] = s[i] == t[j] ? dpl[i][j] + 1 : 0;
                    else
                        ans += (dpl[i][j] + 1) * (dpr[i + 1][j + 1] + 1);
          
            return ans;
        }
    };
    
  11. 剑指 Offer II 093. 最长斐波那契数列;873. 最长的斐波那契子序列的长度;

  ```cpp
  ```

子数组(连续的子序列)

  1. 718. 最长重复子数组 - 力扣(LeetCode);

    class Solution {
    public:
        int findLength(vector<int> &nums1, vector<int> &nums2) {
            int n1 = nums1.size(), n2 = nums2.size();
            vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1));
            int ans{}, i{}, j{};
            for (i = 1; i <= n1; ++i)
                for (j = 1; j <= n2; ++j) {
                    if (nums1[i - 1] == nums2[j - 1])
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    if (dp[i][j] > ans) ans = dp[i][j];
                }
            return ans;
        }
    };
    // 滚动数组
    class Solution {
    public:
        int findLength(vector<int> &nums1, vector<int> &nums2) {
            int n1 = nums1.size(), n2 = nums2.size();
            vector<int> dp(n2 + 1);
            int ans{}, i{}, j{};
            for (i = 1; i <= n1; ++i)
                for (j = n2; j > 0; --j) {
                    if (nums1[i - 1] == nums2[j - 1])
                        dp[j] = dp[j - 1] + 1;
                    else
                        dp[j] = 0;
                    if (dp[j] > ans) ans = dp[j];
                }
            return ans;
        }
    };
    
  2. 53. 最大子数组和 - 力扣(LeetCode)

    class Solution {
    public:
        int maxSubArray(vector<int>& nums) {
            int n = nums.size();
            int dp[n];
            memset(dp, 0, sizeof(dp));
            dp[0] = nums[0];
            for (int i{1}; i < n; ++i) 
                dp[i] = max(dp[i - 1] + nums[i], nums[i]);
            return *max_element(dp, dp + n);
        }
    };
    // 滚动数组
    class Solution {
    public:
        int maxSubArray(vector<int>& nums) {
            int a, b{INT_MIN}, ans{INT_MIN};
            for (int num : nums) {
                a = num + max(b, 0);
                ans = max(a, ans);
                b = a;
            }
            return ans;
        }
    };
    
  3. 152. 乘积最大子数组;

    class Solution {
    public:
        int maxProduct(vector<int>& nums) {
            // 每次记录最大值(正), 最小值(负)
            int n = nums.size(), mxp[n], mnp[n];
            mxp[0] = mnp[0] = nums[0];
            for (int i{1}; i < n; ++i) {
                mnp[i] =
                    min(min(nums[i], mnp[i - 1] * nums[i]), mxp[i - 1] * nums[i]);
                mxp[i] =
                    max(max(nums[i], mxp[i - 1] * nums[i]), mnp[i - 1] * nums[i]);
            }
            return *max_element(mxp, mxp + n);
        }
    };
    

    滚动数组:

    class Solution {
    public:
        int maxProduct(vector<int>& nums) {
            int n = nums.size(), tx{nums[0]}, tn{nums[0]}, mx, mn, ans{nums[0]};
            for (int i{1}; i < n; ++i) {
                mn = min(min(nums[i], tn * nums[i]), tx * nums[i]);
                mx = max(max(nums[i], tx * nums[i]), tn * nums[i]);
                tx = mx, tn = mn, ans = max(ans, mx);
            }
            return ans;
        }
    };
    
  4. 1043. 分隔数组以得到最大和; (数组类经典动态规划)

    class Solution {
    public:
        int maxSumAfterPartitioning(vector<int> &arr, int k) {
            int n = arr.size();
            int dp[n + 1];
            memset(dp, 0, sizeof(dp));
            for (int i{1}; i <= n; ++i)
                for (int j{i}, mx{}; i - j < k && j > 0; --j)
                    mx = max(mx, arr[j - 1]),
                    dp[i] = max(dp[i], dp[j - 1] + (i - j + 1) * mx);
          
            return dp[n];
        }
    };
    
  5. 1186. 删除一次得到子数组最大和 - 力扣(Leetcode);

  6. 2606. 找到最大开销的子字符串 - 力扣(Leetcode);

  7. 918. 环形子数组的最大和 - 力扣(Leetcode);

          
    
  8. 2321. 拼接数组的最大分数 - 力扣(Leetcode);

  9. 6912. 构造最长非递减子数组 - 力扣(Leetcode);

    class Solution {
    public:
        int maxNonDecreasingLength(vector<int>& a, vector<int>& b) {
            int n = a.size(), ans{1};
            vector<vector<int>> dp(n, vector<int>(2, 1));
            for (int i{1}; i < n; ++i) {
                if (a[i - 1] <= a[i])
                    dp[i][0] = dp[i - 1][0] + 1;
                if (b[i - 1] <= b[i])
                    dp[i][1] = dp[i - 1][1] + 1;
                if (a[i - 1] <= b[i])
                    dp[i][1] = max(dp[i][1], dp[i - 1][0] + 1);
                if (b[i - 1] <= a[i])
                    dp[i][0] = max(dp[i][0], dp[i - 1][1] + 1);
                ans = max(ans, max(dp[i][0], dp[i][1]));
            }
            return ans;
        }
    };
    

多维数组

354. 俄罗斯套娃信封问题 - 力扣(LeetCode);

  

1691. 堆叠长方体的最大高度 - 力扣(LeetCode);

  

字符串

匹配问题($\bigstar$)

  1. 10. 正则表达式匹配;

    class Solution {
    public:
        bool isMatch(string s, string p) {
            int m = s.size(), n = p.size();
            bool dp[m + 1][n + 1];
            memset(dp, false, sizeof(dp));
            dp[0][0] = true;
         
            // 初始化第一行
            for (int j = 2; j <= n; j++) dp[0][j] = dp[0][j - 2] && p[j - 1] == '*';
         
            // 状态转移
            for (int i = 1; i <= m; i++) {
                for (int j = 1; j <= n; j++) {
                    if (p[j - 1] != '*')
                        dp[i][j] = dp[i - 1][j - 1] &&
                                   (s[i - 1] == p[j - 1] || p[j - 1] == '.');
                    else
                        dp[i][j] = dp[i][j - 2] ||
                                   (dp[i - 1][j] &&
                                    (s[i - 1] == p[j - 2] || p[j - 2] == '.'));
                }
            }
         
            return dp[m][n];
        }
    };
    
  2. 44. 通配符匹配;

    class Solution {
     public:
         bool isMatch(string s, string p) {
             int m = s.size(), n = p.size();
             bool dp[m + 1][n + 1];
             memset(dp, false, sizeof(dp));
             // 初始化
             dp[0][0] = true;
             // 仅当前面全为*时候才能匹配空字符串
             for (int i{1}; i <= n && p[i - 1] == '*'; ++i) dp[0][i] = true;
         
             for (int i{1}; i <= m; ++i) {
                 for (int j{1}; j <= n; ++j) {
                     if (s[i - 1] == p[j - 1] || p[j - 1] == '?')
                         dp[i][j] = dp[i - 1][j - 1];
                     else if (p[j - 1] == '*') // 使用*: (i-1, j), 不使用*: (i, j-1)
                         dp[i][j] = dp[i][j - 1] | dp[i - 1][j];
                 }
             }
             return dp[m][n];
         }
     };
    
  3. 32. 最长有效括号; 需要考虑前面的匹配情况.

    class Solution {
    public:
        int longestValidParentheses(string s) {
            if (s.empty()) return 0;
            int ans{}, n = s.size(), dp[n]; // 以s[i]结尾的最长有效括号的长度
            memset(dp, 0, sizeof(dp));
            for (int i{1}; i < n; ++i) {
                if (s[i] == '(') continue; // dp[i] = 0;
                if (s[i - 1] == '(')
                    dp[i] = (i > 1 ? dp[i - 2] : 0) + 2; // s[i]与s[i-1]匹配
                // 此时要考虑前面某一字符与s[i-1]组成有效字符串的情况
                else if (dp[i - 1] && i - dp[i - 1] && s[i - dp[i - 1] - 1] == '(')
                    dp[i] = dp[i - 1] + 2 +
                            (i - dp[i - 1] > 1 ? dp[i - dp[i - 1] - 2] : 0);
         
                ans = max(ans, dp[i]);
            }
            return ans;
        }
    };
    
  4. 1048. 最长字符串链;

         
    
  5. 91. 解码方法 - 力扣(Leetcode);

    class Solution {
    public:
        int numDecodings(string s) {
            int n = s.size(), dp[n + 1];
            memset(dp, -1, sizeof(dp));
            function<int(int)> f = [&](int x) {
                if (x <= 0)
                    return 1; // found
                int &res = dp[x];
                if (~res)
                    return res; // exists
                res = 0;
                if (s[x - 1] != '0')
                    res += f(x - 1);
                if (x > 1 && s[x - 2] != '0' && stoi(s.substr(x - 2, 2)) <= 26)
                    res += f(x - 2);
                return res;
            };
            return f(n);
        }
    };
    

    改成递推:

    class Solution {
    public:
        int numDecodings(string s) {
            int n = s.size(), dp[n + 1];
            memset(dp, 0, sizeof(dp));
            dp[0] = 1;
            for (int i{1}; i <= n; ++i) {
                if (s[i - 1] != '0')
                    dp[i] += dp[i - 1];
                if (i > 1 && s[i - 2] != '0' && stoi(s.substr(i - 2, 2)) <= 26)
                    dp[i] += dp[i - 2];
            }
            return dp[n];
        }
    };
    //
    class Solution {
    public:
        int numDecodings(string s) {
            int n = s.size(), a{}, b{1}, c{};
            for (int i{1}; i <= n; ++i) {
                a = 0;
                if (s[i - 1] != '0')
                    a += b;
                if (i > 1 && s[i - 2] != '0' && stoi(s.substr(i - 2, 2)) <= 26)
                    a += c;
                c = b, b = a;
            }
            return a;
        }
    };
    

回文问题

可以不用DP, 使用双指针或者马拉车都比DP快很多. 但是 dp 作为一种通解还是要掌握的.

  1. 647. 回文子串 - 力扣(LeetCode);

    class Solution {
    public:
        int countSubstrings(string s) {
            int n = s.size(), ans{};
            bool dp[n][n]; // s[i:j] 是否为回文
            memset(dp, false, sizeof(dp));
            for (int i{n - 1}; i >= 0; --i)
                for (int j{i}; j < n; ++j)
                    if (s[i] == s[j])
                        if (j - i <= 1) // 单一字符成回文
                            dp[i][j] = true, ans++;
                        else if (dp[i + 1][j - 1]) // 双字符
                            dp[i][j] = true, ans++;
            return ans;
        }
    };
    
  2. 5. 最长回文子串 - 力扣(LeetCode);(回文子串那个题的基础上加上更新最大值的操作即可)

    class Solution {
    public:
        string longestPalindrome(string s) {
            int n = s.size(), maxL{}, st{};
            bool dp[n][n];
            memset(dp, false, sizeof(dp));
            for (int i{}; i < n; ++i) dp[i][i] = true;
            for (int i{n - 1}; i >= 0; --i)
                for (int j{i}; j < n; ++j) {
                    if (s[i] == s[j])
                        if (j - i <= 1)
                            dp[i][j] = true;
                        else if (dp[i + 1][j - 1])
                            dp[i][j] = dp[i + 1][j - 1];
                    // 更新
                    if (dp[i][j] && j - i + 1 > maxL) maxL = j - i + 1, st = i;
                }
            return s.substr(st, maxL);
        }
    };
    
  3. 516. 最长回文子序列 - 力扣(LeetCode);

    class Solution {
    public:
        int longestPalindromeSubseq(string s) {
            int n = s.size();
            int dp[n][n];
            memset(dp, 0, sizeof(dp));
            for (int i{n - 1}; i >= 0; --i) dp[i][i] = 1;
            for (int i{n - 1}; i >= 0; --i)
                for (int j{i + 1}; j < n; ++j)
                    if (s[i] == s[j])
                        dp[i][j] = dp[i + 1][j - 1] + 2;
                    else
                        dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
            return dp[0][n - 1];
        }
    };
    // 滚动数组优化
    class Solution {
    public:
        int longestPalindromeSubseq(string s) {
            int n = s.size();
            int dp[n];
            memset(dp, 0, sizeof(dp));
            for (int i{}; i < n; ++i) dp[i] = 1;
            for (int i{n - 1}; i >= 0; --i)
                for (int j{i + 1}, pre{}; j < n; ++j) {
                    int tmp{dp[j]};
                    dp[j] = (s[i] == s[j]) ? pre + 2 : max(dp[j - 1], dp[j]);
                    pre = tmp;
                }
            return dp[n - 1];
        }
    };
    
  4. 131. 分割回文串;(用动态规划预处理回文子串)

    class Solution {
    public:
        vector<vector<string>> partition(string s) {
            int n = s.size();
            bool g[n][n]; // s[i:j] is Palindrome
            memset(g, 0, sizeof(g));
            for (int i{n - 1}; i >= 0; --i)
                for (int j{i}; j < n; ++j)
                    g[i][j] = (s[i] == s[j] && (j - i <= 1 || g[i + 1][j - 1]));
            vector<string> path;
            vector<vector<string>> ans;
            function<void(int)> f = [&](int x) { // x: start index
                if (x == n) {
                    ans.emplace_back(path);
                    return;
                }
                for (int i{}; i < n; ++i) {
                    if (!g[x][i]) continue;
                    path.emplace_back(s.substr(x, i - x + 1));
                    f(i + 1);
                    path.pop_back();
                }
            };
            f(0);
            return ans;
        }
    };
    
  5. 132. 分割回文串 II;(用到了上面预处理的方法)

    class Solution {
    public:
        int minCut(string s) {
            int n = s.size(), dp[n];
            bool g[n][n];
            memset(g, 0, sizeof(g));
            for (int i{n - 1}; i >= 0; --i)
                for (int j{i}; j < n; ++j)
                    g[i][j] = (s[i] == s[j] && (j - i <= 1 || g[i + 1][j - 1]));
            memset(dp, 0x3f, sizeof(dp)); // max cut
            for (int r{}; r < n; ++r) {
                if (g[0][r])
                    dp[r] = 0;
                else {
                    for (int l{}; l < r; ++l)
                        if (g[l + 1][r]) 
                            dp[r] = min(dp[r], dp[l] + 1);
                }
            }
            return dp[n - 1];
        }
    };
    

    单调性问题

    926. 将字符串翻转到单调递增;剑指 Offer II 092. 翻转字符;

树形 dp

换根 dp

834. 树中距离之和 - 力扣(Leetcode);

状态压缩 dp

主要处理子集问题..