如何解决子序列问题

这篇是动态规划最后一篇文章,我感觉这类问题难度有点大,重点在子序列的元素在原序列当中是否连续。

子序列不连续问题

最长递增子序列

题目:300. 最长递增子序列 - 力扣(LeetCode)

  • dp数组及下标含义

dp[i]表示到第i个元素时,最长递增子序列的长度

  • 递推公式

如果dp[j] < dp[i],dp[i] = max(dp[i], dp[j] + 1) j为0到i - 1的数

  • 初始化

全初始化为1,只有一个元素,则子序列长度为1

  • 遍历顺序

从小到大,因为dp[i]的值由下标比它小的值推出

int lengthOfLIS(vector<int> nums){
    int result = 0;
    vector<int> dp(nums.size(), 1);
    for(int i = 1; i < nums.size(); i++)
        for(int j = 0; j < i; j++){
            if(dp[j] < dp[i])
                dp[i] = max(dp[i], dp[j] + 1);
        }
        if(dp[i] > result)
            result = dp[i];
    }
    return result;
}

上述方法的时间复杂度为O(),在最长上升子序列(二)_牛客题霸_牛客网 (nowcoder.com)就会运行超时。下面介绍一种结合二分查找的动态规划方法。

dp[i]有两个来源:

  • 前面的数都比它大,则dp[i] = 1

  • 找到最长递增子序列中,替换第一个比它大的,可以让更多略大于它的数加入进来(贪心策略)

找出最长递增序列第一个比nums[i]大的下标,就可以使用二分查找

int lengthOfLIS(vector<int> nums){
    int result = 1;
    vector<int> number(nums.size(), nums[0]);
    for(int i = 1; i < nums.size(); i++){
        int low = 0, high = result;
        while(low < high){
            int mid = (low + high) / 2;
            if(number[mid] < nums[i])
                low = mid + 1;
            else
                high = mid;
        }
        if(low == result)
            result++;
        number[low] = nums[i];
    }
    return result;
}

最长公共子序列

题目:给定两个字符串text1和text2,返回两个字符串的最长公共子序列的长度。一个字符串的子序列是指:它由原字符串在不改变字符的相对顺序的情况下删除某些字符后组成的新字符串。

示例1:

输入:text1 = "abcde", text2 = "ace"

输出:3

示例2:

输入:text1 = "abc", text2 = "abc"

输出:3

示例3:

输入:text1 = "abc", text2 = "def"

输出:0

  • dp数组及下标含义

dp[i][j]表示text1下标i - 1之前部分与text2下标j - 1前的最长公共子序列长度

  • 递推公式

如果text1[i - 1] == text2[j - 1],dp[i][j] = dp[i - 1][j - 1] + 1

如果不相等,dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

  • 初始化

全都初始化为0

  • 遍历顺序

从小到大

int longestCommonSubsequence(string text1, string text2){
    vector<vector<int>> dp(text1.length() + 1, vector<int>(text2.length() + 1, 0));
    for(int i = 1; i <= text1.length(); i++){
        for(int j = 1; j <= text2.length(); j++){
            if(text1[i] == text2[j])
                dp[i][j] = dp[i - 1][j - 1] + 1;
            else
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
        }
    }
    return dp[text1.length()][text2.length()];
}

不同的子序列

题目:115. 不同的子序列 - 力扣(LeetCode)

  • dp数组及下标含义

dp[i][j]表示字符串s下标为i - 1的部分含t下标为j - 1部分的个数

  • 疑问:为什么dp数组的i表示字符串的下标为i - 1,而不是i?

  • 答:为了初始化简单,如果表示为下标i,以dp[0][j]为例,如果s[0] == t[0],dp[0][0]初始化为1,反之,初始化为0,dp[0][j] = dp[0][j - 1],如果s[0] == t[j],dp[0][j] += 1

  • 递推公式

如果s[i - 1] == t[j - 1],dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]

  • 疑问:为什么要加dp[i - 1][j]?

  • 答:因为s[i - 1]与t[j - 1]匹配,还可能与t[j]匹配

如果s[i - 1] != t[j - 1],dp[i][j] = dp[i - 1][j]

  • 初始化

dp[0][j]都初始化为0,因为s为空字符串,不可能包含t

dp[i][0]都初始化为1,因为t为空字符串,s删除全部元素,就是t

  • 遍历顺序

从小到大

int numDistinct(string s, string t) {
    vector<vector<int>> dp(s.length() + 1, vector<int>(t.length() + 1, 0));
    for(int i = 0; i <= s.length(); i++)
        dp[i][0] = 1;
    for(int i = 1; i <= s.length(); i++){
        for(int j = 1; j <= t.length(); j++){
            if(s[i - 1] == t[i - 1])
                dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
            else
                dp[i][j] = dp[i - 1][j];
        }
    }
    return dp[s.length()][t.length()];
}

子序列连续问题

最长连续递增序列

题目:674. 最长连续递增序列 - 力扣(LeetCode)

  • dp数组及下标含义

dp[i]表示到第i个元素时,最长连续递增子序列的长度

  • 递推公式

如果(dp[i] > dp[i - 1]),dp[i] = dp[i - 1] + 1

  • 初始化

dp数组全部初始化为1

  • 遍历顺序

从小到大

int findLengthOfLCIS(vector<int> nums){
    int result = 1;
    vector<int> dp(nums.size(), 1);
    for(int i = 1; i < nums.size(); i++){
        if(dp[i] > dp[i - 1])
            dp[i] = dp[i - 1] + 1;
        if(result < dp[i])
            result = dp[i];
    }
    return result;
}

最长重复子数组

题目:718. 最长重复子数组 - 力扣(LeetCode)

  • dp数组及下标含义

dp[i][j]表示数组A的下标i前部分和数组B的下标j前部分重复子数组的长度

  • 递推公式

如果A[i] == B[j],dp[i][j] = dp[i - 1][j - 1] + 1

  • 初始化

如果A[i] == B[j],dp[i][j] = 1,否则dp[i][j] = 0

  • 遍历顺序

从小到大

int findLength(vector<int> A, vector<int> B){
    int result = 0;
    vector<vector<int>> dp(A.size(), vector<int>(B.size(), 0));
    for(int i = 0; i < A.size(); i++){
        for(int j = 0; j < B.size(); j++){
            if(A[i] == B[j])
                dp[i][j] = 1;
        }
    }
    for(int i = 1; i < A.size(); i++){
        for(int j = 1; j < B.size(); j++){
            if(A[i] == B[j])
                dp[i][j] = dp[i - 1][j - 1] + 1;
            if(result < dp[i][j])
                result = dp[i][j];
        }
    }
    return result;
}

最大子序和

题目:53. 最大子数组和 - 力扣(LeetCode)

  • dp数组及下标含义

dp[i]表示[0, i]之间的连续子数组的最大和

  • 递推公式

dp[i] = max(dp[i - 1] + nums[i], nums[i])

  • 初始化

dp[0]初始化为nums[i]

  • 遍历顺序

从小到大

int maxSubArray(vector<int>& nums) {
    int result = 0;
    vector<int> dp(nums.size(), 0);
    dp[0] = nums[0];
    for(int i = 1; i < nums.size(); i++){
        dp[i] = max(dp[i - 1] + nums[i], nums[i]);
        if(result < dp[i])
            result = dp[i];
    }
    return result;
}

编辑距离

编辑距离

题目:给定两个单词word1和word2,计算出将word1转换成word2所使用的最少操作数。可以对一个单词进行如下三种操作:

  • 插入一个字符

  • 删除一个字符

  • 替换一个字符

示例1:

输入:word1 = "horse", word2 = "ros"

输出:3

示例2:

输入:word1 = "intention", word2 = "execution"

输出:5

解释:

intention -> inention

inention -> enention

enention -> exention

exention -> exection

exection -> execution

  • dp数组及下标含义

dp[i][j]表示将以下标i - 1结尾的word1转换为以下标j - 1结尾的word2最少需要的操作数

  • 递推公式

如果word1[i - 1] == word2[j - 1],不需要编辑,dp[i][j] = dp[i - 1][j - 1]

反之,需要编辑

  • 增删操作(因为word1增加一个元素,相当于把word2对应元素删除,所以放一起)

dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + 1

  • 替换操作

dp[i][j] = dp[i - 1][j - 1] + 1

综上,word1[i - 1] != word2[j - 1]时,dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1]))

  • 初始化

dp[0][j]和dp[i][0]都是要把其中一个字符串转换为空串的最少操作数,即字符串的长度

  • 遍历顺序

从小到大

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

回文子串

回文子串

题目:647. 回文子串 - 力扣(LeetCode)

  • dp数组及下标含义

dp[i][j]表示下标i到下标j的部分是否为回文子串

  • 递推公式

如果s[i] != s[j],dp[i][j] = false

如果s[i] == s[j]

  • j >= i + 2时,dp[i + 1][j - 1] == true,dp[i][j] = true

  • j < i + 2时,dp[i][j] = true

  • 初始化

全部初始化为false

  • 遍历顺序

i从大到小,j从小到大,j大于等于i

int countSubstrings(string s) {
    int result = 0;
    vector<vector<bool>> dp(s.length(), vector<bool>(s.length(), false);
    for(int i = s.length() - 1; i >= 0; i--){
        for(int j = i; j < s.length(); j++){
            if(s[i] == s[j]){
                if(j < i + 2){
                    dp[i][j] = true;
                    result++;
                }
                else if(dp[i + 1][j - 1] == true){
                        dp[i][j] = true;
                        result++;
                }
            }
        }
    }
    return result;
}

最长回文子序列

题目:516. 最长回文子序列 - 力扣(LeetCode)

  • dp数组及下标含义

dp[i][j]表示下标i - j部分含有的回文子序列的最大长度

  • 递推公式

如果s[i] == s[j],

如果j < i + 2,dp[i][j] = j - i + 1

反之,dp[i][j] = dp[i + 1][j - 1] + 2

反之,看左右两边哪个的最长回文子序列更长,dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])

  • 初始化

无需初始化

  • 遍历顺序

i从大到小,j从小到大,j大于等于i

int longestPalindromeSubseq(string s) {
    vector<vector<int>> dp(s.length(), vector<int>(s.length()));
    for(int i = s.length(); i >= 0; i--){
        for(int j = i; j < s.length(); 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 + 1][j], dp[i][j - 1]);
        }
    }
    return dp[0][s.length() - 1];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值