算法沉淀——动态规划之两个数组的 dp
- 01.最长公共子序列
- 02.不相交的线
- 03.不同的子序列
- 04.通配符匹配
01.最长公共子序列
题目链接:https://leetcode.cn/problems/longest-common-subsequence/
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000
text1
和text2
仅由小写英文字符组成。
思路
状态转移方程:
- 如果当前字符相同(
text1[i] == text2[j]
),说明我们找到了一个新的公共字符,因此最长公共子序列的长度在之前的基础上加 1:dp[i][j] = dp[i - 1][j - 1] + 1
- 如果当前字符不同(
text1[i] != text2[j]
),则我们需要考虑去掉text1[i]
或者去掉text2[j]
之后的最长公共子序列。这可以通过比较text1
的前i-1
个字符和text2
的前j
个字符的最长公共子序列长度,以及text1
的前i
个字符和text2
的前j-1
个字符的最长公共子序列长度,取最大值:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
初始化:为了方便处理边界情况,我们在文本 text1
和 text2
前各添加一个空字符,即 text1[0]
和 text2[0]
表示空串。这样,dp
表的大小为 (m+1) x (n+1)
,其中 m
和 n
分别是两个文本的长度。初始化时,将第一行和第一列的值都设置为 0。
填表顺序:最终的结果存储在 dp[m][n]
中,其中 m
和 n
是两个文本的长度。这个值表示 text1
和 text2
的最长公共子序列的长度。
代码
class Solution {
public:int longestCommonSubsequence(string text1, string text2) {int m=text1.size(),n=text2.size();text1=" "+text1,text2=" "+text2;vector<vector<int>> dp(m+1,vector<int>(n+1));for(int i=1;i<=m;i++)for(int j=1;j<=n;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[m][n];}
};
02.不相交的线
题目链接:https://leetcode.cn/problems/uncrossed-lines/
在两条独立的水平线上按给定的顺序写下 nums1
和 nums2
中的整数。
现在,可以绘制一些连接两个数字 nums1[i]
和 nums2[j]
的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
示例 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
示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2
提示:
1 <= nums1.length, nums2.length <= 500
1 <= nums1[i], nums2[j] <= 2000
思路
如果我们希望确保两条直线不相交,我们可以将问题转化为在两个整数数组中寻找最长的公共子序列。这是因为,如果两条直线不相交,它们在同一列上对应的两个元素在两个数组中的顺序关系应该保持一致。
这个问题可以用动态规划来解决,具体步骤如下:
- 状态表达:
- 我们选取两个数组分别表示两条直线,数组长度分别为
m
和n
。 - 定义状态表
dp
,其中dp[i][j]
表示数组1的前i
个元素和数组2的前j
个元素中的最长公共子序列的长度。
- 我们选取两个数组分别表示两条直线,数组长度分别为
- 状态转移方程:
- 如果
nums1[i] == nums2[j]
,说明找到了一个公共元素,状态转移方程为dp[i][j] = dp[i-1][j-1] + 1
。 - 如果
nums1[i] != nums2[j]
,则需要在两个数组的前面部分分别寻找最长公共子序列,状态转移方程为dp[i][j] = max(dp[i-1][j], dp[i][j-1])
。
- 如果
- 初始化:
- 为了方便处理边界情况,可以在两个数组的前面各添加一个虚拟元素,表示空串。
- 初始化
dp
数组的第一行和第一列为0,因为空串与任何数组的最长公共子序列长度都为0。
- 填表顺序:
- 从上到下,从左到右填写
dp
数组。
- 从上到下,从左到右填写
- 返回值:
- 最终的结果存储在
dp[m][n]
中,表示两个数组的最长公共子序列的长度。
- 最终的结果存储在
代码
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));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];}
};
03.不同的子序列
题目链接:https://leetcode.cn/problems/distinct-subsequences/
你两个字符串 s
和 t
,统计并返回在 s
的 子序列 中 t
出现的个数,结果需要对 109 + 7 取模。
示例 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
提示:
1 <= s.length, t.length <= 1000
s
和t
由英文字母组成
思路
-
状态表达:
- 选取第一个字符串
[0, i]
区间以及第二个字符串[0, j]
区间作为研究对象,结合题目要求来定义状态表达。 - 在这道题中,状态表达为
dp[i][j]
,表示在字符串s
的[0, j]
区间内的所有子序列中,有多少个t
字符串[0, i]
区间内的子串。
- 选取第一个字符串
-
状态转移方程:
-
根据最后一个位置的元素,结合题目要求进行分类讨论:
-
当
t[i] == s[j]
时:
- 选择
s[j]
作为结尾,相当于在状态dp[i-1][j-1]
中的所有符合要求的子序列的后面,再加上一个字符s[j]
,此时dp[i][j] = dp[i-1][j-1]
。 - 不选择
s[j]
作为结尾,相当于选择了状态dp[i][j-1]
中所有符合要求的子序列,继承了上个状态里面的求得的子序列,此时dp[i][j] = dp[i][j-1]
。
- 选择
-
当
t[i] != s[j]
时:
- 子序列只能从
dp[i][j-1]
中选择所有符合要求的子序列,继承上个状态里面求得的子序列,此时dp[i][j] = dp[i][j-1]
。
- 子序列只能从
-
-
综上,状态转移方程为:
- 所有情况下都可以继承上一次的结果:
dp[i][j] = dp[i][j-1]
。 - 当
t[i] == s[j]
时,可以多选择一种情况:dp[i][j] += dp[i-1][j-1]
。
- 所有情况下都可以继承上一次的结果:
-
-
初始化:
- 引入空串后,方便初始化,注意下标的映射关系以及确保后续填表是正确的。
- 当
s
为空时,t
的子串中有一个空串和它一样,因此初始化第一行全部为1
。
-
填表顺序:
- 从上往下填每一行,每一行从左往右。
-
返回值:
- 根据状态表达,返回
dp[m][n]
的值。
- 根据状态表达,返回
代码
class Solution {
public:int numDistinct(string s, string t) {int m=t.size(),n=s.size();vector<vector<double>> dp(m+1,vector<double>(n+1));for(int j=0;j<=n;j++) dp[0][j]=1;for(int i=1;i<=m;i++)for(int j=1;j<=n;j++){dp[i][j]+=dp[i][j-1];if(t[i-1]==s[j-1]) dp[i][j]+=dp[i-1][j-1];}return dp[m][n];}
};
04.通配符匹配
题目链接:https://leetcode.cn/problems/wildcard-matching/
给你一个输入字符串 (s
) 和一个字符模式 (p
) ,请你实现一个支持 '?'
和 '*'
匹配规则的通配符匹配:
'?'
可以匹配任何单个字符。'*'
可以匹配任意字符序列(包括空字符序列)。
判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。
示例 1:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "*"
输出:true
解释:'*' 可以匹配任意字符串。
示例 3:
输入:s = "cb", p = "?a"
输出:false
解释:'?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。
提示:
0 <= s.length, p.length <= 2000
s
仅由小写英文字母组成p
仅由小写英文字母、'?'
或'*'
组成
思路
在处理两个字符串之间的动态规划问题时,通常按照以下步骤进行:
-
状态表达:
- 选取第一个字符串
[0, i]
区间以及第二个字符串[0, j]
区间作为研究对象,结合题目的要求定义状态表达。 - 在这道题中,我们定义状态表达为
dp[i][j]
,表示字符串p
的[0, j]
区间内的子串能否匹配字符串s
的[0, i]
区间内的子串。
- 选取第一个字符串
-
状态转移方程:
- 根据最后一个位置的元素,结合题目要求,进行分类讨论:
- 当
s[i] == p[j]
或p[j] == '?'
时,两个字符串匹配上了当前的一个字符,只能从dp[i-1][j-1]
中看当前字符前面的两个子串是否匹配,继承上个状态中的匹配结果,dp[i][j] = dp[i][j-1]
。 - 当
p[j] == '*'
时,匹配策略有两种选择:- 选择
'*'
匹配空字符串,相当于它匹配了一个寂寞,直接继承状态dp[i][j-1]
,dp[i][j] = dp[i][j-1]
。 - 选择
'*'
向前匹配1 ~ n
个字符,直至匹配整个s
串。相当于从dp[k][j-1]
(0 <= k <= i) 中所有匹配情况中,选择性继承可以成功的情况,dp[i][j] = dp[k][j-1]
(0 <= k <= i)。
- 选择
- 当
p[j]
不是特殊字符且不与s[i]
相等时,无法匹配。综上,状态转移方程为:- 当
s[i] == p[j]
或p[j] == '?'
时:dp[i][j] = dp[i-1][j-1]
。 - 当
p[j] == '*'
时,状态转移方程优化为:dp[i][j] = dp[i-1][j] || dp[i][j-1]
。
- 当
- 当
- 根据最后一个位置的元素,结合题目要求,进行分类讨论:
-
初始化:
dp
数组的值表示是否匹配,初始化整个数组为false
。- 由于需要用到前一行和前一列的状态,初始化第一行和第一列。
dp[0][0]
表示两个空串是否匹配,初始化为true
。- 第一行表示
s
为空串,p
串与空串只有一种匹配可能,即p
串表示为"***"
,此时也相当于空串匹配上空串,将所有前导为"*"
的p
子串与空串的dp
值设为true
。 - 第一列表示
p
为空串,不可能匹配上s
串,跟随数组初始化即可。
-
填表顺序:
- 从上往下填每一行,每一行从左往右。
-
返回值:
- 根据状态表达,返回
dp[m][n]
的值。
- 根据状态表达,返回
代码
class Solution {
public:bool isMatch(string s, string p) {int m=s.size(),n=p.size();s=" "+s,p=" "+p;vector<vector<bool>> dp(m+1,vector<bool>(n+1));dp[0][0]=true;for(int i=1;i<=n;i++)if(p[i]=='*') dp[0][i]=true;else break;for(int i=1;i<=m;i++)for(int j=1;j<=n;j++){if(p[j]=='*') dp[i][j]=dp[i][j-1]||dp[i-1][j];else dp[i][j]=(p[j]=='?'||s[i]==p[j])&&dp[i-1][j-1];}return dp[m][n];}
};