【动态规划系列】路径问题、子数组 和 子序列问题、背包问题

文章目录

  • 一、斐波那契数列模型
    • [1. 第 N 个泰波那契数(详细拆解动态规划过程)](#1. 第 N 个泰波那契数(详细拆解动态规划过程))
    • [2. 使用最小花费爬楼梯](#2. 使用最小花费爬楼梯)
    • [3. 解码方法](#3. 解码方法)
  • 二、路径问题
    • [1. 不同路径(拆解路径问题的动态规划过程)](#1. 不同路径(拆解路径问题的动态规划过程))
    • [2. 不同路径 II](#2. 不同路径 II)
    • [3. 珠宝的最高价值](#3. 珠宝的最高价值)
    • [4. 下降路径最小和](#4. 下降路径最小和)
    • [5. 地下城游戏(与前4题的推导方法 不同)](#5. 地下城游戏(与前4题的推导方法 不同))
  • [三、简单多状态 dp 问题](#三、简单多状态 dp 问题)
    • [1. 按摩师](#1. 按摩师)
    • [2. 打家劫舍 II](#2. 打家劫舍 II)
    • [3. 删除并获得点数](#3. 删除并获得点数)
    • [4. 粉刷房子](#4. 粉刷房子)
    • [5. 买卖股票的最佳时机含冷冻期](#5. 买卖股票的最佳时机含冷冻期)
  • 四、子数组、子串系列(数组中连续的一段)
    • [1. 最大子数组和](#1. 最大子数组和)
    • [2. 环形子数组的最大和](#2. 环形子数组的最大和)
    • [3. 乘积最大子数组](#3. 乘积最大子数组)
    • [4. 单词拆分](#4. 单词拆分)
    • [5. 环绕字符串中唯一的子字符串](#5. 环绕字符串中唯一的子字符串)
  • 五、子序列问题(数组中不连续的一段)
    • [1. 最长递增子序列](#1. 最长递增子序列)
    • [2. 摆动序列](#2. 摆动序列)
    • [3. 最长递增子序列的个数](#3. 最长递增子序列的个数)
    • [4. 最长定差子序列](#4. 最长定差子序列)
    • [5. 最长等差数列(二维bp表)](#5. 最长等差数列(二维bp表))
    • [6. 最长的斐波那契子序列的长度](#6. 最长的斐波那契子序列的长度)
  • 六、回文子串问题
    • [1. 回文子串(模板题)](#1. 回文子串(模板题))
    • [2. 分割回文串 IV(hard)](#2. 分割回文串 IV(hard))
    • [3. 分割回文串 II(hard)](#3. 分割回文串 II(hard))
    • [4. 最长回文子序列](#4. 最长回文子序列)
  • [七、两个数组的 dp(含字符串数组)](#七、两个数组的 dp(含字符串数组))
    • [1. 最长公共子序列(模板题)](#1. 最长公共子序列(模板题))
    • [2. 不同的子序列(hard)](#2. 不同的子序列(hard))
    • [3. 交错字符串(子序列)](#3. 交错字符串(子序列))
    • [4. 最长重复子数组(子数组连续,模板题)](#4. 最长重复子数组(子数组连续,模板题))
  • 八、背包问题概述
    • [1. 01背包(模板题)](#1. 01背包(模板题))
    • [2. 分割等和子集](#2. 分割等和子集)
    • [3. 目标和(特殊)](#3. 目标和(特殊))
    • [4. 完全背包(模板题)](#4. 完全背包(模板题))
    • [5. 完全平方数](#5. 完全平方数)
    • [6. 一和零(二维限制条件的 01背包问题)](#6. 一和零(二维限制条件的 01背包问题))
    • [7. 组合总和 Ⅳ(似包非包)](#7. 组合总和 Ⅳ(似包非包))

一、斐波那契数列模型

1. 第 N 个泰波那契数(详细拆解动态规划过程)

1137. 第 N 个泰波那契数

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int tribonacci(int n) {
        if(n == 0)
            return 0;
        if(n == 1 || n == 2)
            return 1;

        vector<int> dp(n+1, 0);  // dp表

        // 初始化dp表
        dp[0] = 0, dp[1] = 1, dp[2] = 1;
        
        for(int i = 3; i <= n; i++)
            dp[i] = dp[i-3] + dp[i-2] + dp[i-1]; // 状态转义方程
        
        return dp[n];
    }
};

2. 使用最小花费爬楼梯

746. 使用最小花费爬楼梯

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        vector<int> dp(n+1, 0);
        
        // 初始化
        dp[0] = 0, dp[1] = 0;
        
        for(int i = 2; i <= n; i++)
        {
            // 状态转移方程
            dp[i] = min(dp[i-2] + cost[i-2], dp[i-1] + cost[i-1]);
        }

        return dp[n];
    }
};

3. 解码方法

91. 解码方法

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int numDecodings(string s) 
    {
        int n = s.size();
        vector<int> dp(n, 0);
 
        // 初始化dp[0] 和 dp[1]
        if(s[0] == '0')
            return 0;
        dp[0] = 1;
        if(n == 1)
            return dp[0]; 
            
        dp[1] = 1;
        if(s[1] == '0')
        {
            if(s[0] > '2')
                return 0;
        }
        else
        {
            if((s[1] <= '6' && s[0] == '2') || s[0] == '1')
                dp[1] = 2;
        }
        
        for(int i = 2; i < n; i++)
        {
            // 分情况使用 状态转移方程
            if(s[i] != '0')
                dp[i] += dp[i-1];
            if((s[i] <= '6' && s[i-1] == '2') || s[i-1] == '1')
                dp[i] += dp[i-2];
            if(dp[i] == 0)
                return 0;
        }

        return dp[n-1];
    }
};

二、路径问题

1. 不同路径(拆解路径问题的动态规划过程)

62. 不同路径

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // 创建dp表时,多开辟一行一列作为虚拟节点
        vector<vector<int>> dp(m+1, vector(n+1, 0));
        
        // 初始化虚拟节点
        dp[1][0] = 1;
        
        for(int i = 1; i < m+1; i++)
        {
            for(int j = 1; j < n+1; j++)
                dp[i][j] = dp[i-1][j] + dp[i][j-1]; // 状态转移方程
        }
        return dp[m][n];
    }
};

2. 不同路径 II

63. 不同路径 II

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        // 创建dp表时,多开辟一行一列作为虚拟节点
        vector<vector<long long>> dp(m+1, vector<long long>(n+1, 0));
        // 初始化虚拟节点
        dp[0][1] = 1;

        for(int i = 1; i < dp.size(); i++)
        {
            for(int j = 1; j < dp[0].size(); j++)
            {
                if(obstacleGrid[i-1][j-1] == 0)  // 对空位置进行 dp值计算
                    dp[i][j] = dp[i][j-1] + dp[i-1][j]; // 状态转移方程
            }
        }
        return dp[m][n];
    }
};

3. 珠宝的最高价值

LCR 166. 珠宝的最高价值

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int jewelleryValue(vector<vector<int>>& frame) {
        int m = frame.size();
        int n = frame[0].size();
        // 创建dp表
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));

        for(int i = 1; i < m+1; i++)
        {
            for(int j = 1; j < n+1; j++)
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + frame[i-1][j-1]; // 状态转移方程
        }

        return dp[m][n];
    }
};

4. 下降路径最小和

931. 下降路径最小和

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int n = matrix.size();
        
        // 创建dp表
        vector<vector<int>> dp(n, vector<int>(n, 0));
        // 初始化第一行的dp值
        for(int i = 0; i < n; i++)
            dp[0][i] = matrix[0][i];

        for(int i = 1; i < n; i++)
        {
            for(int j = 0; j < n; j++)
            {
                if(j == 0)
                    dp[i][j] = min(dp[i-1][j], dp[i-1][j+1]) + matrix[i][j];
                else if(j == n-1)
                    dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + matrix[i][j];
                else
                    dp[i][j] = min( min(dp[i-1][j-1], dp[i-1][j]), dp[i-1][j+1] ) + matrix[i][j]; // 状态转移方程
            }
        }
        
        // 对比最后一行的所有dp值,最小的一个就是最终结果
        int min = dp[n-1][0];
        for(int i = 1; i < n; i++)
        {
            if(dp[n-1][i] < min)
                min = dp[n-1][i];
        }

        return min;
    }
};

5. 地下城游戏(与前4题的推导方法 不同)

174. 地下城游戏

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int m = dungeon.size();
        int n = dungeon[0].size();
        vector<vector<int>> dp(m+1, vector<int>(n+1, INT_MAX));

        dp[m][n-1] = 1, dp[m-1][n] = 1;
        for(int i = m-1; i >= 0 ; i--)
        {
            for(int j = n-1; j >= 0; j--)
            {
                if(dp[i][j+1] > dp[i+1][j])
                    dp[i][j] = max( dp[i+1][j] - dungeon[i][j], 1);
                else
                    dp[i][j] = max( dp[i][j+1] - dungeon[i][j], 1);
            }
        }

        return dp[0][0];
    }
};

三、简单多状态 dp 问题

1. 按摩师

面试题 17.16. 按摩师

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int massage(vector<int>& nums) {
        if(nums.size() == 0)
            return 0;
            
        int n = nums.size();
        // dp表
        vector<int> f(n, 0);
        vector<int> g(n, 0);

        f[0] = nums[0];

        for(int i = 1; i < nums.size(); i++)
        {
            // 状态转移方程
            f[i] = g[i-1] + nums[i];
            g[i] = max(f[i-1], g[i-1]);
        }

        return max(f[n-1], g[n-1]);
    }
};

2. 打家劫舍 II

213. 打家劫舍 II

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();

        if(n == 1)
            return nums[0];

        vector<int> f(n, 0);
        vector<int> g(n, 0);

        // 1. 情况1
        // 初始化
        f[0] = nums[0];
        g[1] = f[0];
        // 实际上,0号房必选的情况下,无法选1号房,
        // 将 f[1]的值初始化成 与 g[1]一样,是为了便于后续推导,
        // 而且 由于f[1] 的值与 g[1]一样,后续无论选到谁 都是 g[1]的值,不影响后续结果
        f[1] = g[1];
        
        for(int i = 2; i < n-1; i++)
        {
            f[i] = g[i-1] + nums[i];
            g[i] = max(f[i-1], g[i-1]);
        }
        g[n-1] = max(f[n-2], g[n-2]);
        int max_money = g[n-1];
        
        // 1. 情况2
        // 初始化
        g[0] = 0;
        g[1] = g[0];
        f[1] = g[0] + nums[1];
        
        for(int i = 2; i < n; i++)
        {
            f[i] = g[i-1] + nums[i];
            g[i] = max(f[i-1], g[i-1]);
        }
        
        if(max(f[n-1],g[n-1]) > max_money)
            max_money = max(f[n-1],g[n-1]);

        return max_money;
    }
};

3. 删除并获得点数

740. 删除并获得点数

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        sort(nums.begin(), nums.end());

        vector<int> sums;

        int left = 0, right = 0;

        while(right < nums.size())
        {
            if(nums[right] != nums[left])
            {
                sums.push_back(nums[left] * (right - left));
                if(nums[right] - nums[left] > 1)
                    sums.push_back(0);
                left = right;   
            }
            right++;
        }
        sums.push_back(nums[left] * (right - left));

        // 进行动态规划
        int n = sums.size();

        vector<int> f(n, 0);
        vector<int> g(n, 0);
        f[0] = sums[0];

        for(int i = 1; i < n; i++)
        {
            f[i] = g[i-1] + sums[i];
            g[i] = max(f[i-1], g[i-1]);
        }

        return max(f[n-1], g[n-1]);
    }
};

4. 粉刷房子

LCR 091. 粉刷房子

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        int n = costs.size();
        // 创建dp表
        vector<int> f(n, 0);
        vector<int> g(n, 0);
        vector<int> h(n, 0);
        // 初始化
        f[0] = costs[0][0];
        g[0] = costs[0][1];
        h[0] = costs[0][2];

        for(int i = 1; i < n; i++)
        {
            // 状态转移方程
            f[i] = min(g[i-1], h[i-1]) + costs[i][0];
            g[i] = min(f[i-1], h[i-1]) + costs[i][1];
            h[i] = min(f[i-1], g[i-1]) + costs[i][2];
        }

        return min( min(f[n-1], g[n-1]), h[n-1] );
    }
};

5. 买卖股票的最佳时机含冷冻期

309. 买卖股票的最佳时机含冷冻期

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if(n == 1)
            return 0;
        // 创建dp表
        vector<int> f(n, 0);
        vector<int> g(n, 0);
        vector<int> h(n, 0);
        vector<int> d(n, 0);
        // 初始化
        f[0] = 0, g[0] = -prices[0];
        f[1] = f[0], g[1] = max(g[0], f[0] - prices[1]), h[1] = g[0] + prices[1]; 
        if(n > 2)
        {
            f[2] = f[1], g[2] = max(g[1], f[1] - prices[2]), h[2] = g[1] + prices[2], d[2] = h[1];
        }

        for(int i = 3; i < n; i++)
        {
            // 状态转移方程
            f[i] = max(f[i-1], d[i-1]);
            g[i] = max( max(g[i-1], d[i-1] - prices[i]), f[i-1] - prices[i]);
            h[i] = g[i-1] + prices[i];
            d[i] = h[i-1];
        }

        return max( max(f[n-1], g[n-1]), max(h[n-1], d[n-1]) );
    }
};

四、子数组、子串系列(数组中连续的一段)

1. 最大子数组和

53. 最大子数组和

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n, 0);

        dp[0] = nums[0];
        int max = dp[0];

        for(int i = 1; i < n; i++)
        {
            if(dp[i-1] < 0)
                dp[i] = nums[i];
            else
                dp[i] = dp[i-1] + nums[i];
            if(dp[i] > max)
                max = dp[i];
        }

        return max; 
    }
};

2. 环形子数组的最大和

918. 环形子数组的最大和

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        int n = nums.size();
        // dp表:以 i 位置为结尾的 最大和连续子数组(不考虑首尾相连)
        vector<int> dp(n, 0);

        // 初始化
        dp[0] = nums[0];

        // 找出 dp表 和 sp表中的最大值
        int max = dp[0];
        // 顺便计算数组总和
        int sum = nums[0];

        for(int i = 1; i < n; i++)
        {
            if(dp[i-1] < 0)
                dp[i] = nums[i];
            else
                dp[i] = dp[i-1] + nums[i];
            if(dp[i] > max)
                max = dp[i];
            sum += nums[i];
        }
        
        // fp表:以 i 位置为起点的 最小和连续子数组(不考虑首尾相连),用来推算sp表
        vector<int> fp(n, 0);
        // sp表:以 i 位置为结尾的 最大和连续子数组(只考虑 结果在数组首尾相连部分 的情况)
        vector<int> sp(n, 0);

        // 初始化
        fp[n-1] = nums[n-1];
        sp[n-1] = sum;
        if(sp[n-1] > max)
            max = sp[n-1];

        for(int i = n-2; i >= 0; i--)
        {
            if(fp[i+1] < 0)
            {
                fp[i] = fp[i+1] + nums[i];
                sp[i] = sum - fp[i+1];
            }
            else
            {
                fp[i] = nums[i];
                sp[i] = sum;
            }

            if(sp[i] > max)
                max = sp[i];
        }

        return max; 
    }
};

3. 乘积最大子数组

152. 乘积最大子数组

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int n = nums.size();
        
        // 创建dp表
        vector<int> d(n, 0);
        vector<int> f(n, 0);
        
        // 初始化
        d[0] = f[0] = nums[0];
        int max = d[0];

        for(int i = 1; i < n; i++)
        {
            if(nums[i] > 0 && f[i-1] <= 0)
                f[i] = nums[i] * f[i-1];
            else if(nums[i] < 0 && d[i-1] > 0)
                f[i] = nums[i] * d[i-1];
            else
                f[i] = nums[i];
            
            if(nums[i] > 0 && d[i-1] > 0)
                d[i] = nums[i] * d[i-1];
            else if(nums[i] < 0 && f[i-1] <= 0)
                d[i] = nums[i] * f[i-1];
            else
                d[i] = nums[i];
            
            if(d[i] > max)
                max = d[i];
        }

        return max;
    }
};

4. 单词拆分

139. 单词拆分

  • 代码解析:
cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        int n = s.size();
        
        // dp表
        vector<bool> dp(n+1, false);
        // 初始化
        dp[0] = true;

        for(int i = 0; i < n; i++)
        {
            bool flag = false;
            for(auto word : wordDict)
            {
                int sz = word.size();
                int left = i - sz + 1;
                if(left >= 0 && dp[left] == true)
                {                    
                    string str = s.substr(left, sz);
                    int n = strcmp(word.c_str(), str.c_str());
                    if(n == 0)
                    {
                        flag = true;
                        break;
                    }
                }
            }
            dp[i+1] = flag;
        }
        return dp[n];
    }
};

5. 环绕字符串中唯一的子字符串

467. 环绕字符串中唯一的子字符串

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int findSubstringInWraproundString(string s) {
        int n = s.size();
        // 创建 dp表
        vector<int> dp(n,0);
        // 初始化
        dp[0] = 1;
        // arr数组:用来去重
        int arr[26] = {0};
        arr[s[0] - 'a'] = 1;
        
        for(int i = 1; i < n; i++)
        {
            if( (s[i] - s[i-1] == 1) || (s[i-1] == 'z' && s[i] == 'a') )
                dp[i] = dp[i-1] + 1;
            else
                dp[i] = 1;
                
            if(dp[i] > arr[s[i] - 'a'])
                arr[s[i] - 'a'] = dp[i]; 
        }

        int sum = 0;
        for(int i = 0; i < 26; i++)
            sum += arr[i];

        return sum;
    }
};

五、子序列问题(数组中不连续的一段)

1. 最长递增子序列

300. 最长递增子序列

  • 代码示例:
cpp 复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        
        // 创建dp表
        vector<int> dp(n, 0);
        // 初始化
        dp[0] = 1;

        int length = 1;
        for(int i = 1; i < n; i++)
        {
            int len = 1;
            for(int j = 0; j < i; j++)
            {
                if(nums[j] < nums[i])
                {
                    if(dp[j] + 1 > len)
                        len = dp[j] + 1;
                }
            }
            dp[i] = len;
            if(dp[i] > length)
                length = dp[i];
        }

        return length;
    }
};

2. 摆动序列

376. 摆动序列

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();

        // 创建dp表
        vector<int> dp(n, 0);
        vector<int> sp(n, 0);
        // 初始化
        dp[n-1] = sp[n-1] = 1;

        int max_len = 1;

        for(int i = n-2; i >= 0; i--)
        {
            int dp_num = 1;
            int sp_num = 1;
            
            for(int j = i+1; j < n; j++)
            {
                if(nums[i] > nums[j])
                {
                    if(sp[j] + 1 > dp_num)
                        dp_num = sp[j] + 1;
                }
                if(nums[i] < nums[j])
                {
                    if(dp[j] + 1 > sp_num)
                        sp_num = dp[j] + 1;
                }
            }

            dp[i] = dp_num;
            if(dp[i] > max_len)
                max_len = dp[i];
            sp[i] = sp_num;
            if(sp[i] > max_len)
                max_len = sp[i];
        }

        return max_len;
    }
};

3. 最长递增子序列的个数

673. 最长递增子序列的个数

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n = nums.size();

        vector< pair<int, int> > dp(n, {0, 0});
        dp[0] = {1, 1};

        int arr[2] = {1, 1};
        for(int i = 1; i < n; i++)
        {
            int len = 1;
            int count = 1; 
            for(int j = 0; j < i; j++)
            {
                if(nums[i] > nums[j])
                {
                    if(dp[j].first + 1 > len)
                    {
                        len = dp[j].first + 1;
                        count = dp[j].second;
                    }
                    else if(dp[j].first + 1 == len)
                        count += dp[j].second;;
                }
            }
            dp[i] = {len, count};
            if(len > arr[0])
            {
                arr[0] = len;
                arr[1] = count;
            }
            else if(len == arr[0])
                arr[1] += count;
        }

        return arr[1];
    }
};

4. 最长定差子序列

1218. 最长定差子序列

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int longestSubsequence(vector<int>& arr, int difference) {
        int n = arr.size();
        
        // 创建 并 初始化dp表
        vector<int> dp(n, 1);
        
        // 创建hash表
        unordered_map<int, int> hash;
        // 统计dp表中的最大值
        int max = 1;

        for(int i = 1; i < n; i++)
        {
            hash[ arr[i-1] ] = i-1;

            auto iter = hash.find(arr[i] - difference);
            if(iter != hash.end())
                dp[i] = dp[ iter->second ] + 1;

            if(dp[i] > max)
                max = dp[i];
        }

        return max;
    }
};

5. 最长等差数列(二维bp表)

1027. 最长等差数列

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int longestArithSeqLength(vector<int>& nums) {
        int n = nums.size();
        // 创建 并 初始化dp表
        vector< vector<int> > dp(n, vector<int>(n, 2));
        
        // 创建hash表
        unordered_map<int, int> hash;
        // 统计dp表中的最大值
        int max = 2;

        for(int i = 1; i < n; i++)
        {
            hash[ nums[i-1] ] = i-1;
            for(int j = i + 1; j < n; j++)
            {
                int x = 2 * nums[i] - nums[j];
                auto it = hash.find(x);

                if(it != hash.end())
                {
                    dp[i][j] = dp[it->second][i] + 1;
                }

                if(dp[i][j] > max)
                    max = dp[i][j];
            }
        }

        return max;
    }
};

6. 最长的斐波那契子序列的长度

873. 最长的斐波那契子序列的长度

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();

        // 创建 并 初始化dp表
        vector< vector<int> > dp(n, vector<int>(n, 2));

        // 创建hash表
        unordered_map<int, int> hash;
        // 统计dp表中的最大值
        int max = 2;

        for(int i = 1; i < n; i++)
        {
            hash[ arr[i-1] ] = i-1;
            for(int j = i + 1; j < n; j++)
            {
                int x = arr[j] - arr[i];
                auto it = hash.find(x);

                if(it != hash.end())
                {
                    dp[i][j] = dp[it->second][i] + 1;
                }

                if(dp[i][j] > max)
                    max = dp[i][j];
            }
        }
        
        if(max < 3)
            return 0;

        return max; 
    }
};

六、回文子串问题

1. 回文子串(模板题)

647. 回文子串

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();
        // 创建 dp表
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        // 计算 dp表中 true 的总数
        int count = 0;
        
        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;
                        count++;
                    }
                    else
                    {
                        dp[i][j] = dp[i+1][j-1];
                        if(dp[i][j]) 
                            count++;
                    }
                }
            }
        }

        return count;
    }
};

2. 分割回文串 IV(hard)

1745. 分割回文串 IV

  • 代码解析:
cpp 复制代码
class Solution {
public:
    bool checkPartitioning(string s) {
        int n = s.size();
        
        // 1. ⽤ dp 把所有的⼦串是否是回⽂预处理⼀下
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        
        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
                    {
                        dp[i][j] = dp[i+1][j-1];
                    }
                }
            }
        }

        // 2. 枚举所有的第⼆个字符串的起始位置以及结束位置
        for(int i = 1; i < n-1; i++)
        {
            for(int j = i; j < n-1; j++)
            {
                if(dp[0][i-1] == true && dp[i][j] == true && dp[j+1][n-1] == true)
                    return true;
            }
        }

        return false;  
    }
};

3. 分割回文串 II(hard)

132. 分割回文串 II

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int minCut(string s) {
        int n = s.size();
        // 1. ⽤ dp 把所有的⼦串是否是回⽂预处理⼀下
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        
        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
                    {
                        dp[i][j] = dp[i+1][j-1];
                    }
                }
            }
        }
        
        // 2. 用 fp 推导最小分割次数
        vector<int> fp(n, 0);

        for(int j = 1; j < n; j++)
        {
            int min = INT_MAX;
            // 循环遍历一遍 i 的取值
            for(int i = 0; i <= j; i++)
            {
                if(dp[i][j] == true)
                {
                    if(i == 0)
                    {
                        min = 0;
                        break;
                    }
                    else
                    {
                        if(fp[i-1] + 1 < min)
                            min = fp[i-1] + 1;
                    }
                }
            }
            fp[j] = min;
        }

        return fp[n-1];
    }
};

4. 最长回文子序列

516. 最长回文子序列

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();

        vector< vector<int> > dp(n, vector<int>(n, 1));

        for(int i = n-1; i >= 0; i--)
        {
            for(int j = i; j < n; j++)
            {
                if(s[i] == s[j])
                {
                    if(j - i < 2)
                        dp[i][j] = j-i+1;
                    else
                        dp[i][j] = dp[i+1][j-1] + 2;
                }
                else
                {
                    dp[i][j] = max(dp[i][j-1], dp[i+1][j]);
                }
            }
        }

        return dp[0][n-1];
    }
};

七、两个数组的 dp(含字符串数组)

1. 最长公共子序列(模板题)

1143. 最长公共子序列

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n = text1.size(), m = text2.size();

        // 创建并初始化 dp表
        vector< vector<int> > dp(n+1, vector<int>(m+1, 0));

        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                if(text1[i-1] == text2[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[n][m];
    }
};

2. 不同的子序列(hard)

115. 不同的子序列

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int numDistinct(string s, string t) {
        int n = s.size(), m = t.size();

        // 创建dp表
        vector< vector<unsigned long long> > dp(n+1, vector<unsigned long long>(m+1, 0));
        // 初始化dp表
        for(int i = 0; i <= n; i++)
            dp[i][0] = 1;

        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                if(s[i-1] == t[j-1])
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-1];
                else
                    dp[i][j] = dp[i-1][j];
            }
        }

        return dp[n][m];
    }
};

3. 交错字符串(子序列)

97. 交错字符串

  • 代码解析:
cpp 复制代码
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n = s1.size();
        int m = s2.size();
        int w = s3.size();

        if(n+m != w) 
        {
            return false;
        }
        
        s1.insert(0, "_");
        s2.insert(0, "_");
        s3.insert(0, "_");
        
        // 创建 dp表
        vector< vector<bool> > dp(n+1, vector<bool>(m+1, false));
        
        // 初始化 dp表
        dp[0][0] = true;

        for(int i = 1; i <= n; i++)
        {
            if(s1[i] == s3[i] && dp[i-1][0] == true)
                dp[i][0] = true;
        }
        
        for(int j = 1; j <= m; j++)
        {
            if(s2[j] == s3[j] && dp[0][j-1] == true)
                dp[0][j] = true;
        }
        
        // 状态转移推导
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                if( (s1[i] == s3[i+j] && dp[i-1][j] == true) ||
                    (s2[j] == s3[i+j] && dp[i][j-1] == true) )
                {
                    dp[i][j] = true;
                }
            }
        }

        return dp[n][m];
    }
};

4. 最长重复子数组(子数组连续,模板题)

718. 最长重复子数组

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        int m = nums2.size();
        
        // 创建并初始化 dp表
        vector<vector<int>> dp(n+1, vector<int>(m+1, 0));

        int max = 0;
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= m; j++)
            {
                if(nums1[i-1] == nums2[j-1])
                {
                    dp[i][j] = dp[i-1][j-1] + 1;

                    if(dp[i][j] > max)
                        max = dp[i][j];
                }
            }
        }

        return max;
    }
};

八、背包问题概述

1. 01背包(模板题)

DP41 【模板】01背包

  • 方案一:不要求装满背包,求能获得的最大总价值
  • 方案二:要求最终恰好装满背包,求能获得的最大总价值。若不存在使背包恰好装满的装法,则答案记为 0
  • 代码解析:
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, V;
    cin >> n >> V;
    vector<int> v(n+1, 0);
    vector<int> w(n+1, 0);
    
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    
    // 方案一:不要求装满背包,求能获得的最大总价值
    // 创建并初始化dp表
    vector<vector<int>> dp(n+1, vector<int>(V+1, 0));

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= V; j++)
        {
            if(j - v[i] >= 0)
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
            else
                dp[i][j] = dp[i-1][j];
        }
    }

    printf("%d\n", dp[n][V]);
    
    // 方案二:要求最终恰好装满背包,求能获得的最大总价值。若不存在使背包恰好装满的装法,则答案记为 0
    // 初始化 dp表
    for(int j = 1; j <= V; j++)
        dp[0][j] = -1;

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= V; j++)
        {
            if(j - v[i] >= 0)
            {
                if(dp[i-1][j - v[i]] == -1)
                    dp[i][j] = max(dp[i-1][j], -1);
                else
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
            }
            else
                dp[i][j] = dp[i-1][j];
        }
    }

    if(dp[n][V] == -1)
        printf("%d\n", 0);
    else
        printf("%d\n", dp[n][V]);

    return 0;
}

2. 分割等和子集

416. 分割等和子集

  • 代码解析:
cpp 复制代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;

        for(auto it : nums)
            sum += it;
        
        if(sum % 2 == 1)
            return false;

        int target = sum / 2;

        int n = nums.size();
        nums.insert(nums.begin(), -1);

        // 创建并初始化 dp表
        vector<vector<bool>> dp(n+1, vector<bool>(target+1, false));
        for(int i = 0; i <= n; i++)
            dp[i][0] = true;

        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= target; j++)
            {
                if(j - nums[i] >= 0)
                {
                    if(dp[i-1][j] || dp[i-1][j-nums[i]])
                        dp[i][j] = true;
                }
                else
                    dp[i][j] = dp[i-1][j];
            }
        }

        return dp[n][target];
    }
};

3. 目标和(特殊)

494. 目标和

  • 代码示例:
cpp 复制代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(auto it : nums)
            sum += it;
        
        if( sum + target < 0 || (sum+target)%2 == 1 )
            return 0;

        int sum_A = (sum+target) / 2;

        if(sum_A > sum)
            return 0;

        int n = nums.size();
        nums.insert(nums.begin(), -1);

        // 创建并初始化 dp表
        vector<vector<int>> dp(n+1, vector<int>(sum_A+1, 0));
        dp[0][0] = 1;
        
        for(int i = 1; i <= n; i++)
        {
            // 从 dp[1][0]开始往后推导
            for(int j = 0; j <= sum_A; j++)
            {
                if(j-nums[i] >= 0)
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];
                else
                    dp[i][j] = dp[i-1][j];
            }
        }

        return dp[n][sum_A];
    }
};

4. 完全背包(模板题)

DP42 【模板】完全背包

  • 求这个背包至多能装多大价值的物品
  • 若背包恰好装满,求至多能装多大价值的物品
  • 代码解析:
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, V;
    cin >> n >> V;

    vector<int> v(n+1, 0);
    vector<int> w(n+1, 0);

    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    // 求这个背包至多能装多大价值的物品
    // 创建并初始化 dp表
    vector<vector<int>> dp(n+1, vector<int>(V+1, 0));

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= V; j++)
        {
            if(j - v[i] >= 0)
                dp[i][j] = max( dp[i-1][j], dp[i][j-v[i]] + w[i]);
            else
                dp[i][j] = dp[i-1][j];
        }
    }

    printf("%d\n", dp[n][V]);
    
    // 若背包恰好装满,求至多能装多大价值的物品
    // 初始化 dp表
    for(int j = 1; j <= V; j++)
    {
        dp[0][j] = -1;
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= V; j++)
        {
            if(j - v[i] >= 0 && dp[i][j-v[i]] != -1)
                dp[i][j] = max( dp[i-1][j], dp[i][j-v[i]] + w[i]);
            else
                dp[i][j] = dp[i-1][j];
        }
    }

    if(dp[n][V] == -1)
        printf("%d\n", 0);
    else
        printf("%d\n", dp[n][V]);

    return 0;
}

5. 完全平方数

279. 完全平方数

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int numSquares(int n) {
        vector<int> nums(1, -1);

        int squ = 1;
        while(squ * squ <= n)
        {
            nums.push_back(squ * squ);
            squ++;
        }

        int count = nums.size()-1;
        
        // 创建并初始化 dp表
        vector<vector<int>> dp(count+1, vector<int>(n+1, 0));
        for(int j = 1; j <= n; j++)
            dp[0][j] = INT_MAX;
        
        for(int i = 1; i <= count; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                if(j-nums[i] >= 0 && dp[i][j-nums[i]] != INT_MAX)
                    dp[i][j] = min( dp[i-1][j], dp[i][j-nums[i]] + 1 );
                else
                    dp[i][j] = dp[i-1][j];
            }
        }

        return dp[count][n];
    }
};

6. 一和零(二维限制条件的 01背包问题)

474. 一和零

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int len = strs.size();
        strs.insert(strs.begin(), "");
        
        // 统计每个字符串中 字符0 和 字符1的个数
        vector<int> x(len+1, 0);
        vector<int> y(len+1, 0);

        for(int i = 1; i <= len; i++)
        {
            int x_0 = 0;
            int y_1 = 0;
            for(auto it : strs[i])
            {
                if(it == '0')
                    x_0++;
                else
                    y_1++;
            }
            x[i] = x_0;
            y[i] = y_1;
        }
        
        // 创建并初始化 dp表
        vector<vector<vector<int>>> dp(len+1, vector<vector<int>>(m+1, vector<int>(n+1, 0)));

        for(int i = 1; i <= len; i++)
        {
            for(int j = 0; j <= m; j++)
            {
                for(int k = 0; k <= n; k++)
                {
                    if(j - x[i] >= 0 && k - y[i] >= 0)
                        dp[i][j][k] = max( dp[i-1][j][k], dp[i-1][j-x[i]][k-y[i]] + 1);
                    else
                        dp[i][j][k] = dp[i-1][j][k];
                }
            }
        }

        return dp[len][m][n];
    }
};

7. 组合总和 Ⅳ(似包非包)

377. 组合总和 Ⅳ

  • 代码解析:
cpp 复制代码
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        // 创建并初始化 dp表
        vector<unsigned long long> dp(target+1, 0);
        dp[0] = 1;

        for(int i = 1; i <= target; i++)
        {
            for(auto it : nums)
            {
                if(i - it >= 0)
                    dp[i] += dp[i-it];
            }
        }

        return dp[target];
    }
};

相关推荐
aqiu1111113 小时前
【数位 DP / 记忆化搜索】蓝桥云课 - 光轨区的 AI 数 题解
c++·算法·蓝桥杯·数位dp
地平线开发者3 小时前
Horizon J6m 部署 YOLOv8s INT8 精度下降问题排查记录
算法
橘子汽水1684 小时前
Leetcode 200,994岛屿数量,腐烂的橘子
数据结构·算法·leetcode
欧特克_Glodon4 小时前
OpenCV计算机视觉开发入门与实践<三十七>:OpenCV中的机器学习
c++·opencv·机器学习·计算机视觉
老赵的博客4 小时前
c++ 之观察者模式
c++·qt
坚定学代码5 小时前
static在c++中的使用
开发语言·c++
hansang_IR5 小时前
【题解】P5364 [SNOI2017] 礼物
c++·线性代数·算法
Tisfy5 小时前
LeetCode 3871.统计范围内的逗号 II:每次算一个逗号
数学·算法·leetcode·题解
无小道5 小时前
C/C++——tuple小记
c++·tuple