第九章 动态规划

一、基础题目

1.1 斐波那契数

Leetcode 509

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

滚动数组:

class Solution {
public:
    int fib(int n) {
        if (n <= 1) return n;
        int a = 0, b = 1;
        for (int i = 2; i <= n; i ++ ) {
            int sum = a + b;
            a = b, b = sum;
        }
        return b;
    }
};

递归:

class Solution {
public:
    int fib(int n) {
        if (n < 2) return n;
        return fib(n - 2) + fib(n - 1);
    }
};

1.2 爬楼梯

Leetcode 70

class Solution {
public:
    int climbStairs(int n) {
        if (n <= 2) return n;
        int a = 1, b = 2;
        for (int i = 3; i <= n; i ++ ) {
            int sum = a + b;
            a = b, b = sum;
        }
        return b;
    }
};

1.3 使用最小花费爬楼梯

Leetcode 746

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int a = 0, b = 0; // 从0或者1出发不消耗体力
        for (int i = 2; i <= cost.size(); i ++ ) { // 注意这里的等号!
            int sum = min(a + cost[i - 2], b + cost[i - 1]);
            a = b, b = sum;
        }
        return b;
    }
};

1.4 不同路径

Leetcode 62

二维DP:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n, 0));
        for (int i = 0; i < m; i ++ ) dp[i][0] = 1;
        for (int i = 0; i < n; i ++ ) dp[0][i] = 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];
    }
};

一维DP:

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

数论:

由题意可知,不管怎么走,总共需要走 m + n − 2 m + n - 2 m+n2 步,其中 m − 1 m - 1 m1 步必须往下走,剩余的 n − 1 n - 1 n1 步必须往右走。那么这个题目就可以转换为一个组合问题:从 m + n − 2 m + n - 2 m+n2 个数中选择 m − 1 m - 1 m1 个数,总共有多少种选择方法。

C m + n − 2 m − 1 = ( m + n − 2 ) ! ( m − 1 ) ! ( n − 1 ) ! = ( m + n − 2 ) × ( m + n − 3 ) × ⋯ × n ( m − 1 ) ! C_{m + n - 2}^{m - 1} = \frac{(m + n - 2)!}{(m-1)!(n-1)!} = \frac{(m + n - 2) \times (m + n - 3) \times \cdots \times n}{(m-1)!} Cm+n2m1=(m1)!(n1)!(m+n2)!=(m1)!(m+n2)×(m+n3)××n

上面公式中,分子有 m + n − 2 − n + 1 = m − 1 m + n - 2 - n + 1=m-1 m+n2n+1=m1 项。

求解组合数的公式: C n m = n ! m ! ( n − m ) ! = C n − 1 m − 1 + C n − 1 m C_n^m = \frac{n!}{m! (n-m)!} = C_{n-1}^{m-1} + C_{n - 1}^m Cnm=m!(nm)!n!=Cn1m1+Cn1m
后面一个公式推导思路:

  • 选择第 m m m 个数: C n − 1 m − 1 C_{n-1}^{m-1} Cn1m1
  • 不选择第 m m m 个数: C n − 1 m C_{n - 1}^m Cn1m

在计算组合树的时候,一定要防止两个 i n t int int 整数相乘溢出!比如下面的代码:

class Solution {
public:
    int uniquePaths(int m, int n) {
        int numerator = 1, denominator = 1;
        int count = m - 1, t = m + n - 2;
        while (count -- ) numerator *= (t -- ); // 分子(会溢出)
        for (int i = 1; i <= m - 1; i ++ ) denominator *= i; // 分母
        return numerator / denominator;
    }
};

解决办法是在处理计算分子的时候不断除以分母:

class Solution {
public:
    int uniquePaths(int m, int n) {
        long long numerator = 1, denominator = m - 1; // 分子与分母
        int count = m - 1, t = m + n - 2;
        while (count -- ) {
            numerator *= (t --);
            while (denominator && numerator % denominator == 0)
                numerator /= denominator, denominator -- ;
        }
        return numerator;
    }
};

1.5 不同路径 Ⅱ

Leetcode 63

二维数组:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size(), n = obstacleGrid[0].size();
        if (obstacleGrid[m - 1][n - 1] == 1|| obstacleGrid[0][0] == 1) return 0;
        vector<vector<int>> dp(m, vector<int>(n, 0));
        for (int i = 0; i < m && !obstacleGrid[i][0]; i ++ ) dp[i][0] = 1; // 遇到障碍物就停止
        for (int i = 0; i < n && !obstacleGrid[0][i]; i ++ ) dp[0][i] = 1;
        for (int i = 1; i < m; i ++ )
            for (int j = 1; j < n; j ++ ) {
                if (obstacleGrid[i][j] == 1) continue;
                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();
        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) return 0;
        vector<int> dp(n, 0);
        for (int i = 0; i < n && !obstacleGrid[0][i]; i ++ ) dp[i] = 1;
        for (int i = 1; i < m; i ++ )
            for (int j = 0; j < n; j ++ ) { // 改动
                if (obstacleGrid[i][j] == 1) dp[j] = 0; // 改动
                else if (j) dp[j] += dp[j - 1]; // 改动
            }
        return dp[n - 1];
    }
};

1.6 整数拆分

Leetcode 343

d p [ i ] dp[i] dp[i]:分解数字 i i i,可以获得的最大乘积 d p [ i ] dp[i] dp[i]

递推公式: d p [ i ] = m a x ( d p [ i ] , ( i − j ) × j , d p [ i − j ] × j ) dp[i] = max(dp[i], (i - j) \times j, dp[i - j] \times j) dp[i]=max(dp[i],(ij)×j,dp[ij]×j)

  • 递推公式的思路其实就是将 d [ i ] d[i] d[i] 拆解成两个部分:
  • 一部分是直接两个数 j 、 i − j j、i-j jij 相乘,注意 i = j + ( i − j ) i = j + (i - j) i=j+(ij)
  • 另一部分是将 d p [ i ] dp[i] dp[i] 拆解成一个数与两个及两个以上的数相乘: j × d p [ i − j ] j \times dp[i-j] j×dp[ij]

初始化: d p [ 2 ] = 1 dp[2]=1 dp[2]=1,对于 d p [ 0 ] 、 d p [ 1 ] dp[0]、dp[1] dp[0]dp[1] 初始化是没有意义的。

注意:将一个数拆分成两个数并之和且使之乘积最大,那么只需要将其拆分成两个值比较相近的数即可。比如 6 = 3 + 3 6 = 3 + 3 6=3+3,那么 3 × 3 = 9 3 \times 3=9 3×3=9 是乘积最大的组合。

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n + 1);
        dp[2] = 1;
        for (int i = 3; i <= n; i ++ ) // 从3开始,1、2没有意义
            for (int j = 1; j <= i / 2; j ++ ) // 从1开始遍历,0没有意义,拆分成两个值相近的数
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
        return dp[n];
    }
};

1.7 不同的二叉搜索树

Leetcode 96

参考题解

d p [ i ] dp[i] dp[i]:表示 i i i 个不同元素节点组成的二叉搜索树的个数

递推公式: d p [ i ] + = d p [ j − 1 ] × d p [ i − j ] dp[i] += dp[j - 1] \times dp[i - j] dp[i]+=dp[j1]×dp[ij]

  • 上述递推公式表示:
  • d p [ i ] dp[i] dp[i] i i i 个不同元素节点组成的二叉搜索树的个数
  • d p [ j − 1 ] dp[j - 1] dp[j1] j − 1 j - 1 j1 为以 j j j 为头结点左子树节点数量
  • d p [ i − j ] dp[i - j] dp[ij] i − j i - j ij 为以 j j j 为头结点右子树节点数量

初始化: d p [ 0 ] = 1 dp[0] = 1 dp[0]=1,空节点也是一棵二叉树。

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

二、背包问题

2.1 01 背包

滚动数组优化二维 01 背包为一维 01 背包问题,看这篇文章

还需要注意几个点:

  • 遍历顺序:倒序遍历是为了保证物品 i 只被放入一次
  • 两个 for 循环谁先谁后:因为一维 dp 的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个 dp[j] 就只会放入一个物品,即:背包里只放入了一个物品。

2.1.1 分割等和子集

Leetcode 416

这个题目可以转换为 01 背包:

  • 背包的体积为 sum / 2
  • 背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集
  • 背包中每一个元素是不可重复放入

代码中 dp 数组大小:由于 dp 数组表示包内元素的总和,题目中说每个数组元素不会大于 100,数组大小不会超过 200,因此 dp 总和不会超过 20000,但是这里只需要求 sum / 2,因此 dp 数组初始化为 10000 即可。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int x: nums) sum += x;
        if (sum % 2 == 1) return false;
        int target = sum / 2;
        vector<int> dp(10001, 0);
        for (int i = 0; i < nums.size(); i ++ )
            for (int j = target; j >= nums[i]; j -- )
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
        if (dp[target] == target) return true;
        return false;
    }
};

2.1.1.1 划分为k个相等的子集

Leetcode 698

2.1.1.2 火柴拼正方形

Leetcode 473

2.1.2 最后一块石头的重量 Ⅱ

Leetcode 1049

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成 01 背包问题了。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int> dp(15010, 0);
        int sum = 0;
        for (int x: stones) sum += x;
        int target = sum / 2; // 向下取整:sum - dp[target] > dp[target]
        for (int i = 0; i < stones.size(); i ++ )
            for (int j = target; j >= stones[i]; j -- )
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
        return sum - dp[target] - dp[target];
    }
};

2.1.3 目标和

Leetcode 494

本题要如何使表达式结果为target,

既然为target,那么就一定有 left组合 - right组合 = target。

left + right = sum,而sum是固定的。right = sum - left

公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。

target是固定的,sum是固定的,left就可以求出来。

此时问题就是在集合nums中找出和为left的组合。

回溯:

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;

    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i ++ ) sum += nums[i];
        if (target > sum) return 0;
        if ((target + sum) % 2) return 0;
        int bagSize = (target + sum) / 2;
        res.clear(), path.clear();
        sort(nums.begin(), nums.end());
        backtracking(nums, bagSize, 0, 0);
        return res.size();
    }

    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) res.push_back(path);
        for (int i = startIndex; i < candidates.size() && candidates[i] + sum <= target; i ++ ) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i + 1);
            sum -= candidates[i];
            path.pop_back();
        }
    }
};

DP:虽然是一个 01 背包,但是这里是组合问题,有多少种方案。

d p [ j ] dp[j] dp[j] 表示:填满 j j j(包括 j j j)这么大容积的包,有 d p [ j ] dp[j] dp[j] 种方法

递推公式:只要搞到 n u m s [ i ] nums[i] nums[i],凑成 dp[j]$ 就有 d p [ j − n u m s [ i ] ] dp[j - nums[i]] dp[jnums[i]] 种方法。 d p [ j ] + = d p [ j − n u m s [ i ] ] dp[j] += dp[j - nums[i]] dp[j]+=dp[jnums[i]]

初始化: d p [ 0 ] = 1 dp[0]=1 dp[0]=1

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int x: nums) sum += x;
        if (abs(target) > sum) return 0;
        if ((sum + target) % 2) return 0;
        int bagSize = (sum + target) / 2;
        vector<int> dp(bagSize + 10, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i ++ )
            for (int j = bagSize; j >= nums[i]; j -- )
                dp[j] += dp[j - nums[i]];
        return dp[bagSize];
    }
};

2.1.4 一和零

Leetcode 474

d p [ i ] [ j ] dp[i][j] dp[i][j]:最多有 i i i 0 0 0 j j j 1 1 1 s t r s strs strs 的最大子集的大小为 d p [ i ] [ j ] dp[i][j] dp[i][j]

递推公式: d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − z e r o N u m ] [ j − o n e N u m ] + 1 ) dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1) dp[i][j]=max(dp[i][j],dp[izeroNum][joneNum]+1)

初始化:全为零

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 初始化为0
        for (string str: strs) {
            int oneNum = 0, zeorNum = 0;
            for (char c: str) 
                if (c == '0') zeorNum ++ ;
                else oneNum ++ ;
            for (int i = m; i >= zeorNum; i -- )
                for (int j = n; j >= oneNum; j -- )
                    dp[i][j] = max(dp[i][j], dp[i - zeorNum][j - oneNum] + 1);
        }
        return dp[m][n];
    }
};

2.2 完全背包

遍历顺序解释看这篇文章

注意:

  • for 循环遍历顺序:因为物品无限多,不需要保证其仅被装入背包一次,所以可以从小到大遍历
  • 两层 for 循环谁先谁后:完全背包是无所谓的

2.2.1 零钱兑换 Ⅱ

Leetcode 518

d p [ j ] dp[j] dp[j]:凑成总金额 j j j 的货币组合数为 d p [ j ] dp[j] dp[j]

递推公式: d p [ j ] + = d p [ j − c o i n s [ i ] ] dp[j] += dp[j - coins[i]] dp[j]+=dp[jcoins[i]]

初始化: d p [ 0 ] = 1 dp[0] = 1 dp[0]=1

本题两层 for 循环遍历顺序是有区别的!看文章,一定要看!

在求装满背包有几种方案的时候,认清遍历顺序是非常关键的:

  • 如果求组合数:外层for循环遍历物品,内层for遍历背包。

  • 如果求排列数:外层for遍历背包,内层for循环遍历物品。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < coins.size(); i ++ )
            for (int j = coins[i]; j <= amount; j ++ )
                dp[j] += dp[j - coins[i]];
        return dp[amount];
    }
};

2.2.2 组合总和 Ⅳ

Leetcode 377

这个题目是求解排列数,上一个题目是求解组合数。

d p [ i ] dp[i] dp[i]:凑成目标正整数为 i i i 的排列个数为 d p [ i ] dp[i] dp[i]

递推公式: d p [ i ] + = d p [ i − n u m s [ j ] ] dp[i] += dp[i - nums[j]] dp[i]+=dp[inums[j]]

初始化: d p [ 0 ] = 1 dp[0] = 1 dp[0]=1

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target + 1, 0);
        dp[0] = 1;
        for (int i = 0; i <= target; i ++ ) // 遍历背包
            for (int j = 0; j < nums.size(); j ++ )  // 遍历物品
                // C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]
                if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]])
                    dp[i] += dp[i - nums[j]];
        return dp[target];
    }
};

2.2.3 爬楼梯

Leetcode 70

求解排列数

d p [ i ] dp[i] dp[i]:爬到有 i i i 个台阶的楼顶,有 d p [ i ] dp[i] dp[i] 种方法。

递推公式: d p [ i ] + = d p [ i − j ] dp[i] += dp[i - j] dp[i]+=dp[ij]

初始化: d p [ 0 ] = 1 dp[0] = 1 dp[0]=1

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

2.2.4 零钱兑换

Leetcode 322

d p [ j ] dp[j] dp[j]:凑足总额为j所需钱币的最少个数为 d p [ j ] dp[j] dp[j]

递推公式: d p [ j ] = m i n ( d p [ j − c o i n s [ i ] ] + 1 , d p [ j ] ) dp[j] = min(dp[j - coins[i]] + 1, dp[j]) dp[j]=min(dp[jcoins[i]]+1,dp[j])

初始化: d p [ 0 ] = 0 dp[0] = 0 dp[0]=0,对于非零下标的数,考虑到递推公式的特性, d p [ j ] dp[j] dp[j] 必须初始化为一个最大的数,否则就会在 m i n ( d p [ j − c o i n s [ i ] ] + 1 , d p [ j ] ) min(dp[j - coins[i]] + 1, dp[j]) min(dp[jcoins[i]]+1,dp[j]) 比较的过程中被初始值覆盖。所以下标非 0 0 0 的元素都是应该是最大值。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i < coins.size(); i ++ )
            for (int j = coins[i]; j <= amount; j ++ )
                if (dp[j - coins[i]] != INT_MAX)
                    dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

2.2.5 完全平方数

Leetcode 279

题目翻译一下:完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

d p [ j ] dp[j] dp[j]:和为 j j j 的完全平方数的最少数量为 d p [ j ] dp[j] dp[j]

递推公式: d p [ j ] = m i n ( d p [ j − i ∗ i ] + 1 , d p [ j ] ) dp[j] = min(dp[j - i * i] + 1, dp[j]) dp[j]=min(dp[jii]+1,dp[j])

初始化: d p [ 0 ] = 0 dp[0]=0 dp[0]=0

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

2.2.6 单词拆分

Leetcode 139

回溯(会超时):

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        return backtracking(s, wordSet, 0);
    }

    bool backtracking(const string& s, const unordered_set<string>& wordSet, int startIndex) {
        if (startIndex >= s.size()) return true;
        for (int i = startIndex; i < s.size(); i ++ ) {
            string word = s.substr(startIndex, i - startIndex + 1);
            if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1))
                return true;
        }
        return false;
    }
};

DP:

d p [ i ] dp[i] dp[i]:字符串长度为 i i i 的话, d p [ i ] dp[i] dp[i] t r u e true true,表示可以拆分为一个或多个在字典中出现的单词。

递推公式:

  • 如果确定 d p [ j ] dp[j] dp[j] t r u e true true,且 [ j , i ] [j, i] [j,i] 这个区间的子串出现在字典里,那么 d p [ i ] dp[i] dp[i] 一定是 t r u e true true ( j < i ) (j < i ) (j<i)
  • 所以递推公式是 i f ( [ j , i ] if([j, i] if([j,i] 这个区间的子串出现在字典里并且 d p [ j ] dp[j] dp[j] t r u e true true,那么 d p [ i ] = t r u e dp[i] = true dp[i]=true

初始化:

  • i = 0 i=0 i=0 时, d p [ i ] = t r u e dp[i] = true dp[i]=true
  • i ≠ 0 i \ne 0 i=0 时, d p [ i ] = f a l s e dp[i] = false dp[i]=false

本题是求解排列数

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> dp(s.size() + 1, false);
        dp[0] = true;
        for (int i = 1; i <= s.size(); i ++ )
            for (int j = 0; j < i; j ++ ) {
                string word = s.substr(j, i - j);
                if (wordSet.find(word) != wordSet.end() && dp[j])
                    dp[i] = true;
            }
        return dp[s.size()];
    }
};

2.3 多重背包

三、打家劫舍

3.1 打家劫舍

Leetcode 198

d p [ i ] dp[i] dp[i]:考虑下标 i i i(包括 i i i)以内的房屋,最多可以偷窃的金额为 d p [ i ] dp[i] dp[i]

递推公式: d p [ i ] = m a x ( d p [ i − 2 ] + n u m s [ i ] , d p [ i − 1 ] ) dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]) dp[i]=max(dp[i2]+nums[i],dp[i1])

初始化: d p [ 1 ] = m a x ( n u m s [ 0 ] , n u m s [ 1 ] ) , d p [ 0 ] = n u m s [ 0 ] dp[1] = max(nums[0], nums[1]), dp[0] = nums[0] dp[1]=max(nums[0],nums[1]),dp[0]=nums[0]

class Solution {
public:
    int rob(vector<int>& nums) {
        if (!nums.size()) return 0;
        if (nums.size() == 1) return nums[0];
        vector<int> dp(nums.size());
        dp[0] = nums[0], dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < nums.size(); i ++ )
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        return dp[nums.size() - 1];
    }
};

3.2 打家劫舍 Ⅱ

Leetcode 213

对于一个数组,成环的话主要有如下三种情况:

  • 情况一:考虑不包含首尾元素
  • 情况二:考虑包含首元素,不包含尾元素
  • 情况三:考虑包含尾元素,不包含首元素

注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素!

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if (!n) return 0;
        if (n == 1) return nums[0];
        int res1 = robRange(nums, 0, n - 2); // 情况二
        int res2 = robRange(nums, 1, n - 1); // 情况三
        return max(res1, res2);
    }

    int robRange(vector<int>& nums, int l, int r) {
        if (l == r) return nums[l];
        vector<int> dp(nums.size());
        dp[l] = nums[l];
        dp[l + 1] = max(nums[l], nums[l + 1]);
        for (int i = l + 2; i <= r; i ++ )
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        return dp[r];
    }
};

3.3 打家劫舍 Ⅲ

Leetcode 337

本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算。

暴力递归

class Solution {
public:
    int rob(TreeNode* root) {
        if (!root) return 0;
        if (!root->left && !root->right) return root->val;
        // 偷父节点
        int v1 = root->val;
        if (root->left) v1 += rob(root->left->left) + rob(root->left->right);
        if (root->right) v1 += rob(root->right->left) + rob(root->right->right);
        // 不偷父结点
        int v2 = rob(root->left) + rob(root->right);
        return max(v1, v2);
    }
};

会超时,因为遍历了 root 的四个子孙(孩子的孩子),又计算了左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把子孙计算了一遍。

记忆化递推

使用一个 map 把计算过的结果保存,这样如果计算过孙子,那么再计算孩子的时候就可以服用孙子节点的结果.

class Solution {
public:
    unordered_map<TreeNode*, int> mp;
    int rob(TreeNode* root) {
        if (!root) return 0;
        if (!root->left && !root->right) return root->val;
        if (mp[root]) return mp[root]; // 有记录直接返回
        // 偷父节点
        int v1 = root->val;
        if (root->left) v1 += rob(root->left->left) + rob(root->left->right);
        if (root->right) v1 += rob(root->right->left) + rob(root->right->right);
        // 不偷父结点
        int v2 = rob(root->left) + rob(root->right);
        mp[root] = max(v1, v2);
        return max(v1, v2);
    }
};

树形DP

思路参考

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> res = robTree(root);
        return max(res[0], res[1]);
    }

    // 长度为2的数组,0不偷当前结点,1偷当前结点
    vector<int> robTree(TreeNode* cur) {
        if (!cur) return vector<int>{0, 0};
        vector<int> left = robTree(cur->left), right = robTree(cur->right);
        // 不偷cur,那么可以偷也可以不偷左右节点,取一个较大值即可
        int v1 = max(left[0], left[1]) + max(right[0], right[1]);
        // 偷cur,那么就不可以偷左右结点
        int v2 = cur->val + left[0] + right[0];
        return {v1, v2};
    }
};

四、股票问题

4.1 买卖股票的最佳时期(只能买卖一次)

Leetcode 121

贪心

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int low = INT_MAX, res = 0;
        for (int i = 0; i < prices.size(); i ++ ) {
            low = min(low, prices[i]);
            res = max(res, prices[i] - low);
        }
        return res;
    }
};

DP

d p [ i ] [ 0 ] dp[i][0] dp[i][0]:表示第 i i i 天持有股票所得最多现金
d p [ i ] [ 1 ] dp[i][1] dp[i][1]:表示第 i i i 天不持有股票所得最多现金

递推公式:

  • 如果第 i i i 天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来
    • i − 1 i-1 i1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: d p [ i − 1 ] [ 0 ] dp[i - 1][0] dp[i1][0]
    • i i i 天买入股票,所得现金就是买入今天的股票后所得现金即: − p r i c e s [ i ] -prices[i] prices[i]
    • 那么 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 应该选所得现金最大的,所以 d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , − p r i c e s [ i ] ) dp[i][0] = max(dp[i - 1][0], -prices[i]) dp[i][0]=max(dp[i1][0],prices[i])
  • 如果第 i i i 天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1], 也可以由两个状态推出来
    • i − 1 i-1 i1 天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即: d p [ i − 1 ] [ 1 ] dp[i - 1][1] dp[i1][1]
    • i i i 天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即: p r i c e s [ i ] + d p [ i − 1 ] [ 0 ] prices[i] + dp[i - 1][0] prices[i]+dp[i1][0]
    • d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , p r i c e s [ i ] + d p [ i − 1 ] [ 0 ] ) dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]) dp[i][1]=max(dp[i1][1],prices[i]+dp[i1][0])

初始化:

  • d p [ 0 ] [ 0 ] = − p r i c e s [ 0 ] dp[0][0] = -prices[0] dp[0][0]=prices[0]
  • d p [ 0 ] [ 1 ] = 0 dp[0][1] = 0 dp[0][1]=0
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (!len) return 0;
        vector<vector<int>> dp(len, vector<int>(2));
        dp[0][0] = -prices[0], dp[0][1] = 0;
        for (int i = 1; i < len; i ++ ) {
            dp[i][0] = max(dp[i - 1][0], -prices[i]);
            dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
        }
        return dp[len - 1][1]; // 最后不持有股票的钱一定比持有股票的钱多
    }
};

滚动数组:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (!len) return 0;
        vector<vector<int>> dp(2, vector<int>(2));
        dp[0][0] = -prices[0], dp[0][1] = 0;
        for (int i = 1; i < len; i ++ ) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
        }
        return dp[(len - 1) % 2][1]; // 最后不持有股票的钱一定比持有股票的钱多
    }
};

4.2 买卖股票的最佳时期 Ⅱ(可以买卖多次)

Leetcode 122

d p [ i ] [ 0 ] dp[i][0] dp[i][0] 表示第 i i i 天持有股票所得现金
d p [ i ] [ 1 ] dp[i][1] dp[i][1] 表示第 i i i 天不持有股票所得最多现金

递推公式:

  • 如果第 i i i 天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来
    • i − 1 i-1 i1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: d p [ i − 1 ] [ 0 ] dp[i - 1][0] dp[i1][0]
    • i i i 天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格 即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i1][1]prices[i]
  • 如果第 i i i 天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1] 的情况, 依然可以由两个状态推出来
    • i − 1 i-1 i1 天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即: d p [ i − 1 ] [ 1 ] dp[i - 1][1] dp[i1][1]
    • i i i 天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即: p r i c e s [ i ] + d p [ i − 1 ] [ 0 ] prices[i] + dp[i - 1][0] prices[i]+dp[i1][0]
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(2, 0));
        dp[0][0] = -prices[0], dp[0][1] = 0;
        for (int i = 1; i < len; 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[len - 1][1];
    }
};

滚动数组版本:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(2, vector<int>(2, 0));
        dp[0][0] = -prices[0], dp[0][1] = 0;
        for (int i = 1; i < len; i ++ ) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]);
        }
        return dp[(len - 1) % 2][1];
    }
};

4.3 买卖股票的最佳时期 Ⅲ(最多买卖两次)

Leetcode 123

d p [ i ] [ j ] dp[i][j] dp[i][j] i i i 表示第 i i i 天, j j j [ 0 − 4 ] [0 - 4] [04] 五个状态, d p [ i ] [ j ] dp[i][j] dp[i][j] 表示第 i i i 天状态 j j j 所剩最大现金。

  • j = 0 j = 0 j=0:没有操作 (其实我们也可以不设置这个状态)
  • j = 1 j=1 j=1:第一次持有股票
  • j = 2 j=2 j=2:第一次不持有股票
  • j = 3 j=3 j=3:第二次持有股票
  • j = 4 j=4 j=4:第二次不持有股票

递推公式:

  • 达到 d p [ i ] [ 1 ] dp[i][1] dp[i][1] 状态,有两个具体操作:
    • i i i 天买入股票了,那么 d p [ i ] [ 1 ] = d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] dp[i][1] = dp[i-1][0] - prices[i] dp[i][1]=dp[i1][0]prices[i]
    • i i i 天没有操作,而是沿用前一天买入的状态,即: d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] dp[i][1] = dp[i - 1][1] dp[i][1]=dp[i1][1]
    • d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] , d p [ i − 1 ] [ 1 ] ) dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]) dp[i][1]=max(dp[i1][0]prices[i],dp[i1][1])
  • d p [ i ] [ 2 ] dp[i][2] dp[i][2] 也有两个操作:
    • i i i 天卖出股票了,那么 d p [ i ] [ 2 ] = d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] dp[i][2] = dp[i - 1][1] + prices[i] dp[i][2]=dp[i1][1]+prices[i]
    • i i i 天没有操作,沿用前一天卖出股票的状态,即: d p [ i ] [ 2 ] = d p [ i − 1 ] [ 2 ] dp[i][2] = dp[i - 1][2] dp[i][2]=dp[i1][2]
    • d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] , d p [ i − 1 ] [ 2 ] ) dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]) dp[i][2]=max(dp[i1][1]+prices[i],dp[i1][2])
  • d p [ i ] [ 3 ] = m a x ( d p [ i − 1 ] [ 3 ] , d p [ i − 1 ] [ 2 ] − p r i c e s [ i ] ) dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]) dp[i][3]=max(dp[i1][3],dp[i1][2]prices[i])
  • d p [ i ] [ 4 ] = m a x ( d p [ i − 1 ] [ 4 ] , d p [ i − 1 ] [ 3 ] + p r i c e s [ i ] ) dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]) dp[i][4]=max(dp[i1][4],dp[i1][3]+prices[i])

初始化:

  • d p [ 0 ] [ 0 ] = 0 dp[0][0] = 0 dp[0][0]=0
  • d p [ 0 ] [ 1 ] = − p r i c e s [ 0 ] dp[0][1] = -prices[0] dp[0][1]=prices[0]
  • d p [ 0 ] [ 2 ] = 0 dp[0][2] = 0 dp[0][2]=0:第 0 天做第一次卖出,相当于当天买当天卖
  • d p [ 0 ] [ 3 ] = − p r i c e s [ 0 ] dp[0][3] = -prices[0] dp[0][3]=prices[0]
  • d p [ 0 ] [ 4 ] = 0 dp[0][4] = 0 dp[0][4]=0
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (!prices.size()) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
        dp[0][1] = dp[0][3] = -prices[0];
        for (int i = 1; i < prices.size(); i ++ ) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return dp[prices.size() - 1][4];
    }
};

不设置状态 j = 0 j=0 j=0 的写法:因为没有操作,手上资金就只能为 0。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (!prices.size()) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
        dp[0][1] = dp[0][3] = -prices[0];
        for (int i = 1; i < prices.size(); i ++ ) {
            dp[i][1] = max(dp[i - 1][1], 0 - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return dp[prices.size() - 1][4];
    }
};

滚动数组:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (!prices.size()) return 0;
        vector<int> dp(5, 0);
        dp[1] = dp[3] = -prices[0];
        for (int i = 1; i < prices.size(); i ++ ) {
            dp[1] = max(dp[1], dp[0] - prices[i]);
            dp[2] = max(dp[2], dp[1] + prices[i]);
            dp[3] = max(dp[3], dp[2] - prices[i]);
            dp[4] = max(dp[4], dp[3] + prices[i]);
        }
        return dp[4];
    }
};

4.4 买卖股票的最佳时期 Ⅳ (最多买卖 k 次)

Leetcode 188

这个题目与上面一个题目类似,只是状态 j j j 0 ∼ 4 0 \sim 4 04 变成了 0 ∼ k 0 \sim k 0k

并且从上面一题中的状态定义可以发现, j j j 为奇数就是买入, j j j 为偶数就是卖出,总共 k k k 次交易,每次交易一次买入、一次卖出,所以状态总共需要定义 2 × k + 1 2 \times k + 1 2×k+1,并且状态转移与初始化都有这样的规律。

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        if (!prices.size()) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
        for (int j = 1; j < 2 * k; j += 2) dp[0][j] = -prices[0];
        for (int i = 1; i < prices.size(); i ++ )
            for (int j = 0; j < 2 * k - 1; j += 2) {
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
                dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
            }
        return dp[prices.size() - 1][2 * k];
    }
};

4.5 最佳买卖股票的最佳时期含冷冻期(买卖多次,卖出有一天冷冻期)

Leetcode 309

d p [ i ] [ j ] dp[i][j] dp[i][j],第 i i i 天状态为 j j j,所剩的最多现金为 d p [ i ] [ j ] dp[i][j] dp[i][j]

四个状态:

  • 状态一( j = 0 j=0 j=0) :持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
  • 不持有股票状态,这里就有两种卖出股票状态
    • 状态二( j = 1 j=1 j=1):保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
    • 状态三( j = 2 j=2 j=2):今天卖出股票
  • 状态四( j = 3 j=3 j=3):今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

递推公式:

  • 达到买入股票状态(状态一)即: d p [ i ] [ 0 ] dp[i][0] dp[i][0],有两个具体操作:

    • 操作一:前一天就是持有股票状态(状态一), d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] dp[i][0] = dp[i - 1][0] dp[i][0]=dp[i1][0]

    • 操作二:今天买入了,有两种情况

      • 前一天是冷冻期(状态四), d p [ i − 1 ] [ 3 ] − p r i c e s [ i ] dp[i - 1][3] - prices[i] dp[i1][3]prices[i]
      • 前一天是保持卖出股票的状态(状态二), d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i1][1]prices[i]
    • d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 3 ] − p r i c e s [ i ] , d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] ) dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]) dp[i][0]=max(dp[i1][0],dp[i1][3]prices[i],dp[i1][1]prices[i])

  • 达到保持卖出股票状态(状态二)即: d p [ i ] [ 1 ] dp[i][1] dp[i][1],有两个具体操作:

    • 操作一:前一天就是状态二
    • 操作二:前一天是冷冻期(状态四)
    • d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 3 ] ) dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]) dp[i][1]=max(dp[i1][1],dp[i1][3])
  • 达到今天就卖出股票状态(状态三),即: d p [ i ] [ 2 ] dp[i][2] dp[i][2] ,只有一个操作:

    • 昨天一定是持有股票状态(状态一),今天卖出,即: d p [ i ] [ 2 ] = d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] dp[i][2] = dp[i - 1][0] + prices[i] dp[i][2]=dp[i1][0]+prices[i]
  • 达到冷冻期状态(状态四),即: d p [ i ] [ 3 ] dp[i][3] dp[i][3],只有一个操作:

    • 昨天卖出了股票(状态三): d p [ i ] [ 3 ] = d p [ i − 1 ] [ 2 ] dp[i][3] = dp[i - 1][2] dp[i][3]=dp[i1][2]

初始化:

  • d p [ 0 ] [ 0 ] = − p r i c e s [ 0 ] dp[0][0] = -prices[0] dp[0][0]=prices[0]
  • d p [ 0 ] [ 1 ] = d p [ 0 ] [ 2 ] = d p [ 0 ] [ 3 ] = 0 dp[0][1] = dp[0][2] = dp[0][3] = 0 dp[0][1]=dp[0][2]=dp[0][3]=0
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (!n) return 0;
        vector<vector<int>> dp(n, vector<int>(4, 0));
        dp[0][0] = -prices[0];
        for (int i = 1; i < n; i ++ ) {
            dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];
            dp[i][3] = dp[i - 1][2];
        }
        return max(dp[n - 1][3], max(dp[n - 1][1], dp[n - 1][2]));
    }
};

4.6 买卖股票的最佳时期含手续费(买卖多次,每次有手续费)

Leetcode 714

d p [ i ] [ 0 ] dp[i][0] dp[i][0] 表示第 i i i 天持有股票所剩最多现金。 d p [ i ] [ 1 ] dp[i][1] dp[i][1] 表示第 i i i 天不持有股票所得最多现金。

递推公式:

  • 如果第 i i i 天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来

    • i − 1 i-1 i1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: d p [ i − 1 ] [ 0 ] dp[i - 1][0] dp[i1][0]

    • i i i 天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i1][1]prices[i]

    • d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] ) dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]) dp[i][0]=max(dp[i1][0],dp[i1][1]prices[i])

  • 在来看看如果第 i i i 天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1] 的情况, 依然可以由两个状态推出来

    • i − 1 i-1 i1 天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即: d p [ i − 1 ] [ 1 ] dp[i - 1][1] dp[i1][1]
    • i i i 天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了即: d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] − f e e dp[i - 1][0] + prices[i] - fee dp[i1][0]+prices[i]fee
    • d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] − f e e ) dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee) dp[i][1]=max(dp[i1][1],dp[i1][0]+prices[i]fee)
class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        vector<vector<int>> dp(2, vector<int>(2, 0));
        dp[0][0] = -prices[0];
        for (int i = 1; i < n; i ++ ) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i] - fee);
        }
        return max(dp[(n - 1) % 2][0], dp[(n - 1) % 2][1]);
    }
};

五、子序列问题

5.1 子序列(不连续)

5.1.1 最长上升子序列

Leetcode 300

d p [ i ] dp[i] dp[i] 表示 i i i 之前包括i的以 n u m s [ i ] nums[i] nums[i] 结尾的最长递增子序列的长度

递推公式: i f ( n u m s [ i ] > n u m s [ j ] )    ⇒    d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) if (nums[i] > nums[j])\ \ \Rightarrow \ \ dp[i] = max(dp[i], dp[j] + 1) if(nums[i]>nums[j])    dp[i]=max(dp[i],dp[j]+1)

初始化: d p [ i ] = 1 dp[i] = 1 dp[i]=1

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int len = nums.size();
        if (len <= 1) return len;
        vector<int> dp(len, 1);
        int res = 0;
        for (int i = 1; i < nums.size(); i ++ )
            for (int j = 0; j < i; j ++ ) {
                if (nums[i] > nums[j])
                    dp[i] = max(dp[i], dp[j] + 1);
                res = max(dp[i], res);
            }
        return res;
    }
};

5.1.2 最长递增子序列的个数

Leetcode 673

d p [ i ] dp[i] dp[i] i i i 之前(包括 i i i )最长递增子序列的长度为 d p [ i ] dp[i] dp[i]

c o u n t [ i ] count[i] count[i]:以 n u m s [ i ] nums[i] nums[i] 为结尾的字符串,最长递增子序列的个数为 c o u n t [ i ] count[i] count[i]

递推公式:

  • 对于 d p [ i ] dp[i] dp[i],由上面一题的推导可知 i f ( n u m s [ i ] > n u m s [ j ] )     d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) if (nums[i] > nums[j])\ \ \ dp[i] = max(dp[i], dp[j] + 1) if(nums[i]>nums[j])   dp[i]=max(dp[i],dp[j]+1)
  • 对于 c o u n t [ i ] count[i] count[i],在 n u m s [ i ] > n u m s [ j ] nums[i] > nums[j] nums[i]>nums[j] 的前提下:
    • 如果在 [ 0 , i − 1 ] [0, i-1] [0,i1] 的范围内,找到了 j j j,使得 d p [ j ] + 1 > d p [ i ] dp[j] + 1 > dp[i] dp[j]+1>dp[i],说明找到了一个更长的递增子序列,则有 c o u n t [ i ] = c o u n t [ j ] count[i] = count[j] count[i]=count[j]
    • 如果在 [ 0 , i − 1 ] [0, i-1] [0,i1] 的范围内,找到了 j j j,使得 d p [ j ] + 1 = = d p [ i ] dp[j] + 1 == dp[i] dp[j]+1==dp[i],说明找到了两个相同长度的递增子序列,则有 c o u n t [ i ] + = c o u n t [ j ] count[i] += count[j] count[i]+=count[j]

初始化: d p [ i ] = 1 , c o u n t [ i ] = 1 dp[i] = 1, count[i] = 1 dp[i]=1,count[i]=1

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        if(nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1), count(nums.size(), 1);
        int maxCount = 0;
        for (int i = 1; i < nums.size(); i ++ ) 
            for (int j = 0; j < i; j ++ ) {
                if (nums[i] > nums[j]) {
                    if (dp[j] + 1 > dp[i])
                        dp[i] = dp[j] + 1, count[i] = count[j];
                    else if (dp[j] + 1 == dp[i])
                        count[i] += count[j];
                }
                if (dp[i] > maxCount) maxCount = dp[i];
            }
        int res = 0;
        for (int i = 0; i < nums.size(); i ++ )
            if (maxCount == dp[i]) res += count[i];
        return res;
    }
};

5.1.3 最长公共子序列

Leetcode 1143

d p [ i ] [ j ] dp[i][j] dp[i][j]:长度为 [ 0 , i − 1 ] [0, i - 1] [0,i1] 的字符串 text1 与长度为 [ 0 , j − 1 ] [0, j - 1] [0,j1] 的字符串 text2 的最长公共子序列为 d p [ i ] [ j ] dp[i][j] dp[i][j]

递推公式:

  • 如果 t e x t 1 [ i − 1 ] text1[i - 1] text1[i1] t e x t 2 [ j − 1 ] text2[j - 1] text2[j1] 相同,那么找到了一个公共元素,所以 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i - 1][j - 1] + 1 dp[i][j]=dp[i1][j1]+1;
  • 如果 t e x t 1 [ i − 1 ] text1[i - 1] text1[i1] t e x t 2 [ j − 1 ] text2[j - 1] text2[j1] 不相同,那就看看 t e x t 1 [ 0 , i − 2 ] text1[0, i - 2] text1[0,i2] t e x t 2 [ 0 , j − 1 ] text2[0, j - 1] text2[0,j1] 的最长公共子序列和 t e x t 1 [ 0 , i − 1 ] text1[0, i - 1] text1[0,i1] t e x t 2 [ 0 , j − 2 ] text2[0, j - 2] text2[0,j2] 的最长公共子序列,取最大的。即: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) dp[i][j]=max(dp[i1][j],dp[i][j1])

初始化: d p [ i ] [ 0 ] = d p [ 0 ] [ j ] = 0 dp[i][0] = dp[0][j] = 0 dp[i][0]=dp[0][j]=0

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
        for (int i = 1; i <= text1.size(); i ++ )
            for (int j = 1; j <= text2.size(); 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[text1.size()][text2.size()];
    }
};

5.1.4 不相交的线

Leetcode 1035

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; i ++ )
            for (int j = 1; j <= n; 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[m][n];
    }
};

5.2 子序列(连续)

5.2.1 最长连续递增序列

Leetcode 674

DP

d p [ i ] dp[i] dp[i] 以下标 i i i 为结尾的连续递增的子序列长度为 d p [ i ] dp[i] dp[i]

递推公式:如果 n u m s [ i ] > n u m s [ i − 1 ] nums[i] > nums[i - 1] nums[i]>nums[i1],那么以 i i i 为结尾的连续递增的子序列长度 一定等于 以 i − 1 i - 1 i1 为结尾的连续递增的子序列长度 + 1 + 1 +1 。即: d p [ i ] = d p [ i − 1 ] + 1 dp[i] = dp[i - 1] + 1 dp[i]=dp[i1]+1

初始化: d p [ i ] = 1 dp[i] = 1 dp[i]=1

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

贪心

遇到 n u m s [ i ] > n u m s [ i − 1 ] nums[i] > nums[i - 1] nums[i]>nums[i1] 的情况, c o u n t count count 就 ++,否则 c o u n t count count 1 1 1,记录 c o u n t count count 的最大值就可以了。

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        int len = nums.size();
        if (!len) return 0;
        int res = 1, count = 1; // 至少是1
        for (int i = 1; i < len; i ++ ) {
            if (nums[i] > nums[i - 1]) count ++ ;
            else count = 1;
            res = max(res, count);
        }
        return res;
    }
};

5.2.2 最长重复子数组

Leetcode 718

d p [ i ] [ j ] dp[i][j] dp[i][j] :以下标 i − 1 i - 1 i1 为结尾的 A,和以下标 j − 1 j - 1 j1 为结尾的 B,最长重复子数组长度为 d p [ i ] [ j ] dp[i][j] dp[i][j]

递推公式:当 A [ i − 1 ] A[i - 1] A[i1] B [ j − 1 ] B[j - 1] B[j1] 相等的时候, d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i - 1][j - 1] + 1 dp[i][j]=dp[i1][j1]+1

初始化: d p [ i ] [ 0 ] = d p [ 0 ] [ j ] = 0 dp[i][0] = dp[0][j] = 0 dp[i][0]=dp[0][j]=0

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        int res = 0;
        for (int i = 1; i <= nums1.size(); i ++ )
            for (int j = 1; j <= nums2.size(); j ++ ) {
                if (nums1[i - 1] == nums2[j - 1]) 
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                res = max(res, dp[i][j]);
            }
        return res;
    }
};

滚动数组版本:

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int res = 0;
        vector<int> dp(nums2.size() + 1, 0);
        for (int i = 1; i <= nums1.size(); i ++ )
            for (int j = nums2.size(); j; j -- ) { // 这里从大到小,避免重复覆盖
                if (nums1[i - 1] == nums2[j - 1]) dp[j] = dp[j - 1] + 1;
                else dp[j] = 0; // 不相等要赋值为0
                res = max(res, dp[j]);
            }
        return res;
    }
};

5.2.3 最大子序和

Leetcode 53

贪心

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int res = INT_MIN, count = 0; // count记录中间结果
        for (int i = 0; i < nums.size(); i ++ ) {
            count += nums[i];
            if (count > res) res = count;
            if (count <= 0) count = 0;
        }
        return res;
    }
};

DP

d p [ i ] dp[i] dp[i]:包括下标 i i i(以 n u m s [ i ] nums[i] nums[i] 为结尾)的最大连续子序列和为 d p [ i ] dp[i] dp[i]

递推公式:

  • d p [ i − 1 ] + n u m s [ i ] dp[i - 1] + nums[i] dp[i1]+nums[i],即: n u m s [ i ] nums[i] nums[i] 加入当前连续子序列和
  • n u m s [ i ] nums[i] nums[i],即:从头开始计算当前连续子序列和
  • d p [ i ] = m a x ( d p [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) dp[i] = max(dp[i - 1] + nums[i], nums[i]) dp[i]=max(dp[i1]+nums[i],nums[i])

初始化: d p [ 0 ] = n u m s [ 0 ] dp[0] = nums[0] dp[0]=nums[0]

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int len = nums.size();
        if (!len) return 0;
        vector<int> dp(len, 0);
        dp[0] = nums[0];
        int res = dp[0];
        for (int i = 1; i < nums.size(); i ++ ) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
            res = max(res, dp[i]);
        }
        return res;
    }
};

5.3 编辑距离

5.3.1 判断子序列

Leetcode 392

d p [ i ] [ j ] dp[i][j] dp[i][j]:表示以下标 i − 1 i-1 i1 为结尾的字符串 s s s,和以下标 j − 1 j-1 j1 为结尾的字符串 t t t,相同子序列的长度为 d p [ i ] [ j ] dp[i][j] dp[i][j]

递推公式:

  • i f ( s [ i − 1 ] = = t [ j − 1 ] ) if (s[i - 1] == t[j - 1]) if(s[i1]==t[j1]) t t t 中找到了一个字符在 s s s 中也出现了,那么 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i - 1][j - 1] + 1 dp[i][j]=dp[i1][j1]+1
  • i f ( s [ i − 1 ] ≠ t [ j − 1 ] ) if (s[i - 1] \ne t[j - 1]) if(s[i1]=t[j1]):此时相当于 t t t 要删除元素, t t t 如果把当前元素 t [ j − 1 ] t[j - 1] t[j1] 删除,那么 d p [ i ] [ j ] dp[i][j] dp[i][j] 的数值就是看 s [ i − 1 ] s[i - 1] s[i1] t [ j − 2 ] t[j - 2] t[j2] 的比较结果了,即: d p [ i ] [ j ] = d p [ i ] [ j − 1 ] dp[i][j] = dp[i][j - 1] dp[i][j]=dp[i][j1]

初始化: d p [ 0 ] [ 0 ] = d p [ i ] [ 0 ] = 0 dp[0][0] = dp[i][0] = 0 dp[0][0]=dp[i][0]=0,因为递推公式是从 [ i − 1 ] [ j − 1 ] [i - 1][j - 1] [i1][j1] [ i ] [ j − 1 ] [i][j - 1] [i][j1] 推导而来的。

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int m = s.size(), n = t.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        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] = dp[i][j - 1];
        if (dp[m][n] == m) return true;
        return false;
    }
};

5.3.2 不同的子序列

Leetcode 115

这道题目如果不是子序列,而是要求连续序列的,那就可以考虑用KMP。

d p [ i ] [ j ] dp[i][j] dp[i][j]:以 i − 1 i-1 i1 为结尾的 s s s 子序列中出现以 j − 1 j-1 j1 为结尾的 t t t 的个数为 d p [ i ] [ j ] dp[i][j] dp[i][j]

递推公式:

  • s [ i − 1 ] = = t [ j − 1 ] s[i - 1] == t[j - 1] s[i1]==t[j1]
    • s [ i − 1 ] s[i - 1] s[i1] 来匹配,那么个数为 d p [ i − 1 ] [ j − 1 ] dp[i - 1][j - 1] dp[i1][j1]。即不需要考虑当前 s s s 子串和 t t t 子串的最后一位字母,所以只需要 d p [ i − 1 ] [ j − 1 ] dp[i-1][j-1] dp[i1][j1]
    • 一部分是不用 s [ i − 1 ] s[i - 1] s[i1] 来匹配,个数为 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i1][j]。例如,有字符串 s : b a g g s:bagg sbagg t : b a g t:bag tbag s [ 3 ] s[3] s[3] t [ 2 ] t[2] t[2] 是相同的,但是字符串 s s s 也可以不用 s [ 3 ] s[3] s[3] 来匹配,即用 s [ 0 ] s [ 1 ] s [ 2 ] s[0]s[1]s[2] s[0]s[1]s[2] 组成的 b a g bag bag。当然也可以用 s [ 3 ] s[3] s[3] 来匹配,即: s [ 0 ] s [ 1 ] s [ 3 ] s[0]s[1]s[3] s[0]s[1]s[3] 组成的 b a g bag bag
    • d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] dp[i][j]=dp[i1][j1]+dp[i1][j]
  • s [ i − 1 ] ≠ t [ j − 1 ] s[i - 1] \ne t[j - 1] s[i1]=t[j1]
    • 不用 s [ i − 1 ] s[i - 1] s[i1] 来匹配(就是模拟在 s s s 中删除这个元素),即: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i1][j]

初始化:

  • d p [ i ] [ 0 ] = d p [ 0 ] [ 0 ] = 1 dp[i][0] = dp[0][0] = 1 dp[i][0]=dp[0][0]=1
  • d p [ 0 ] [ j ] = 0 dp[0][j] = 0 dp[0][j]=0
class Solution {
public:
    int numDistinct(string s, string t) {
        vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size()+ 1, 0));
        for (int i = 0; i < s.size(); i ++ ) dp[i][0] = 1;
        for (int i = 1; i <= s.size(); i ++ )
            for (int j = 1; j <= t.size(); j ++ ) 
                if (s[i - 1] == t[j - 1]) 
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                else dp[i][j] = dp[i - 1][j];
        return dp[s.size()][t.size()];
    }
};

5.3.3 两个字符串的删除操作

Leetcode 583

d p [ i ] [ j ] dp[i][j] dp[i][j]:以 i − 1 i-1 i1 为结尾的字符串 word1,和以 j − 1 j-1 j1 位结尾的字符串 word2,想要达到相等,所需要删除元素的最少次数。

递推公式:

  • w o r d 1 [ i − 1 ] word1[i - 1] word1[i1] w o r d 2 [ j − 1 ] word2[j - 1] word2[j1] 相同的时候, d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j] = dp[i - 1][j - 1] dp[i][j]=dp[i1][j1];
  • w o r d 1 [ i − 1 ] word1[i - 1] word1[i1] w o r d 2 [ j − 1 ] word2[j - 1] word2[j1] 不相同的时候,有三种情况:
    • w o r d 1 [ i − 1 ] word1[i - 1] word1[i1],最少操作次数为 d p [ i − 1 ] [ j ] + 1 dp[i - 1][j] + 1 dp[i1][j]+1
    • w o r d 2 [ j − 1 ] word2[j - 1] word2[j1],最少操作次数为 d p [ i ] [ j − 1 ] + 1 dp[i][j - 1] + 1 dp[i][j1]+1
    • 同时删 w o r d 1 [ i − 1 ] word1[i - 1] word1[i1] w o r d 2 [ j − 1 ] word2[j - 1] word2[j1],操作的最少次数为 d p [ i − 1 ] [ j − 1 ] + 2 dp[i - 1][j - 1] + 2 dp[i1][j1]+2
    • d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j − 1 ] + 2 , d p [ i − 1 ] [ j ] + 1 , d p [ i ] [ j − 1 ] + 1 ) dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1}) dp[i][j]=min(dp[i1][j1]+2,dp[i1][j]+1,dp[i][j1]+1)
    • 因为 d p [ i ] [ j − 1 ] + 1 = d p [ i − 1 ] [ j − 1 ] + 2 dp[i][j - 1] + 1 = dp[i - 1][j - 1] + 2 dp[i][j1]+1=dp[i1][j1]+2,所以递推公式可简化为: d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] + 1 , d p [ i ] [ j − 1 ] + 1 ) dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1) dp[i][j]=min(dp[i1][j]+1,dp[i][j1]+1)

初始化: d p [ i ] [ 0 ] = i , d p [ 0 ] [ j ] = j dp[i][0] = i,dp[0][j] = j dp[i][0]=idp[0][j]=j

class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
        for (int i = 0; i <= word1.size(); i ++ ) dp[i][0] = i;
        for (int i = 0; i <= word2.size(); i ++ ) dp[0][i] = i;
        for (int i = 1; i <= word1.size(); i ++ )
            for (int j = 1; j <= word2.size(); 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] + 1, dp[i][j - 1] + 1);
        return dp[word1.size()][word2.size()];
    }
};

使用最长公共子序列的思路,找出两个序列的最长公共子序列,那么本题结果就是两个字符串长度之和减去最长公共组序列长度的两倍。

class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
        for (int i = 1; i <= word1.size(); i ++ )
            for (int j = 1; j <= word2.size(); j ++ )
                if (word1[i - 1] == word2[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 word1.size() + word2.size() - dp[word1.size()][word2.size()] * 2;
    }
};

5.3.4 编辑距离

Leetcode 72

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示以下标 i − 1 i-1 i1 为结尾的字符串 word1,和以下标 j − 1 j-1 j1 为结尾的字符串word2,最近编辑距离为 d p [ i ] [ j ] dp[i][j] dp[i][j]

递推公式:

  • w o r d 1 [ i − 1 ] = = w o r d 2 [ j − 1 ] word1[i - 1] == word2[j - 1] word1[i1]==word2[j1] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j] = dp[i - 1][j - 1] dp[i][j]=dp[i1][j1]
  • w o r d 1 [ i − 1 ] ≠ w o r d 2 [ j − 1 ] word1[i - 1] \ne word2[j - 1] word1[i1]=word2[j1]
    • word1 删除一个元素,那么就是以下标 i − 2 i - 2 i2 为结尾的 word1 与 j − 1 j-1 j1 为结尾的 word2 的最近编辑距离再加上一个操作。即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + 1 dp[i][j] = dp[i - 1][j] + 1 dp[i][j]=dp[i1][j]+1
    • word2 删除一个元素,那么就是以下标 i − 1 i - 1 i1 为结尾的 word1 与 j − 2 j-2 j2 为结尾的 word2 的最近编辑距离再加上一个操作。即 d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + 1 dp[i][j] = dp[i][j - 1] + 1 dp[i][j]=dp[i][j1]+1
    • 替换元素,word1 替换 w o r d 1 [ i − 1 ] word1[i - 1] word1[i1],使其与 w o r d 2 [ j − 1 ] word2[j - 1] word2[j1] 相同,此时不用增删加元素。即: d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i - 1][j - 1] + 1 dp[i][j]=dp[i1][j1]+1
    • d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j − 1 ] , d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + 1 dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1 dp[i][j]=min(dp[i1][j1],dp[i1][j],dp[i][j1])+1
    • 注意:上面的操作中没有删除,是因为word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a"word1 删除元素 'd'word2 添加一个元素 'd',变成 word1="a", word2="ad", 最终的操作数是一样!

初始化: d p [ i ] [ 0 ] = i , d p [ 0 ] [ j ] = j dp[i][0] = i,dp[0][j] = j dp[i][0]=idp[0][j]=j

class Solution {
public:
    int minDistance(string word1, string word2) {
        vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
        for (int i = 0; i <= word1.size(); i ++ ) dp[i][0] = i;
        for (int i = 0; i <= word2.size(); i ++ ) dp[0][i] = i;
        for (int i = 1; i <= word1.size(); i ++ )
            for (int j = 1; j <= word2.size(); 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 - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
        return dp[word1.size()][word2.size()];
    }
};

5.4 回文

5.4.1 回文子串

Leetcode 647

DP思路

d p [ i ] [ j ] dp[i][j] dp[i][j]:表示区间范围 [ i , j ] [i,j] [i,j] (注意是左闭右闭)的子串是否是回文子串,如果是 d p [ i ] [ j ] dp[i][j] dp[i][j] 为true,否则为false。

递推公式:

  • s [ i ] ≠ s [ j ] s[i] \ne s[j] s[i]=s[j] d p [ i ] [ j ] = f a l s e dp[i][j] = false dp[i][j]=false
  • s [ i ] = = s [ j ] s[i] == s[j] s[i]==s[j]
    • 下标 i i i j j j 相同,是回文子串
    • 下标 i i i j j j 相差为 1 1 1,例如 aa,也是回文子串
    • 下标 i i i j j j 相差大于 1 1 1 的时候,例如 cabac,此时 s [ i ] s[i] s[i] s [ j ] s[j] s[j] 已经相同了,我们看 i i i j j j 区间是不是回文子串就看 aba 是不是回文就可以了,那么 aba 的区间就是 i + 1 i+1 i+1 j − 1 j-1 j1 区间,这个区间是不是回文就看 d p [ i + 1 ] [ j − 1 ] dp[i + 1][j - 1] dp[i+1][j1] 是否为 true

初始化: d p [ i ] [ j ] = f a l s e dp[i][j]=false dp[i][j]=false

遍历顺序:根据递归公式,这个题目对于 i i i 要从大到小, j j j 要从小到大遍历。并且由于 dp 的定义,一定有 i < = j i <= j i<=j

class Solution {
public:
    int countSubstrings(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
        int res = 0;
        for (int i = s.size() - 1; i >= 0; i -- )
            for (int j = i; j < s.size(); j ++ )
                if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1]))
                    res ++ , dp[i][j] = true;
        return res;
    }
};

双指针

首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以了。在遍历中心点的时候,要注意中心点有两种情况

class Solution {
public:
    int countSubstrings(string s) {
        int res = 0;
        for (int i = 0; i < s.size(); i ++ ) {
            res += extend(s, i, i, s.size()); // 以i为中心
            res += extend(s, i, i + 1, s.size()); // 以i和i+1为中心
        }
        return res;
    }

    int extend(const string& s, int i, int j, int n) {
        int res = 0;
        while (i >= 0 && j < n && s[i] == s[j])
            i -- , j ++ , res ++ ;
        return res;
    }
};

5.4.2 最长回文子串

Leetcode 5

DP

大体思路与上题动态规划方法一样,只需要在得到 [ i , j ] [i,j] [i,j] 区间是否是回文子串的时候,直接保存最长回文子串的左边界和右边界。

class Solution {
public:
    string longestPalindrome(string s) {
        vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), 0));
        int maxLen = 0, left = 0, right = 0;
        for (int i = s.size() - 1; i >= 0; i -- ) 
            for (int j = i; j < s.size(); j ++ ) {
                if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) dp[i][j] = true;
                if (dp[i][j] && j - i + 1 > maxLen)
                    maxLen = j - i + 1, left = i, right = j;
            }
        return s.substr(left, maxLen);
    }
};

双指针

class Solution {
public:
    int left = 0, right = 0, maxLen = 0;
    
    string longestPalindrome(string s) {
        for (int i = 0; i < s.size(); i ++ ) {
            extend(s, i, i, s.size());
            extend(s, i, i + 1, s.size());
        }
        return s.substr(left, maxLen);
    }

    void extend(const string& s, int i, int j, int n) {
        while (i >= 0 && j < n && s[i] == s[j]) {
            if (j - i + 1 > maxLen)
                maxLen = j - i + 1, left = i, right = j;
            i -- , j ++ ;
        }
    }
};

5.4.3 最长回文子序列

Leetcode 516

回文子串是要连续的,回文子序列可不是连续的!

d p [ i ] [ j ] dp[i][j] dp[i][j]:字符串 s s s [ i , j ] [i, j] [i,j] 范围内最长的回文子序列的长度为 d p [ i ] [ j ] dp[i][j] dp[i][j]

递推公式:

  • s [ i ] = s [ j ] s[i]=s[j] s[i]=s[j] d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 2 dp[i][j] = dp[i + 1][j - 1] + 2 dp[i][j]=dp[i+1][j1]+2
  • s [ i ] ≠ s [ j ] s[i]\ne s[j] s[i]=s[j]
    • 加入 s [ j ] s[j] s[j] 的回文子序列长度为 d p [ i + 1 ] [ j ] dp[i + 1][j] dp[i+1][j]
    • 加入 s [ i ] s[i] s[i] 的回文子序列长度为 d p [ i ] [ j − 1 ] dp[i][j - 1] dp[i][j1]
    • d p [ i ] [ j ] = m a x ( d p [ i + 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) dp[i][j]=max(dp[i+1][j],dp[i][j1])

初始化:

  • 从递推公式: d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 2 dp[i][j] = dp[i + 1][j - 1] + 2 dp[i][j]=dp[i+1][j1]+2;可以看出递推公式是计算不到 i i i j j j 相同时候的情况,因此需要初始化 d p [ i ] [ j ] = 1 dp[i][j] = 1 dp[i][j]=1
  • 其余情况 d p [ i ] [ j ] = 0 dp[i][j] = 0 dp[i][j]=0,保证递推公式: d p [ i ] [ j ] = m a x ( d p [ i + 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) dp[i][j]=max(dp[i+1][j],dp[i][j1]) 不会被初始值覆盖
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        for (int i = 0; i < s.size(); i ++ ) dp[i][i] = 1;
        for (int i = s.size() - 1; i >= 0; i -- )
            for (int j = i + 1; j < s.size(); j ++ ) // i=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][s.size() - 1];
    }
};

5.4.4 分割回文串 Ⅱ

Leetcode 132

d p [ i ] dp[i] dp[i]:范围是 [ 0 , i ] [0, i] [0,i] 的回文子串,最少分割次数是 d p [ i ] dp[i] dp[i]

递推公式:

如果要对长度为 [ 0 , i ] [0, i] [0,i] 的子串进行分割,分割点为 j j j。那么如果分割后,区间 [ j + 1 , i ] [j + 1, i] [j+1,i] 是回文子串,那么 d p [ i ] dp[i] dp[i] 就等于 d p [ j ] + 1 dp[j] + 1 dp[j]+1,而区间 [ 0 , j ] [0,j] [0,j] 的最小分割次数就等于 d p [ j ] dp[j] dp[j]

最后递推公式为:dp[i] = min(dp[i], dp[j] + 1)

初始化: d p [ 0 ] = 0 , d p [ i ≠ 0 ] = I N T _ M A X dp[0] = 0, dp[i \ne 0]=INT\_MAX dp[0]=0,dp[i=0]=INT_MAX

class Solution {
public:
    int minCut(string s) {
        vector<vector<bool>> isPalindromic(s.size(), vector<bool>(s.size(), false));
        for (int i = s.size() - 1; i >= 0; i -- )
            for (int j = i; j < s.size(); j ++ )
                if (s[i] == s[j] && (j - i <= 1 || isPalindromic[i + 1][j - 1]))
                    isPalindromic[i][j] = true;

        // 初始化:dp[i] 的最大值就是 i,将每个字符都分割出来
        // 并没有赋值为 INT_MAX,这样更具有 dp[i] 的定义
        vector<int> dp(s.size(), 0);
        for (int i = 0; i < s.size(); i ++ ) dp[i] = i;

        for (int i = 1; i < s.size(); i ++ ) {
            if (isPalindromic[0][i]) { // 如果是回文串就不用分割了
                dp[i] = 0;
                continue;
            }
            for (int j = 0; j < i; j ++ )
                if (isPalindromic[j + 1][i])
                    dp[i] = min(dp[i], dp[j] + 1);
        }
        return dp[s.size() - 1];
    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值