动态规划(2)---回文串/子串系列题目

动态规划第一篇详见此博客,介绍了理论和股票买卖系列题目
此篇介绍其他主题的动态规划题目:如 最大递增序列,回文串等等。

题目示例

leetcode300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组
[0,3,1,6,2,2,7] 的子序列。

法1.动态规划

思路分析

我们用dp[i]表示数组的前i个元素构成的最长上升子序列,如果要求dp[i],我们需要用num[i]和前面的数字一个个比较,如果比前面的任何一个数字大,说明加入到他的后面可以构成一个上升子序列,就更新dp[i]。

  • 数组dp[i]记录的就是 对应以nums[i]中每一个数结尾时 的最长子序列长度

我们就以[8,2,3,1,4]为例来画个图看一下
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> dp(nums.size(), 1);//DP数组记录 以nums中每个位置 为结尾时 的 最长子序列长度
        
        for(int i = 0; i < nums.size(); i++){
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i]){
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
        }
        return *max_element(dp.begin(), dp.end());
    }
};

法2.贪心+二分查找

暂不记录,可参考官方解答

leetcode354. 俄罗斯套娃信封问题

给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h)出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
说明: 不允许旋转信封。

法1.动态规划

思路分析

今天的题目要求信封套信封最多套多少层,并且套的过程中信封长与宽不能旋转;长或者宽相等的时候,两个信封不能套在一起。
可以抽象成:

  题意:找出二维数组的一个排列,使得其中有最长的单调递增子序列(两个维度都递增)。

我在之前的题解里面讲过:「遇事不决先排序」,排序能让数据变成有序的,降低了混乱程度,往往就能帮助我们理清思路。本题也是如此。

1. 两个维度都递增的排序

第一感觉肯定是各种语言默认的排序方法:两个维度都递增的顺序。
对于题目给出的[[5,4],[6,4],[6,7],[2,3]]示例,如果按照两个维度都递增的排序方法,会得到:

  [[2, 3], [5, 4], [6, 4], [6, 7]] 

然后我们利用最长递增子序列的方法,即使用动态规划,定义dp[i] 表示以 i 结尾的最长递增子序列的长度。对每个 i 的位置,遍历 [0,i),对两个维度同时判断是否是严格递增(不可相等)的,如果是的话,dp[i] = max(dp[i], dp[j] +1)。

这个方法对应的代码是:

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        if not envelopes:
            return 0
        N = len(envelopes)
        envelopes.sort()
        res = 0
        dp = [1] * N
        for i in range(N):
            for j in range(i):
                if envelopes[j][0] < envelopes[i][0] and envelopes[j][1] < envelopes[i][1]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

2. 第一维递增,第二维递减的排序

上面的方法,我们在循环中对两个维度都进行判断是否严格递增的。其实有个技巧,可以减少第一个维度的判断。
先看个例子,假如排序的结果是下面这样:

  [[2, 3], [5, 4], [6, 5], [6, 7]] 

如果我们只看第二个维度 [3, 4, 5,7],会得出最长递增子序列的长度是 4 的结论。实际上,由于第 3 和第 4 个信封的第一个维度都是6,导致他们不能套娃。所以,利用第一个维度递增,第二个维度递减的顺序排序,会得到下面的结果:

  [[2, 3], [5, 4], [6, 7], [6, 5]] 

这个时候,只看第二个维度 [3, 4, 7,5],就会得到最长递增子序列的长度是 3 的正确结果。

该方法对应的代码为:

class Solution {
public:
    //要对两个维度排序,且多个信封有同一个维度一样大时只能选一个
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        vector<int> ans(envelopes.size(), 1);
        //按w升序,w相同时h降序 对原数组排序,排完后直接遍历h,找到h的最长升序子序列长度即使答案
        sort(envelopes.begin(), envelopes.end(), [](const auto& e1, const auto& e2){
            return e1[0] < e2[0] || (e1[0] == e2[0] && e1[1] > e2[1]);
        });
        //定义状态 f[i] 为考虑前 i 个物品,并以第 i 个物品为结尾的最大值。
        //为什么要DP查找,因为每个数结尾和前面数组成的长度都有很多种情况,所以要动态查找并记录。确定每一个位置结尾时的所有情况最大值,最终再在这些
        //最大值里找到 (最大值)答案
        for(int i = 0; i < envelopes.size(); i++){
            for(int j = 0; j < i; j++){
                if(envelopes[j][1] < envelopes[i][1]){
                    ans[i] = max(ans[i], ans[j]+1);
                }
            }
        }
        return *max_element(ans.begin(), ans.end());
    }
};

法2.二分法+动态规划 / 树状数组+动态规划

前面的方法: 时间复杂度:O(n^2), 空间复杂度:O(n)
时间复杂度可以优化到O(nlogn),此处暂不记录,可参考此篇讲解

leetcode647. 回文子串

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

思路

1. 动态规划

如何快速判断连续一段 [i, j] 是否为回文串? 我们不可能每次都使用双指针去线性扫描一遍 [i, j] 判断是否回文。

一个直观的做法是,我们先预处理除所有的 f[i][j](动态规划数组,动规表),f[i][j] 代表 [i, j]这一段是否为回文串

预处理 f[i][j] 的过程可以用递推去做。

要想 f[i][j] == true ,必须满足以下两个条件:

  - f[i + 1][j - 1] == true      //中间子串必须回文
  - s[i] == s[j]			     //两端字符一样 

由于状态 f[i][j] 依赖于状态 f[i + 1][j - 1],因此需要我们左端点 i 是从大到小进行遍历;而右端点 j 是从小到大进行遍历

我们的遍历过程可以整理为:右端点 j 一直往右移动(从小到大),在 j 固定情况下,左端点 i 在 j 在左边开始,一直往左移动(从大到小)

class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size(), cnt = 0;
        vector<vector<int>> dp(n, vector<int>(n));
        for(int j = 0; j < n; j++){
            for(int i = j; i >= 0; i--){
                if(i == j){//当[i, j]只有一个字符时,必然是回文串
                    dp[i][j] = 1;
                    cnt++;
                }else if(i + 1 == j){//当[i, j]长度为2时,满足 cs[i] == cs[j] 即回文串
                    if(s[i] == s[j]){
                        dp[i][j] = 1;
                        cnt++;
                    }
                }else{//当[i, j]长度大于2时,满足[i + 1]到[j - 1]回文且两端一样, 即回文串
                    if(dp[i+1][j-1] && s[i] ==s[j]){
                        dp[i][j] = 1;
                        cnt++;
                    }
                }
            }
        }
        return cnt;
    }
};

2. 中心扩散等其他解法
详见 其他解答

leetcode5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

思路

思路同上题,额外比较 更新一下最长字串

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));//记录i-j是否为回文子串
        string ans;//回文子串长度即为ans.size()

        for(int j = 0; j < n; j++){
            for(int i = j; i >= 0; i--){
                if(i == j){//只有一个字符(对应动规表的对角线)必是回文
                    dp[i][j] = 1;
                }else if(i + 1 == j){//两个字符时
                    dp[i][j] = s[i] == s[j];
                }else{//长度>2,由里面子串和两端决定,对应动规表中最下角和自身
                    dp[i][j] = dp[i+1][j-1] && s[i] == s[j];
                }
                //判断是否最长并记录
                if(dp[i][j] && j - i + 1 > ans.size()){
                    ans = s.substr(i, j - i + 1);
                }
            }
        }
        return ans;
    }
};

leetcode132. 分割回文串 II

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。

思路

如果 考察回溯算法的力扣 131题分割回文串,你有使用动态规划做预处理的话,或上面两题做过的话,可以很快想到如何求得任意段[i,j]是否为回文子串。

  1. 快速判断「任意一段子串是否回文」思路
    剩下的问题是,我们如何快速判断连续一段 [i, j] 是否为回文串,做法和上面的题 一模一样。
  • PS. 131题,数据范围只有 16,因此我们可以不使用 DP 进行预处理,而是使用双指针来判断是否回文也能过。但是该题数据范围为2000(数量级为 103),使用朴素做法判断是否回文的话,复杂度会去到 O(n3)(计算量为 109),必然超时。
    因此我们不可能每次都使用双指针去线性扫描一遍 [i, j] 判断是否回文。 一个直观的做法是,我们先预处理除所有的
    f[i][j],f[i][j] 代表 [i, j] 这一段是否为回文串。具体思路见上题。
  1. 递推「最小分割次数」思路
    我们定义 f[i] 为以下标为 i 的字符作为结尾的最小分割次数,那么最终答案为 f[n - 1]。
    不失一般性的考虑第 j 字符的分割方案:

    • 从起点字符到第 j 个字符能形成回文串,那么最小分割次数为 0。此时有 f[j] = 0
    • 从起点字符到第 j 个字符不能形成回文串:
      2.1 该字符独立消耗一次分割次数。此时有 f[j] = f[j - 1] + 1
      2.2 该字符不独立消耗一次分割次数,而是与前面的某个位置 i 形成回文串,[i, j] 作为整体消耗一次分割次数。此时有 f[j] = f[i - 1] + 1
      在 2.2 中满足回文要求的位置 i 可能有很多,我们在所有方案中取一个 min 即可。

第一个动态规划是快速判断所有回文子串,第二个动态规划是快速推出以某个字符结尾的最小分割次数。

class Solution {
public:
    int minCut(string s) {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));
        //动态规划找到所有子串是否为回文子串
        for(int j = 0; j < n; j++){
            for(int i = j; i >= 0; i--){
                if(i == j)
                    dp[i][j] = 1;
                else if(i + 1 == j)
                    dp[i][j] = s[i] == s[j];
                else
                    dp[i][j] = dp[i + 1][j - 1] && s[i] == s[j];
            }
        }
        //根据dp表找最少分割次数,同样使用动态规划:f[i]表示以i结尾的子串最少分割次数
        vector<int> ans(n);
        for(int i = 0; i < n; i++) ans[i] = i;//初始化最大值,每位都需要分割一次
        for(int j = 1; j < n; j++){
            if(dp[0][j]){// 如果 [0,j] 这一段直接构成回文,则无须分割
                ans[j] = 0;
            }
            else{//如果无法直接构成回文:那么对于第 j 个字符,有使用分割次数,或者不使用分割次数两种选择
                for(int i = 0; i < j; i++){
                    if(dp[i+1][j]){//i+1和j之间构成回文子串;其中,当i=j-1时,j自己独立占了一个分割次数
                        ans[j] = min(ans[j], ans[i] + 1);
                    }
                }
            }
        }
        return ans[n-1];
    }
};

leetcode115. 不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。

思路分析

dp[i][j] 代表 T 前 i 字符串可以由 S j 字符串组成最多个数.

所以动态方程:

当 S[j] == T[i] , dp[i][j] = dp[i-1][j-1] + dp[i][j-1];

当 S[j] != T[i] , dp[i][j] = dp[i][j-1]
在这里插入图片描述

  1. 对于第一行, T 为空,因为空集是所有字符串子集, 所以我们第一行都是 1

  2. 对于第一列, S 为空,这样组成 T 个数当然为 0` 了

  3. 对于某个 dp[i][j] 而言,若s[j] == t[i],包含两类决策:

  • 不选择:不让 s[j] 参与匹配,也就是需要让 s 中 [0,j-1]个字符去匹配 t 中的 [0,i]字符。此时匹配值为 dp[i][j-1]
  • 选择:让 s[j] 参与匹配,这时候只需要让 s 中 [0,j-1]个字符去匹配 t 中的 [0,i-1][0,i−1] 字符即可,同时满足 s[i]=t[j]。此时匹配值为 dp[i-1][j-1]
    最终 dp[i][j] 就是两者之和
  1. 若s[j] != t[i] ,dp[i][j]的值 只和 dp[i][j-1] 一样,也就是当前的无法匹配只能和前一个匹配的数量相等。
    详见代码和注释~
class Solution {
public:
    int numDistinct(string s, string t) {
        // 技巧:往原字符头部插入空格,这样得到 string 字符串是从 1 开始
        // 同时由于往头部插入相同的(不存在的)字符,不会对结果造成影响,而且可以使得 dp[0][j] = 1,可以将 1 这个结果滚动下去
        int n = s.size(), m = t.size();
        s = " " + s;
        t = " " + t;
        // dp(i,j) 代表考虑「s 中的下标为 0~j 字符」和「t 中下标为 0~i 字符」是否匹配
        vector<vector<long>> dp(m+1, vector<long>(n+1));
        for(int j = 0; j < n+1; j++) dp[0][j] = 1;
        for(int i = 1; i < m+1; i++){
            for(int j = 1; j < n+1; j++){
                if(s[j] == t[i])// 使用 s[j] 进行匹配,则要求 s[j] == t[i],然后有选和不选两种  dp[i][j] = dp[i - 1][j - 1] + dp[i][j-1]
                    dp[i][j] = dp[i-1][j-1]  dp[i][j-1];
                else//两个不相等,则数量和 s[j-1]匹配的一样
                    dp[i][j] = dp[i][j-1];
            }
        }
        return dp[m][n];
    }
};

分享一个较好的解答

leetcode10.

leetcode53.最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        int dp = 0, max_sum = INT_MIN;
        for(int i = 0; i < n; i++){
            dp = max(nums[i], dp + nums[i]);//要么加入前面子串,要么不加入自己开始
            max_sum = max(dp, max_sum);//重新开始子串,也要和前面已找过的子串和比较
        }
        return max_sum;
    }
};

多解法可参考此讲解

leetcode72.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值