简介
动态规划最核心两步:
- 状态表示:dp[i]代表什么
- 状态转移方程:如何利用已有的dp求解dp[i]
只要这两步搞对了, 就完成了动态规划的%95
剩下的就是细节问题:
- dp初始化顺序(有时是倒序)
- 处理边界问题(n过小则直接返回,避免越界)
这是动态规划第一小节, 简单介绍下dp主要类型
- 斐波那契数列模型
- 路径问题
- 简单多状态 dp 问题
- 子数组系列
- 子序列问题
- 回文串问题
- 两个数组的 dp 问题
- 01 背包问题
- 完全背包问题
- 二维费用的背包问题
- 似包非包
- 卡特兰数

一.斐波那契数列模型
1.第N个泰伯纳妾数
link:1137. 第 N 个泰波那契数 - 力扣(LeetCode)
code
class Solution {
public:
    int tribonacci(int n) {
        if(n == 0) return n;
        if(n == 1 || n == 2) return 1;
        // 动态规划
        vector<int> dp(n + 1, -1);
        // init
        dp[0] = 0; dp[1] = dp[2] = 1;
        for(int i = 3; i < dp.size();i++)
        {
            dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
        }
        return dp[n];
    }
};2.三步问题
link:面试题 08.01. 三步问题 - 力扣(LeetCode)
code
class Solution {
public:
    const int MOD = 1e9 + 7;
    int waysToStep(int n) {
        // 状态表示:dp[n] 表示上n节台阶的方式数
        // 状态转移:dp[n] = dp[n - 1] + dp[n - 2] + dp[n - 3];
        vector<int> dp(n + 5 , -1);
        // init dp
        dp[1] = 1; dp[2] = 2; dp[3] = 4;
        if(n <= 3) return dp[n];
        for(int i = 4; i < dp.size(); i++)
        {
            dp[i] = ((dp[i - 1] + dp[i - 2])%MOD + dp[i - 3])%MOD;
        }
        return dp[n];
    }
};3.使用最小花费爬楼梯
link:746. 使用最小花费爬楼梯 - 力扣(LeetCode)
code
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        // 状态表示:dp[i]表示爬到第i台阶需要的最小费用/不包括i台阶
        // 状态转移:dp[i] = min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1])
        vector<int> dp(cost.size() + 1, -1);
        // init dp
        dp[0] = dp[1] = 0;
        // dp
        for(int i = 2; i < dp.size(); i++)
        {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[cost.size()];
    }
};4.解码方法
code
class Solution {
public:
    int numDecodings(string s) {
        // 状态表示:dp[i]表示s[0,i](包含s[i])解码方法总数
        // 状态转移: dp[i] = valid(s[i])?dp[i - 1]:0 + valid(s[i-1,i]?dp[i - 2]:0
        if(s.size() == 1)
        {
            return valid(string(1, s[0])) ? 1 : 0;
        }
        if(s.size() == 2)
        {
            return (valid(string(1, s[0])) && valid(string(1, s[1]))) + valid(s.substr(0, 2));
        }
        vector<int> dp(s.size(), 0);
        // init dp
        dp[0] = valid(string(string(1, s[0]))) ? 1 : 0;
        dp[1] = (valid(string(1, s[0])) && valid(string(1, s[1]))) + valid(s.substr(0, 2));
        // dp
        for(int i = 2; i < dp.size(); i++)
        {
            dp[i] = (valid(string(1, s[i])) ? dp[i - 1] : 0) + 
            (valid(s.substr(i-1, 2))?dp[i - 2] : 0);
        }
        return dp[s.size() - 1];
    }
    bool valid(string s)
    {
        if(s.size() == 1) return s[0] >= '1' && s[0] <= '9';
        else// s.size()==2
        {
            return stoi(s) >= 10 && stoi(s) <= 26;
        }
    }
};二.路径问题
5.不同路径
tips:
此问题中, 我们可以多开一行和一列,从1下标开始初始有效值,
这样可以避免状态转移时的下标越界问题
code
class Solution {
public:
    vector<vector<int>> dp;
    int uniquePaths(int m, int n) {
        // dp[i][j]:从(0, 0)到(i, j)
        // dp[i][j] = dp[i-1][j] + dp[i][j-1]
        dp = vector<vector<int>>(m + 5, vector<int>(n + 5, 0));// dp此时可以不用-1标记是否初始化, 因为是按顺序初始化的
        // init dp
        dp[0][1] = 1;// 间接初始化dp[1][1], 防止for循环覆盖dp[1][1];
        // dp
        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][n];
    }
};6.不同路径II
link:63. 不同路径 II - 力扣(LeetCode)
code
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& grid) {
        // dp[i][j]:起点到(i, j)的路径数
        // dp[i][j] = grid[i][j] == 1 ? 0 : dp[i-1][j] + dp[i][j-1];
        // 注意dp多开一行和一列, 从下标1开始初始化
        vector<vector<int>> dp
        (grid.size() + 1, vector<int>(grid[0].size() + 1, 0));
        // init dp
        dp[0][1] = 1;// 间接初始化dp[1][1]
        // dp
        for(int i = 1; i <= grid.size(); i++)
        {
            for(int j = 1; j <= grid[0].size(); j++)
            {
                dp[i][j] = (grid[i-1][j-1] == 1 ? 0 : dp[i-1][j] + dp[i][j-1]);
            }
        }
        return dp[grid.size()][grid[0].size()];
    }
};7.珠宝的最高价值
link:LCR 166. 珠宝的最高价值 - 力扣(LeetCode)
code
class Solution {
public:
    int jewelleryValue(vector<vector<int>>& frame) {
        // dp[i][j]:走到(i, j)能拿到的最大珠宝
        // dp[i][j] = max(dp[i-1][j], dp[i][j-1])+frame[i][j];
        // 多开一行一列, 从下标1初始化
        vector<vector<int>> dp
        (frame.size() + 1, vector<int>(frame[0].size() + 1, 0));
        for(int i = 1; i < dp.size(); i++)
        {
            for(int j = 1; j < dp[0].size(); j++)
            {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])+frame[i-1][j-1];//注意dp和frame下标不一致
            }
        }
        return dp[frame.size()][frame[0].size()];
    }
};8.下降路径最小和
link:931. 下降路径最小和 - 力扣(LeetCode)
code
class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        // dp[i][j]:下降到(i, j)的下降路径最小和
        // dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1]) + matrix[i][j]
        // 多开两行两列, 从下标1开始初始化
        // 相当于上下左右都围上了一圈, 避免越界
        vector<vector<int>> 
        dp(matrix.size()+2, vector<int>(matrix[0].size()+2, INT_MAX));
        // init dp
        for(int i = 1; i <= matrix.size(); i++)
        {
            for(int j = 1; j <= matrix[0].size(); j++)
            {
                if(i==1)dp[i][j] = matrix[i-1][j-1];
                else
                {
                    dp[i][j] = matrix[i-1][j-1] + 
                    min(min(dp[i-1][j-1], dp[i-1][j]), 
                    dp[i-1][j+1]);
                }
            }
        }
        int ans = INT_MAX;
        for(int col = 1; col <= matrix[0].size(); col++)
        {
            ans = min(ans, dp[matrix.size()][col]);
        }
        return ans;
    }
};9.最小路径和
code
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        // dp[i][j]:从左上角到(i,j)的数字总和最小值
        // dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
        // dp多开一行一列, 从下标1开始初始化。防止下标越界
        vector<vector<int>> dp
        (grid.size()+1, vector<int>(grid[0].size()+1, INT_MAX));
        // init dp
        dp[0][1] = 0;// 间接初始化dp[1][1]
        // dp
        for(int i = 1; i <= grid.size(); i++)
        {
            for(int j = 1; j <= grid[0].size(); j++)
            {
                dp[i][j] = grid[i-1][j-1] + min(dp[i-1][j], dp[i][j-1]);
            }
        }
        return dp[grid.size()][grid[0].size()];
    }
};10.地下城游戏
link:174. 地下城游戏 - 力扣(LeetCode)
code1 & code2的dp状态表示稍有不同, 但是思路相同
code1
class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& g) {
        // 过程最小值:min(S1~Sn), 代表的是骑士在这条路上血量最低值(假设骑士初始血量0), 也就是所求所需最低初始的健康点数-1的相反数
        // dp[i][j] : 从(i, j)开始到终点的过程最小值
        // 不给dp开多余空间, dp时注意处理初始化与边界情况即可
        // 注意dp时反向dp
        vector<vector<int>> 
        dp(g.size(), vector<int>(g[0].size(), 0));
        // init dp
        // dp
        for(int i = g.size()-1; i >= 0; i--)
        {
            for(int j = g[0].size()-1; j >= 0; j--)
            {
                if(i==g.size()-1 && j==g[0].size()-1) // for内初始化
                {
                    dp[i][j] = g[i][j];
                    continue;
                }
                int val1 = i+1 < g.size() ? min(g[i][j], g[i][j] + dp[i+1][j]) : INT_MIN;// 处理边界情况
                int val2 = (j+1 < g[0].size())?min(g[i][j], g[i][j] + dp[i][j+1]):INT_MIN;
                dp[i][j] = max(val1, val2);
            }
            printf("\n");
        }
        return dp[0][0] >= 1 ? 1 : -dp[0][0] + 1;
    }
};code2
class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& d) {
        // dp[i][j]:从(i, j)出发到终点所需最低初始健康点数
        // dp[i][j] = min(dp[i][j+1], dp[i+1][j]) - d[i][j]
        // dp[i][j] = max(1, dp[i][j])
        // dp时init dp即可
        // 注意dp顺序, 倒序dp
        int m = d.size(), n = d[0].size();
        vector<vector<int>> dp
        (m+1, vector<int>(n+1, INT_MAX));
        // init dp
        dp[m-1][n] = dp[m][n-1] =  1;
        // dp
        for(int i = m-1; i >= 0; i--)
        {
            for(int j = n - 1; j >= 0; j--)
            {
                dp[i][j] = min(dp[i+1][j], dp[i][j+1]) - d[i][j];
                dp[i][j] = max(1, dp[i][j]);
            }
        }
        return dp[0][0];
    }
};三.简单多状态dp问题
dp时如何判断使用哪几种状态?
多状态dp中, 我们选择的dp状态们必须能覆盖所有情况, 这样,状态推导时可以保证条件充足,状态转移会很轻松
具体示例建议参考6.买卖股票的最佳时机
决策描述/状态描述?
决策描述是指用发生了什么决策/事件(买入/卖出)来标记/描述dp[i]
状态描述是指使用该位置可能所处的状态来描述dp[i]
某个位置的状态描述, 不仅可以用此位置的决策/发生事件(如买入/卖出)来标记状态, 还可以使用状态(如持有/不持有)来标记状态。
特别是当不是每个位置都可能发生决策/事件时,我们应采用状态来标记位置,这样能保证覆盖所有情况,而如果采用决策标记方法,则会遗漏不做决策的可能
1.按摩师
link:面试题 17.16. 按摩师 - 力扣(LeetCode)
code1_单一状态dp
class Solution {
public:
    int massage(vector<int>& nums) {
        // dp[i]:从num[i]开始(包含nums[i]), 最优预约时长
        // dp[i] = nums[i] + max(dp[i+2], dp[i+3])  //不可能dp[i+4]因为此时可以选dp[i+2]
        if(nums.size() < 3)
        {
            if(nums.size()==0)return 0;
            if(nums.size()==1)return nums[0];
            if(nums.size()==2)return max(nums[0], nums[1]);
        }
        vector<int> dp(nums.size(), 0);
        // init dp
        int sz = nums.size();
        dp[sz-1] = nums[sz-1];
        dp[sz-2] = nums[sz-2];
        dp[sz-3] = nums[sz-1] + nums[sz-3];
        // dp
        for(int i = sz - 4; i >= 0; i--)
        {
            dp[i] = nums[i] + max(dp[i + 2], dp[i + 3]);
        }
        return max(dp[0], dp[1]);
    }
};code2-多状态dp

class Solution {
public:
    int massage(vector<int>& nums) {
        // 多状态dp
        // f[i]:选择nums[i]
        // g[i]:不选择nums[i]
        // f[i] = g[i-1] + nums[i]
        // g[i] = max(f[i-1], g[i-1])
        if(nums.size()==0)return 0;
        if(nums.size()==1) return nums[0];
        vector<int> f(nums.size(), 0);
        vector<int> g(nums.size(), 0);
        // init dp
        f[0] = nums[0]; 
        g[0] = 0;
        // dp
        for(int i = 1; i < nums.size(); i++)
        {
            f[i] = g[i-1] + nums[i];
            g[i] = max(g[i-1], f[i-1]);
        }
        return max(g[nums.size()-1], f[nums.size()-1]);
    }
};2.打家劫舍
题意和上道题"按摩师"一样, 此题主要为了给打家劫舍II做铺垫
code
class Solution {
public:
    int rob(vector<int> nums)
    {
        // 不可连续, 每房子有偷和不偷两种状态
        // yes[i]: 偷nums[i]
        // no[i]:不偷nus[i]
        // yes[i] = no[i-1] + nums[i]
        // no[i] = max(no[i-1], yes[i-1])
        if(nums.size()==0)return 0;
        int sz = nums.size();
        vector<int> yes(sz);
        vector<int> no(sz);
        // init dp
        yes[0] = nums[0];
        no[0] = 0;
        // dp
        for(int i = 1; i < sz; i++)
        {
            yes[i] = no[i-1] + nums[i];
            no[i] = max(no[i-1], yes[i-1]);
        }
        return max(yes[sz-1], no[sz-1]);
    }
};3.打家劫舍II
link:213. 打家劫舍 II - 力扣(LeetCode)
code
class Solution {
public:
    int rob(vector<int>& nums) {
        // 通过分情况讨论是否偷第一家, 消除环形影响,将问题转化为rob1的线性结构
        int yes = nums.size() > 3 ? nums[0] + rob1({nums.begin() + 2, nums.end()-1}) : nums[0];// 偷第一家 // 就不能偷最后一家
        int no = rob1({nums.begin() + 1, nums.end()});// 不偷第一家
        printf("yes:%d, no:%d\n", yes, no);
        return max(yes, no);
    }
    int rob1(vector<int> nums)
    {
        // 不可连续, 每房子有偷和不偷两种状态
        // yes[i]: 偷nums[i]
        // no[i]:不偷nus[i]
        // yes[i] = no[i-1] + nums[i]
        // no[i] = max(no[i-1], yes[i-1])
        if(nums.size()==0)return 0;
        int sz = nums.size();
        vector<int> yes(sz);
        vector<int> no(sz);
        // init dp
        yes[0] = nums[0];
        no[0] = 0;
        // dp
        for(int i = 1; i < sz; i++)
        {
            yes[i] = no[i-1] + nums[i];
            no[i] = max(no[i-1], yes[i-1]);
        }
        return max(yes[sz-1], no[sz-1]);
    }
};4.删除并获得点数
link:740. 删除并获得点数 - 力扣(LeetCode)
code1_多状态dp模拟
class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        // 先将nums升序排序, 去重
        // 每个点两种状态:删除, 不删除
        // del[i]:删除num[i]情况下, 最大点数
        // sav[i]:不删除num[i]情况下, 最大点数
        // del[i] = (nums[i]-nums[i-1]>1)?max(sav[i-1], del[i-1]):sav[i-1]
        // sav[i] = nums[i] + ((nums[i]-nums[i-1]>1)?max(sav[i-1], del[i-1]):sav[i-1])
        // 预处理nums
        unordered_map<int, int> cnt;
        for(auto it = nums.begin(); it != nums.end(); it++)
        {
            cnt[*it]++;
        }
        sort(nums.begin(), nums.end());
        auto it = unique(nums.begin(), nums.end());// 也可以set去重
        nums.erase(it, nums.end());
        int sz = nums.size();
        if(sz == 0)return 0;
        // check nums
        for(int i = 0; i < sz; i++)
        {
            printf("%d ", nums[i]);
        }
        printf("\n");
        // init dp
        vector<int> del(sz);
        vector<int> sav(sz);
        del[0] = nums[0]*cnt[nums[0]];
        sav[0] = 0;
        for(int i = 1; i < sz; i++)
        {
            del[i] = nums[i]*cnt[nums[i]] + ((nums[i]-nums[i-1]>1)?max(sav[i-1], del[i-1]):sav[i-1]);
            sav[i] = max(sav[i-1], del[i-1]);
        }
        return max(del[sz-1], sav[sz-1]);
    }
};code2-转化为打家劫舍
class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        // 转化为打家劫舍I
        #define N (1e4 + 10)
        vector<int> arr(N, 0);
        // 预处理arr
        for(auto e:nums)arr[e] += e;// 相同数消去时可以累加, 所以+=
        return rob1(arr);
    }
    int rob1(vector<int> nums)
    {
        // 不可连续, 每房子有偷和不偷两种状态
        // yes[i]: 偷nums[i]
        // no[i]:不偷nus[i]
        // yes[i] = no[i-1] + nums[i]
        // no[i] = max(no[i-1], yes[i-1])
        if(nums.size()==0)return 0;
        int sz = nums.size();
        vector<int> yes(sz);
        vector<int> no(sz);
        // init dp
        yes[0] = nums[0];
        no[0] = 0;
        // dp
        for(int i = 1; i < sz; i++)
        {
            yes[i] = no[i-1] + nums[i];
            no[i] = max(no[i-1], yes[i-1]);
        }
        return max(yes[sz-1], no[sz-1]);
    }
};5.粉刷房子
link:LCR 091. 粉刷房子 - 力扣(LeetCode)
这是一道三状态dp问题
code
class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        // 每个房间三种状态:红 蓝 绿
        // r[i], b[i], g[i]:第n个房间为r, b, g时需要最小成本
        // r[i] = costs[i][0] + min(b[i-1], g[i-1])\
        // b[i] = costs[i][1] + min(r[i-1], g[i-1])
        // g[i] = costs[i][2] + min(r[i-1], b[i-1])
        int n = costs.size();
        vector<int> r(n), b(n), g(n);
        // init dp
        r[0] = costs[0][0];
        b[0] = costs[0][1];
        g[0] = costs[0][2];
        // dp
        for(int i = 1; i < n; i++)
        {
            r[i] = costs[i][0] + min(b[i-1], g[i-1]);
            b[i] = costs[i][1] + min(r[i-1], g[i-1]);
            g[i] = costs[i][2] + min(r[i-1], b[i-1]);
        }
        return min(min(r[n-1], b[n-1]), g[n-1]);
    }
};6.买股票的最佳时机含冷冻期
link:309. 买卖股票的最佳时机含冷冻期 - 力扣(LeetCode)
tips:
// 此问题中,必须要使用至少三种状态, 这样才能包含每种可能状态, 方便状态转移中推到当前状态。
// 如果只使用一种状态(持有股票)或两种状态(持有股票和不持有股票),都会给状态转移带来不必要麻烦,甚至将时间复杂度提升一个等级
// 你可以尝试在本题中只使用一种/两种状态,这会使状态转移时非常棘手
更正:以上注释掉的部分有误, 因为只用两种状态(持有/不持有股票)也可以覆盖dp[i]所有可能情况(和第七题 ,依旧是下一题状态表示一样), 关于冷冻期,调整no[i]刷新策略即可。具体写法见code2-两状态表示
虽然3/2状态都可以正常dp,但是一状态(只有持有状态)就不行了,因为不能覆盖dp[i]所有可能状态,不可状态转移(或者说非常棘手,还会将时间复杂度提升一个等级)

code1-三状态表示
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // f[i][0]:第i天后为可买入状态获得的最大利润 / 手里有股票
        // f[i][1]:第i天后为可卖出状态获得的最大利润 / 手里没有股票, 没冷冻期
        // f[i][2]:第i天后是冷冻期状态时获得的最大利润 / 冷冻期
        vector<vector<int>> f(prices.size(), vector<int>(3, 0));
        // init dp
        f[0][0] = -prices[0];
        f[0][1] = f[0][2] = 0;
        // dp
        for(int i = 1; i < prices.size(); i++)
        {
            f[i][0] = max(f[i-1][0], f[i-1][1] - prices[i]);
            f[i][1] = max(f[i-1][1], f[i-1][2]);// 昨天不能是有股票状态, 不然今天就i是冷冻期
            f[i][2] = f[i-1][0] + prices[i];
        }
        int n = prices.size();
        return max(f[n-1][0], max(f[n-1][1], f[n-1][2]));
    }
};code2-两状态表示
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // have[i]:第i天决策后持有股票
        // no[i]:第i天不决策后持有股票
        if(prices.size() <= 1)return 0;
        int n = prices.size();
        vector<int> have(n+1, 0), no(n+1, 0);
        // init dp
        have[0] = -prices[0];
        no[0] = 0;
        have[1] = max(-prices[1], -prices[0]);
        no[1] = max(0, prices[1]-prices[0]);
        // dp
        for(int i = 2; i < n; i++)
        {
            have[i] = max(have[i-1], no[i-2] - prices[i]);
            no[i] = max(have[i-1]+prices[i], no[i-1]);
        }
        return no[n-1];
    }
};7.买股票的最佳时机含手续费
link:714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode)
code
class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        // have[i]:第i天决策后有股票
        // no[i]:第i天决策后没有股票
        int n = prices.size();
        vector have(n, 0);
        vector no(n, 0);
        // init dp
        have[0] = -prices[0];
        no[0] = 0;
        // dp
        for(int i = 1; i < n; i++)
        {
            have[i] = max(have[i-1], no[i-1] - prices[i]);
            no[i] = max(no[i-1], have[i-1] + prices[i] - fee);
        }
        return no[n-1];
    }
};8.买卖股票的最佳时机III
link:123. 买卖股票的最佳时机 III - 力扣(LeetCode)
注意本题是的dp状态是二维的
状态机:

code
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // 二维dp状态, 完全覆盖所有可能状态
        // have[i][j]: 第i天后, 持有股票,且已经完成了j次交易
        // no[i][j]: 第i天后, 不持有股票, 且已经完成了j次交易
        // have[i][j] = max(have[i-1][j], no[i-1][j]);
        // no[i][j] = max(no[i-1][j], have[i-1][j-1]);
        #define MAXN 0x3f3f3f3f
        int n = prices.size();
        vector<vector<int>> have(n, vector(3, -MAXN));
        auto no = have;
        // init dp
        have[0][0] = -prices[0];
        no[0][0] = 0;
        // dp
        for(int i = 1; i < have.size(); i++)
        {
            for(int j = 0; j < have[0].size(); j++)
            {
                have[i][j] = max(have[i-1][j], no[i-1][j] - prices[i]);
                no[i][j] = max(no[i-1][j], j-1>=0?have[i-1][j-1] + prices[i]:-MAXN);
            }
        }
        // return ans
        int ans = 0;
        for(int cnt = 0; cnt < have[0].size(); cnt++)
        {
            ans = max(ans, no[n-1][cnt]);
        }
        return ans;
    }
};9.买卖股票的最佳时机IV
link:188. 买卖股票的最佳时机 IV - 力扣(LeetCode)
和上题III一样,不过2改为k
code
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
         // 二维dp状态, 完全覆盖所有可能状态
        // have[i][j]: 第i天后, 持有股票,且已经完成了j次交易
        // no[i][j]: 第i天后, 不持有股票, 且已经完成了j次交易
        // have[i][j] = max(have[i-1][j], no[i-1][j]);
        // no[i][j] = max(no[i-1][j], have[i-1][j-1]);
        #define MAXN 0x3f3f3f3f
        int n = prices.size();
        vector<vector<int>> have(n, vector(k+1, -MAXN));
        auto no = have;
        // init dp
        have[0][0] = -prices[0];
        no[0][0] = 0;
        // dp
        for(int i = 1; i < have.size(); i++)
        {
            for(int j = 0; j < have[0].size(); j++)
            {
                have[i][j] = max(have[i-1][j], no[i-1][j] - prices[i]);
                no[i][j] = max(no[i-1][j], j-1>=0?have[i-1][j-1] + prices[i]:-MAXN);
            }
        }
        // return ans
        int ans = 0;
        for(int cnt = 0; cnt < have[0].size(); cnt++)
        {
            ans = max(ans, no[n-1][cnt]);
        }
        return ans;
    }
};四.子数组系列
1.最大子数组和
link:53. 最大子数组和 - 力扣(LeetCode)
tips:因为元素可能是负数, 则其序列和不具有单调性, 不可使用滑动窗口
code
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        // dp, 子数组系列问题
        // dp[i]:以i位置结尾的...
        // dp[i] = num[i] + max(0, dp[i-1]);
        vector<int> dp(nums.size(), 0);
        // init dp
        dp[0] = nums[0];
        // dp
        for(int i = 1; i < nums.size(); i++)
        {
            dp[i] = nums[i] + max(0, dp[i-1]);
        }
        // return ans
        int ans = dp[0];
        for(int i = 1; i < dp.size(); i++)
        {
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};2.环形子数组的最大和
link:
tip:
本题关键是有环, 增添了很多边界情况,使问题变得棘手。
和打架劫舍II一样, 我们可以通过分情况讨论消除环的影响,将其变为普通线性结构
两种情况如下:

所以,我们只需求出线性结构时(不成环),最大&最小子数组和即可
本题中,注意nums元素都是负数的情况,此时只返回nums中最大元素即可
code
class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        // 分情况讨论,转化为线性结构
        // f[i]:i结尾,(线性结构)非空子数组最大和
        // g[i]:i结尾,(线性结构)非空子数组最小和
        // f[i] = nums[i] + max(0, f[i-1]);
        // g[i] = nums[i] + min(0, g[i-1]); // i >0 && i < n-1
        int n = nums.size();
        int sum = 0;for(int i = 0; i < nums.size(); i++) sum+=nums[i];
        vector<int> f(n);
        auto g = f;
        // init dp
        f[0] = g[0] = nums[0];
        // dp
        for(int i = 1; i < f.size(); i++)
        {
            f[i] = nums[i] + max(0, f[i-1]);
            g[i] = nums[i] + min(0, g[i-1]);
        }
        // return ans;
        int ans = nums[0];
        for(int i = 1; i < f.size(); i++)
        {
            ans = max(ans, f[i]);
            if(g[i] != sum) ans = max(ans, sum - g[i]);// 防止g中出现选取所有元素时间(nums都是负数的情况)
        }
        return ans;
    }
};3.乘积最大子数组
link:152. 乘积最大子数组 - 力扣(LeetCode)
code
class Solution {
public:
    int maxProduct(vector<int>& nums) {
        // f[i]:以i结尾的绝对值最大的正数
        // g[i]:以i结尾的绝对值最大的负数
        // 这两个状态保证覆盖寻找 数组中成绩罪的的非空连续子数组 的所需值:前最大/最小值
        int n = nums.size();
        vector<int> f(n, 0);
        auto g = f;
        // init dp
        f[0] = g[0] = nums[0];
        // dp
        for(int i = 1; i < n; i++)
        {
            f[i] = max(f[i-1] * nums[i], max(nums[i], g[i-1] * nums[i]));
            g[i] = min(g[i-1] * nums[i], min(nums[i], f[i-1] * nums[i]));
        }
        // return ans;
        int ans = nums[0];
        for(int i = 0; i < n; i++)
        {
            ans = max(ans, f[i]);
        }
        return ans;
    }
};4.乘积为正数的最长子数组长度
link:1567. 乘积为正数的最长子数组长度 - 力扣(LeetCode)
code
class Solution {
public:
    int getMaxLen(vector<int>& nums) {
        // f[i], g[i]:以i结尾的 最长正/负 长度  
        int n = nums.size();
        vector<int> f(n, 0);
        auto g = f;
        // init dp
        f[0] = nums[0] > 0 ? 1 : 0;
        g[0] = nums[0] < 0 ? 1 : 0;
        if(nums[0] == 0) g[0] = f[0] = 0;
        // dp
        for(int i = 1; i < n; i++)
        {
            if(nums[i] == 0)
            {
                g[i] = f[i] = 0;
                continue;
            }
            else if(nums[i] > 0)
            {
                f[i] = 1 + f[i-1];
                g[i] = g[i-1]==0 ? 0 : 1 + g[i-1];
            }
            else // nums[i] < 0
            {
                g[i] = 1 + f[i-1];
                f[i] = g[i-1]==0 ? 0 : 1 + g[i-1];
            }
        }
        // return ans
        int ans = 0;
        for(int i = 0; i < n; i++)
        {
            ans = max(ans, f[i]);
        }
        return ans;
    }
};5.最长湍流子数组
link:978. 最长湍流子数组 - 力扣(LeetCode)
code
class Solution {
public:
    int maxTurbulenceSize(vector<int>& arr) {
        // dp[i]:以i结尾...
        if(arr.size() <= 1) return arr.size();
        int n = arr.size();
        vector<int> dp(n, 0);
        // init dp
        dp[0] = 1;
        dp[1] = arr[1] != arr[0] ? 2 : 1;
        int presub = arr[1] - arr[0];
        // dp
        for(int i = 2; i < n; i++)
        {
            int sub = arr[i] - arr[i-1];
            if(diff(sub, presub)) dp[i] = dp[i-1] + 1;
            else dp[i] = sub == 0 ? 1 : 2;
            presub = sub;
        }
        // return ans
        int ans = 0;
        for(int i = 0; i < n; i++)
        {
            ans = max(ans, dp[i]);
        }
        return ans;
    }
    bool diff(int a, int b)// 是否异号
    {
        if(a == 0 || b == 0) return false;
        return (a > 0) ^ (b > 0);
    }
};6.单词拆分
code
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        // dp[i]:以s[i]结尾的s的字串是否可被wordDict拼接而成
        unordered_set<string> set(wordDict.begin(), wordDict.end());
        int n = s.size();
        vector<bool> dp(n, false);
        // init dp
        dp[0] = set.count(s.substr(0, 1));
        // dp
        for(int i = 1; i < n; i++)
        {
            for(int j = i; j >= 0; j--)// j为最后一个单词的起始位置
            {
                if((j-1 >= 0 ? dp[j-1] : true) && set.count(s.substr(j, i-j+1)))
                {
                    dp[i] = true;
                    break;
                }
            }
        }
        // check dp
        printf("dp:\n");
        for(int i = 0; i < n; i++)
        {
            printf("dp[%d] : %d\n", i, dp[i]?1:0);
        }
        // return ans
        return dp[n-1];
    }
};7.环绕字符串中唯一的子字符串
link:467. 环绕字符串中唯一的子字符串 - 力扣(LeetCode)
code
class Solution {
public:
    int findSubstringInWraproundString(string s) {
        // dp[i]: s[i]结尾的不同非空字串个数中满足条件的不同非空字串
        // init dp
        int n = s.size();
        vector<int> dp(n, 1);
        // dp
        for(int i = 1; i < n; i++)
        {
            if(s[i]-s[i-1] == 1 || (s[i]=='a' && s[i-1]=='z')) dp[i] = 1 + dp[i-1];
            else dp[i] = 1;
        }
        // 去重
        int hash[128];
        memset(hash, 0, sizeof hash);
        for(int i = 0; i < dp.size(); i++)
        {
            hash[s[i]] = max(hash[s[i]], dp[i]);
        }
        // return ans
        int ans = 0;
        for(int i = 0; i < 126; i++)
        {
            ans += hash[i];
        }
        return ans;
    }
};五.子序列问题
子序列 & 子数组?
- 子数组要求连续
- 子序列不要求连续, 但要求相对位置不变
严格递增 & 不严格递增
- 严格递增:a[i] > a[i-1]
- 不严格递增:a[i] >= a[i-1]
1.最长递增子序列
link:300. 最长递增子序列 - 力扣(LeetCode)
code
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        // 动态规划
        // dp[i]表示:以nums[i]为最后一个元素的最长递增子序列
        vector<int> dp = vector<int>(nums.size() + 5, 1);
        // init dp
        dp[0] = 1;
        // dp
        for(int i = 1; i < nums.size(); i++)
        {
            for(int j = 0; j < i; j++)
            {
                if(nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        int ans = dp[0];
        for(int i = 0; i < nums.size(); i++)
        {
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};2.摆动序列
code
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        // f[i]:以nums[i]结尾的, 倒一减倒二大于0的, 最长子序列长度
        // g[i]:以nums[i]结尾的, 倒一减倒二小于0的, 最长子序列长度
        if(nums.size() <= 1) return nums.size();
        int n = nums.size();
        vector<int> f(n, 1);
        auto g = f;
        // init dp
        f[1] = nums[1] - nums[0] > 0 ? 2 : 1;// 上升结尾
        g[1] = nums[1] - nums[0] < 0 ? 2 : 1;
        // dp
        for(int i = 2; i < n; i++)
        {
            for(int j = 0; j < i; j++)
            {
                int sub = nums[i] - nums[j];
                if(sub > 0)
                {
                    f[i] = max(g[j] + 1, f[i]);
                }
                else if(sub < 0)
                {
                    g[i] = max(f[j] + 1, g[i]);
                }
            }
        }
        // return ans;
        int ans = 0;
        for(int i = 0; i < n; i++)
        {
            ans = max(ans, max(f[i], g[i]));
        }
        return ans;
    }
};3.最长递增子序列的个数
link:673. 最长递增子序列的个数 - 力扣(LeetCode)
code
class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        // dp[i][0]:nums[i]结尾的, 最长递增子序列的 长度
        // dp[i][1]:nums[i]结尾的, 最长递增子序列的 个数
        if(nums.size() <= 1) return nums.size();
        int n = nums.size();
        vector<vector<int>> dp(n, vector<int>(2, 1));
        // init dp
        // dp
        for(int i = 1; i < n; i++)
        {
            for(int j = 0; j < i; j++)
            {
                if(nums[i] > nums[j])
                {
                    if(dp[j][0] + 1 == dp[i][0]) dp[i][1]+=dp[j][1];
                    else if(dp[j][0] + 1 > dp[i][0])
                    {
                        dp[i][0] = dp[j][0] + 1;
                        dp[i][1] = dp[j][1];
                    }
                }
            }
        }
        // return ans;
        int maxlen = 1;
        for(int i = 0; i < dp.size(); i++)
        {
            maxlen = max(maxlen, dp[i][0]);
        }
        int cnt = 0;
        for(int i = 0; i < dp.size(); i++)
        {
            if(dp[i][0] == maxlen) cnt += dp[i][1];
        }
        return cnt;
    }
};4.最长数对链
link:646. 最长数对链 - 力扣(LeetCode)
code
class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        // 先排序 创造单调性 为使用dp创造条件
        // dp[i]:排序后, 以pairs[i]结尾的, 最长数对链的长度
        sort(pairs.begin(), pairs.end(), [](vector<int>& vc1, vector<int>& vc2){
            return vc1[0] < vc2[0];
        });
        int n = pairs.size();
        vector<int> dp(n, 1);
        // init dp
        // dp
        for(int i = 1; i < n; i++)
        {
            for(int j = 0; j < i; j++)
            {
                if(pairs[j][1] < pairs[i][0])
                {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
        }
        // return ans;
        return dp[n-1];
    }
};5.最长定差子序列
link:1218. 最长定差子序列 - 力扣(LeetCode)
key: 哈希表代替遍历查找arr[i]-d结尾的满足题意的子序列的最大长度, 降低时间复杂度
code
class Solution {
public:
    int longestSubsequence(vector<int>& arr, int d) {
        // dp[i]:以arr[i]结尾的, 最长等差(diff)子序列
        int n = arr.size();
        vector<int> dp(n, 1);
        unordered_map<int, int> hash;
        // init dp
        // dp
        for(int i = 0; i < n; i++)
        {
            // 使用hash表,避免遍历查找arr[i]-d结尾子序列最大长度
            dp[i] = hash[arr[i]-d] + 1;
            hash[arr[i]] = max(hash[arr[i]], dp[i]);
        }
        // return ans;
        int ans = 1;
        for(int i = 0; i < n; i++)
        {
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};6.最长的斐波那契子序列长度
link:873. 最长的斐波那契子序列的长度 - 力扣(LeetCode)
tip:
注意dp时的顺序, 先列后行
哈希表避免遍历查询元素下标
code
class Solution {
public:
    int lenLongestFibSubseq(vector<int>& arr) {
        // 二维dp
        // dp[i][j]:arr[i], arr[j]为最后两元素
        // 注意初始化顺序,先列后行
        int n = arr.size();
        vector<vector<int>> dp(n, vector<int>(n, 2));
        unordered_map<int, int> map;// 值:下标
        // init dp
        dp[0][0] = 1;
        for(int i = 0 ; i < n; i++)
        {
            map[arr[i]] = i;
        }
        // dp
        int ans = 2;
        for(int j = 2; j < dp.size(); j++)
        {
            for(int i = 1; i < j ; i++)
            {
                int sub = arr[j] - arr[i];
                if(map.count(sub) && sub < arr[i]) dp[i][j] = 1 + dp[map[sub]][i];
                else dp[i][j] = 2;
                ans = max(ans, dp[i][j]);
            }
        }
        // return ans
        printf("ans = %d\n", ans);
        return ans >= 3 ? ans : 0;
    }
};7.最长等差数列
link:1027. 最长等差数列 - 力扣(LeetCode)
tips:
- 和上题 最长斐波那契数列 很类似
- 如果只用dp[i]表示以nums[i]结尾的 最长等差子序列的 长度,写不出状态转移方程, 因为缺少了dp[j]对应的差值, 所以不能判断dp[i]连接在dp[j]后是否为等差数列
- 所以, 我们要用二维dp[i][j]表示:最后两项为nums[i],nums[j]的最长子序列长度
- 为什么不用dp[i], len[i]代替dp[i][j]?信息还是不充足, 写不出状态转移方程
&:注意初始化顺序, 只有先i后j才能同时正确地动态初始化hash
code
class Solution {
public:
    int longestArithSeqLength(vector<int>& nums) {
        
        int n = nums.size();
        vector<vector<int>> dp(n, vector<int>(n, 2));
        unordered_map<int, int> hash;
        // for(int i = 0; i < n; i++) hash[nums[i]] = i;
        // init dp
        // dp
        int ans = 2;
        for(int i = 0; i < n; i++)
        {
            for(int j = i + 1; j < n; j++)
            {
                int sub = nums[j] - nums[i];
                int target = nums[i] - sub;
                if(hash.count(target) && hash[target] < i)
                {
                    dp[i][j] = dp[hash[target]][i] + 1;
                    ans = max(ans, dp[i][j]);
                }
            }
            hash[nums[i]] = i;// 只有dp顺序为先i后j,才能正确初始化hash
        }
        // return ans
        return ans;
    }
};8.等差数列划分II-子序列
link:446. 等差数列划分 II - 子序列 - 力扣(LeetCode)
code
class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& nums) {
        // dp[i][j]:以nums[i], nums[j]结尾的等差子序列的 个数
        using ll = long long int;
        int n = nums.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        unordered_map<ll, vector<int>> hash;
        for(int i = 0; i < n; i++) hash[nums[i]].push_back(i);
        // init dp
        int ans = 0;
        for(int j = 2; j < n; j++)
        {
            for(int i = 1; i < j; i++)
            {
                ll a = 2 * ll(nums[i]) - nums[j];
                for(int idx:hash[a])
                {
                    if(idx < i) dp[i][j] += dp[idx][i] + 1;
                }
                ans += dp[i][j];
            }
        }
        return ans;
    }
};六.回文串问题
1.回文子串
code
class Solution {
public:
    int countSubstrings(string s) {
        // dp[i][j]:s[i:j]是否为回文串
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        // dp
        // 由下向上dp
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = i; j < n; j++)
            {
                if(s[i] != s[j])
                {
                    dp[i][j] = false;
                    continue;
                }
                if(i + 1 < j - 1)
                {
                    dp[i][j] = dp[i+1][j-1];
                }
                else
                {
                    dp[i][j] = true;
                }
            }
        }
        // return ans;
        int ans = 0;
        for(int i = 0; i < n; i++)
        {
            for(int j = i; j < n; j++)
            {
                ans += dp[i][j];
            }
        }
        return ans;
    }
};
2.最长回文字串
code
class Solution {
public:
    string longestPalindrome(string s) {
        // dp[i][j]:s[i][j]是否为回文字串
        int maxlen = 0;
        int bgn = 0;
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        // dp
        for(int i = n; i >= 0; i--)
        {
            for(int j = i; j < n; j++)
            {
                if(s[i] == s[j])
                {
                    dp[i][j] = i + 1 < j - 1 ? dp[i+1][j-1] : true;
                }
                else dp[i][j] = false;
                int len = j - i + 1;
                if(dp[i][j] && len > maxlen)
                {
                    maxlen = len;
                    bgn = i;
                }
            }
        }
        printf("bgn = %d, maxlen = %d\n", bgn, maxlen);
        return s.substr(bgn, maxlen);
    }
};3.分割回文串
link:1745. 分割回文串 IV - 力扣(LeetCode)
code
class Solution {
public:
    bool checkPartitioning(string s) {
        // dp[i][j]:s[i:j]是否为回文串
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        // dp
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = i; j < n; j++)
            {
                if(s[i] == s[j])
                {
                    dp[i][j] = i + 1 < j - 1 ? dp[i+1][j-1] : true;
                }
            }
        }
        // return ans
        // 判断s[0:i], s[i+1:j], s[j+1:]是否为回文
        for(int i = 0; i < n - 2; i++)
        {
            for(int j = i + 1; j < n - 1; j++)
            {
                if(dp[0][i] && dp[i+1][j] && dp[j+1][n-1])
                {
                    return true;
                }
            }   
        }
        return false;
    }
};5.分割回文串II
link:132. 分割回文串 II - 力扣(LeetCode)
code
class Solution {
public:
    int minCut(string s) {
        #define INF 0x3f3f3f3f
        int n = s.size();
        vector<vector<bool>> huiwen(n, vector<bool>(n, false));
        // dp
        for(int i = n - 1; i >= 0; i--)
        {
            for(int j = i; j < n; j++)
            {
                if(s[i] == s[j])
                {
                    huiwen[i][j] = i + 1 < j - 1 ? huiwen[i+1][j-1] : true;
                }
            }
        }
        // dp
        vector<int> dp(n, INF);// dp[i]:s[:i]最少分割数
        for(int i = 0; i < n; i++)
        {
            if(huiwen[0][i])
            {
                dp[i] = 0;
                continue;
            }
            for(int j = 1; j <= i; j++)// dp[j:i]是最后一个回文串
            {
                if(huiwen[j][i])
                {
                    dp[i] = min(dp[i], dp[j-1] + 1);
                }
            }
        }
        return dp[n-1];
    }
};七.两个数组的dp问题
1.最长公共子序列
这是非常非常经典的一道两数组dp问题, 很有借鉴/学习意义。
link:1143. 最长公共子序列 - 力扣(LeetCode)
code
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        // dp[i][j]:text1[:i], text2[:j]最长公共子序列
        int n = text1.size();
        int m = text2.size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));// 空串方便初始化
        text1 = " " + text1;
        text2 = " " + text2;// 保证坐标对应
        // dp
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <=m; j++)
            {
                if(text1[i] == text2[j])
                dp[i][j] = dp[i-1][j-1] + 1;
                else
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
        return dp[n][m];
    }
};tips:
字符串/子序列中, 空串是有意义的,而且加上空串也会方便初始化,防止越界

2.不相交的线
其实就是最长公共子序列
link:1035. 不相交的线 - 力扣(LeetCode)
code
class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        // 就是求最长公共子序列
        // dp[i][j]:nums1[:i], nums2[:j]最大连线数
        int n = nums1.size();
        int m = nums2.size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
        nums1.insert(nums1.begin(), -1);
        nums2.insert(nums2.begin(), -2);// 保证下标对应
        // dp
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                if(nums1[i] == nums2[j])
                dp[i][j] = dp[i-1][j-1] + 1;
                else
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
        return dp[n][m];
    }
};3.不同的子序列
link:115. 不同的子序列 - 力扣(LeetCode)
key:
空串往往是有意义的,一定要考虑空串情况
init dp时,dp[i][0] = 1, 代表s[:i]中有一个" "
code
class Solution {
public:
    int numDistinct(string s, string t) {
        // dp[i][j]:在s[:i]中, t[j]出现的个数
        #define MOD (int(1e9 + 7))
        int n = s.size();
        int m = t.size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
        s = " " + s;
        t = " " + t;// 下标对应
        // init dp 注意, 空串也是有意义的, dp[i][0] = 1, 代表s[:i]中有一个" "
        dp[0][0] = 1;
        for(int i = 1; i <= n; i++) dp[i][0] = 1;
        // dp
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {   // 求s[:i]中, 多少次t[:j]
                if(s[i] == t[j]) dp[i][j] = (dp[i-1][j-1] + dp[i-1][j]) % MOD;// 以s[:i]结尾的t[:j]个数 + s[:i-1]内t[:j]个数
                else
                dp[i][j] = dp[i-1][j] % MOD;// s[:i-1]内t[:j]个数
            }
        }
        return dp[n][m];
    }
};4.通配符匹配
tips:
本题关键,p[j]=='*'时状态转移方程
本题有两种方法推导出上述答案(dp[i][j] = dp[i-1][j] || dp[i][j-1]):数学法和实际分析法

code
class Solution {
public:
    bool isMatch(string s, string p) {
        // dp[i][j]:s[:i]是否可以匹配p[:j]
        // key:p[j]=="*"时状态转移方程
        int n = s.size();
        int m = p.size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, false));
        s = " " + s;
        p = " " + p;
        // init dp
        dp[0][0] = true;// 空串可以匹配空串
        for(int i = 1; i <= m; i++) // *可匹配空串
        {
            if(p[i]=='*')
            {
                dp[0][i] = true;
            }
            else break;
        }
        // dp
        // for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            // for(int j = 1; j <= m; j++)
            for(int i = 1; i <= n; i++)
            {
                if(p[j] == '*') dp[i][j] = dp[i][j-1] || dp[i-1][j];
                else if(p[j] == s[i] || p[j] == '?')dp[i][j] = dp[i-1][j-1];
                else dp[i][j] = false; p[j]不是?*且p[j] != s[i]:一定不能匹配
            }
        }
        return dp[n][m];
    }
};5.正则表达式匹配
link:10. 正则表达式匹配 - 力扣(LeetCode)
和上题差不多, 只是*匹配规则有变化,导致p[j]=='*'时状态转移方程有些许小变化,但两题思路/做法相同
key:
dp init
p[j] == '*'时状态转移方程

code
class Solution {
public:
    bool isMatch(string s, string p) {
        // dp[i][j]: p[:j]能否匹配s[:i]
        int n = s.size();
        int m = p.size();
        vector<vector<bool>> dp(n + 1, vector<bool>(m + 1, false));
        s = " " + s;
        p = " " + p;// 下标对齐, 考虑空串
        // init dp
        dp[0][0] = true;// 空串可以匹配空串
        // p = ".*a*b*?*.........."时, 可匹配s=""(空)
        for(int j = 2; j <= m; j += 2) 
        {
            if(p[j] == '*') dp[0][j] = true;
            else break;
        }
        // dp
        for(int j = 1; j <= m; j++)
        {
            for(int i = 1; i <= n; i++)
            {
                if (p[j] == '*') 
                dp[i][j] = dp[i][j-2] || ((p[j-1] == '.' || p[j-1] == s[i]) && dp[i-1][j]);
                // key,分两种情况,*匹配0个前字符(dp[i][j-2])/
                // *匹配1至多个前字符((p[j-1] == '.' || p[j-1] == s[i]) &&dp[i-1][j])
                else if(p[j] == s[i] || p[j] == '.') dp[i][j] = dp[i-1][j-1];
                else dp[i][j] = false;
            }
        }
        return dp[n][m];
    }
};6.交错字符串
本题比较容易退出状态表示和状态转移方程
code
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        // dp[i][j]:s1[:i], s2[:j]是否可以交错形成s3[:i+j]
        if(s1.size() + s2.size() != s3.size()) return false;
        int n = s1.size();
        int m = s2.size();
        s1 = " " + s1;
        s2 = " " + s2;
        s3 = " " + s3;
        vector<vector<bool>> dp(n + 1, vector(m + 1, false));
        // init dp
        dp[0][0] = true;
        for(int i = 1; i <= n; i++) 
        {
            if(s1[i] == s3[i]) dp[i][0] = true;
            else break;
        }
        for(int j = 1; j <= m; j++) 
        {
            if(s2[j] == s3[j]) dp[0][j] = true;
            else break;
        }
        // dp
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                bool ret1 = s1[i]==s3[i+j] && dp[i-1][j];
                bool ret2 = s2[j]==s3[i+j] && dp[i][j-1];
                dp[i][j] = ret1 || ret2;
            }
        }
        return dp[n][m];
    }
};7.两个字符串的最小ASCII删除和
link:712. 两个字符串的最小ASCII删除和 - 力扣(LeetCode)
key:
正难则反, 将求最小删除ASCII值转化为求公共子序列最大ASCII值
状态转移时, 按照公共子序列最后一个字符是否有s1[i]和s2[j]两个字符,分情况即可

code
class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        // 正难则反, 求删除字符ASCII最小和, 就是求公共子序列的最大ASCII和
        // dp[i][j]:s1[:i], s2[:j]公共子序列的最大ASCII
        // 状态转移时, 根据公共子序列是否包含s1[i],s2[j]字符,分情况即可
        // 或者说, 公共子序列最后一个字符,是否是s1[i],s2[j]
        int n = s1.size(); 
        int m = s2.size();
        s1 = " " + s1;
        s2 = " " + s2;
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
        // init dp
        // 空串情况都是0, 创建dp时已经初始化
        // dp
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                if (s1[i] == s2[j]) dp[i][j] = dp[i-1][j-1] + s1[i];// s1[i],s2[j]都能用到
                else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);// s1[i],s2[j]至少一个用不到
            }
        }
        // return ans;
        int sum1 = 0, sum2 = 0;
        for(int i = 1; i <= n; i++) sum1 += s1[i];
        for(int j = 1; j <= m; j++) sum2 += s2[j];
        return sum1 + sum2 - 2 * dp[n][m];
    }
};8.最长重复子数组
link:718. 最长重复子数组 - 力扣(LeetCode)
tips:
这是一道比较简单的双数组dp问题
注意求子数组时的dp状态表示,dp[i][j]一定表示的是以nums1[i], nums2[j]为结尾的子数组。
这点不同于子序列,因为子序列不要求连续,所以子序序列问题的dp状态表示不要求也不能要求指定结尾
code
class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        // 由于本题是求子数组长度,子数组要求连续,所以dp[i][j]一定要保证是结尾状态, 不能像子序列一样的区间状态;
        // dp[i][j]:以nums1[i], nums2[j]结尾的,最长子数组的长度
        int n = nums1.size();
        int m = nums2.size();
        nums1.insert(nums1.begin(), -1);
        nums2.insert(nums2.begin(), -2);
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
        // dp
        int ans = 0;
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                if(nums1[i] == nums2[j])
                {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }
                ans = max(ans, dp[i][j]);
            }
        }
        return ans;
    }
};01背包问题
01背包问题是所有背包问题的基础
背包问题有很多条件/性质:
- 01背包/完全背包
- 不必装满 / 必须塞满
- ....
背包问题的条件很多,这些条件排列组合,使得背包问题有很多种
1.【模板】01背包
link:【模板】01背包_牛客题霸_牛客网
code
FULL中没有使用滚动数组优化
IsFULL中使用了滚动数组优化
#include <iostream>
#include <vector>
using namespace std;
int n, V;
vector<int> v, w;
int NotFull();
int Full();
int main() {
    cin>>n>>V;
    v = vector<int>(n + 1);// 注意下标对应
    w = v;
    for(int i = 1; i <= n; i++)
    {
        cin>>v[i]>>w[i];
    }
    // 注意,n个物体中,每个物体最多只能选择一次(01背包问题)
    int ans1 = NotFull();
    int ans2 = Full();
    cout<<ans1<<endl;
    cout<<ans2<<endl;
}
int NotFull()
{
    // dp[i][j]:前i个物品,背包容量j,最多能装多大价值的物品
    vector<int> dp = vector<int>(V + 1, 0); // 一行滚动数组
    // init dp
    // 已经初始化:dp第一行第一列默认为0,
    // dp
    for(int i = 1; i <= n; i++)
    {
        for(int j = V; j >= 1; j--)// 反向遍历
        {
            // 装下第i物品
            int val1 = j-v[i] >= 0 ? dp[j-v[i]] + w[i] : 0;
            // 不装i物品
            int val2 = dp[j];
            dp[j] = max(val1, val2);
        }
    }
    return dp[V];
}
int Full()
{
    // dp[i][j]:前i个物品,背包容量j,恰好装满,能装多大价值的物品
    vector<vector<int>> dp(n + 1, vector<int>(V + 1, 0));
    // init dp
    for(int j = 1; j <= V; j++) dp[0][j] = -1;// -1表示dp[i][j]不可能恰好装满
    // dp
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= V; j++)
        {
            int val1, val2;
            // 装下第i物品
            if(j - v[i] >= 0)
            {
                val1 = dp[i-1][j-v[i]] != -1 ? dp[i-1][j-v[i]] + w[i] : -1;
            }
            else val1 = -1;
            // 不装i物品
            val2 = dp[i-1][j];
            dp[i][j] = max(val1, val2);
        }
    }
    return dp[n][V] == -1 ? 0 : dp[n][V];
}
// 64 位输出请用 printf("%lld")滚动数组优化空间复杂度
可以仅使用一行空间存储滚动数组,但此时注意初始化顺序:从右向左,防止覆盖所需值
不必强行解释上述优化后的dp意义。知道如何从源代码优化即可,不必深究优化后意义
滚动数组优化步骤
非常简单, 记住一下两步即可:
- 删除dp的所有行坐标
- 列坐标从右向左遍历
