动态规划系列:3.子序列问题

注意:

  1. 是连续的还是不连续的

  1. 是二维dp还是一维dp求解

  1. dp数组的含义

1.最长递增子序列

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

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

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

分析:

  1. 最优解问题

  1. 最长递增子序列的末尾元素为x,以x元素结尾最长的子序列长度为f(x),则有:

f(x)=max{f(i)+1,if i>x, else f(i)},i从第0个到第i-1个元素

  1. 其长度为f(x),则有f(x)=f(x-1)+1,能分解为子问题

  1. 同一长度的不同子序列可能共用一段子序列,子问题间有重复

因此可尝试动态规划求解

代码:

class Solution {
public:
    int lengthOfLIS(vector& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return 1;
        vector record(nums.size(),1);//记录以第i个元素结尾的最长子序列的长度,初始值都为1
        //从第二个元素开始对每个元素遍历
        int result = 0;
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    record[i] = max(record[i], record[j] + 1);
                }
            }
            if (record[i] > result) result = record[i];
        }
        return result;
    }
};

2.最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 

示例 2:

输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

分析:

和上一题“连续递增子序列”相比,此题增加了“连续的”条件,基本代码差不多,还少一个循环

f(x)=max{f(i)+1,if i>x, else f(i)},i为x前一个元素

class Solution {
public:
    int findLengthOfLCIS(vector& nums) {
        if (nums.size() <= 1)return nums.size();
        //记录
        vector record(nums.size(), 1);
        int result = 0;
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] > nums[i - 1]) {
                record[i] = record[i - 1] + 1;
            }
            if (record[i] > result)result = record[i];
        }
        return result;
    }
};

3.最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

分析:

设i、j分别表示字符串1和2的第i个、第j个元素在字符串里的位置,f(i,j)表示text[1...i]与text[i...j]的最长公共子序列

如果nums[i]==nums[j],则f(i,j)=f(i-1,j-1)+1

否则,f(i,j)=max{f(i,j-1),f(i-1,j)}

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        if (text1.size() == 0 || text2.size() == 0)return 0;
        //记录
        vector> dp(text1.size(), vector(text2.size(), 0));
        //边界处理
        dp[0][0] = text1[0] == text2[0] ? 1 : 0;
        for (int j = 1; j < text2.size(); j++) {
            dp[0][j] = text1[0] == text2[j] ? 1 : dp[0][j - 1];
        }
        for (int i = 1; i < text1.size(); i++) {
            dp[i][0] = text1[i] == text2[0] ? 1 : dp[i-1][0];
        }
        if (text1.size() == 1 || text2.size() == 1) return dp[text1.size()-1][text2.size()-1];
        int result = 0;
        for (int i = 1; i < text1.size(); i++) {
            for (int j = 1; j < text2.size(); j++) {
                if (text1[i] == text2[j]) {
                    dp[i][j] = dp[i - 1][j - 1]+1;
                }
                else {
                    dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
                }
                result = dp[i][j] > result ? dp[i][j] : result;
            }
        }
        return result;
    }
};

边界处理挺费脑子

在上面的代码中,从0开始记录字符序号,这样就会导致计算递推时`dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])`;数组越界

可以从1开始记录字符序号,`dp[0][j]和dp[i]0`记为0,可以这样想:两个字符串最前面还隐藏了一个东西,这个东西与其他任何字符都不相同

代码:

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

4.最长重复子数组

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

示例 1:

输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

示例 2:

输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5

分析:

与前面的“最长公共子序列”相似,与上一题的区别在于此题中子数组的元素在原数组中是连续的

在具体算法中dp数组的含义不一样,上一题dp[i][j]表示text1:1-i与text2:1-j的最长公共子序列长度

这一题dp[i][j]表示以text1[i]与text2[j]结尾的最长公共子数组的长度

二维dp

dp[i][j]表示结尾元素分别为nums1[i-1]和nums2[j-1]的最长公共子数组长度

如果nums1[i-1]!=nums2[j-1]: dp[i][j]=0

如果nums1[i-1]==nums2[j-1]:dp[i][j]=dp[i-1][j-1]+1

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

5.不相交的线

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:

  • nums1[i] == nums2[j]

  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:

动态规划系列:3.子序列问题_第1张图片
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:

输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

分析

此题实际就是求两个数组的最长公共子序列

dp[i][j]表示两个数组中1-i,1-j的部分元素的最长公共子序列

代码

class Solution {
public:
    int maxUncrossedLines(vector& nums1, vector& nums2) {
        if(nums1.size()==0||nums2.size()==0) return 0;
        //记录
        vector> dp(nums1.size()+1,vector(nums2.size()+1,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;
                else dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
            }
        }
        return dp[nums1.size()][nums2.size()];
    }
};

6.最大子序和

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

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

分析:

最优解问题

设dp[i]表示以nums[i]结尾的拥有最大和的子数组的最大和值

如果dp[i-1]>0,dp[i]=dp[i-1]+nums[i]

如果dp[i-1]<=0,dp[i]=nums[i]

class Solution {
public:
    int maxSubArray(vector& nums) {
        if(nums.size()==0)return 0;
        if(nums.size()==1) return nums[0];
        int *dp=new int[nums.size()];
        dp[0]=nums[0];
        int result=0;
        for(int i=1;iresult?dp[i]:result;
        }
        return result;
    }
};

7.判断子序列

给定字符串 st ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:

输入:s = "abc", t = "ahbgdc"
输出:true

示例 2:

输入:s = "axc", t = "ahbgdc"
输出:false

分析:

感觉用不上动态规划,也看不出动态规划的几个特性。如示例一,依次在t中找a\b\c就好了

代码:

class Solution {
public:
	bool isSubsequence(string s, string t) {
		if (s.size() > t.size()) return false;
		int pos = 0;//指向t
		for (int i = 0; i < s.size(); i++) {
			if (s[i] == t[pos]) {
				pos++;
				continue;
			}
			while (t[pos] != s[i]) {
				pos++;
				if (pos >= t.size()) return false;
			}
			pos++;
		}
		return true;
	}
};

这道题还可以用双指针的思想来解:

代码

class Solution {
public:
	bool isSubsequence(string s, string t) {
		if (s.size() > t.size()) return false;
		int i=0,j=0;
        while(i

动态规划:

联想到前面做过的“最长公共子序列”,该题要求两个字符串的最长公共子序列,可以看作本题的“复杂版”,即本题求s与t的最长公共子序列,如果结果值不等于s的长度,则输出false,否则输出true

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

递归关系:

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

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

代码和“最长公共子序列”几乎差不多

class Solution {
public:
	bool isSubsequence(string s, string t) {
        if(s.size()>t.size()) retur false;
        if(s.size()==0) return true;
		vector> dp(s.size()+1,vector(t.size()+1,0));
        for(int i=1;i<=s.size();i++){
            for(int j=1;j<=t.size();j++){
                if(s[i-1]==s[j-1]) dp[i][j]=dp[i-1][j-1]+1;
                else dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
            }
        }
        if(dp[s.size()][t.size()]!=s.size()) return true;
        return false;
	}
};

8.不同的子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit

示例 2:

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbag
babgbag
babgbag
babgbag
babgbag

分析

i表示s的第i个元素,j表示t的第j个元素,dp[i][j]表示在s:1-i中出现t:1-j的次数

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

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

注意dp[0][]和dp[][0]的初始化

代码如下:

class Solution {
public:
	int numDistinct(string s, string t) {
		vector> dp(s.size() + 1, vector(t.size() + 1, 0));
		for (int k = 0; k <= s.size(); k++) dp[k][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()];
	}
};

遇到问题

刚开始定义的二维vector元素是int型,表示数据范围小了,然后改成long int还是小了,改成long long int还是小,最后改成unsigned long long才行

9.两个字符串的删除操作

给定两个单词 word1 和 word2 ,返回使得 word1 和 word2相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

示例 1:

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

示例 2:

输入:word1 = "leetcode", word2 = "etco"
输出:4

分析

感觉这题也是最大公共子序列的扩展

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

10.编辑距离

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符

  • 删除一个字符

  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

分析

设dp[i][j]表示将word1:0----i-1成功编辑到word2:0----j-1所需的步数

如果word1[i-1]==word2[j-1],则有dp[i][j]=dp[i-1][j-1]

如果word1[i-1]!=word2[j-1],则需要进行编辑:

  1. 如果word1:0----i-1比word2:0------j-1少一个元素,如 w1:a,w2:ab dp[1][2]=dp[1][1]+1

则需要给word1末尾添加一个元素,因此,dp[i][j]=dp[i][j-1]+1

  1. 如果word1:0----i-1比word2:0------j-1多一个元素,如 w1:abc,w2:ab dp[3][2]=dp[2][2]+1

则需要删除word1末尾的元素,因此,dp[i][j]=dp[i-1][j]+1

  1. 如果word1:0----i-1比word2:0------j-1元素一样多但最后一个元素不相同·,如 w1:abc,w2:abd dp[3][3]=dp[2][2]+1

则需要将word1末尾的元素进行替换,因此,dp[i][j]=dp[i-1][j-1]+1

总结:

当word1[i-1]==word2[j-1],则有dp[i][j]=dp[i-1][j-1]:

当word1[i-1]!=word2[j-1]时:

代码

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

上面的代码提交不通过,原因在于算法思路不对,当word1[i]!=word2[j]是我们取的是“或者”,实际应该是三个操作取最小值

如horse编辑成ros,可以这样:

	1. horse先编辑成ro,然后添加s,即dp(5,3)=dp(5,2)+1;
	2. hors编辑成ro,然后e替换成s,即dp(5,3)=dp(4,2)+1;
	3. hors编辑成ros,然后删除e,即dp(5,3)=dp(4,3)+1;

正确代码

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

11.回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

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

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

分析

dp(i,j)表示第i个元素到第j个元素间的子串是否是回文串

遍历顺序

dp(i,j)的值依赖于dp(i+1,j-1),因此i要从大往小遍历,j从小往大遍历

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

优化:

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

12.最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

分析

在上一题“回文子串中,我们要求一个字符串中回文子串的数目,回文子串是连续的一串字符。在这题中,与上一题的区别在于:回文子序列不一定是连续的,要求最长的

递推关系:

dp(i,j)表示s:i-j最长回文子序列的长度,有:

如果s[i]=s[j], dp(i,j)=dp(i+1,j-1)+2

如果s[i]!=s[j],dp(i,j)=max{dp(i+1,j),dp(i,j-1)}

遍历顺序:

dp(i,j)的求解依赖于:dp(i+1,j-1),dp(i+1,j),dp(i,j-1)

因此i由大往小遍历,j由小往大遍历

代码

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

你可能感兴趣的:(c++,动态规划,算法)