跳转汇总链接
4.1 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列
是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。 两个字符串的 公共子序列
是这两个字符串所共同拥有的子序列。
- 状态表示
- dp[i][j] 表示,text1 的 [0, i] 区间和 text2 的 [0, j] 区间中,的最长公共子序列的长度
- 状态转移方程
-
分析 i 和 j 位置,如果字符相等,那么其区间中的最长公共子序列一定可以以这个字符为结尾;如果不等,再分情况讨论
dp[i][j] 的值有如下情况需要考虑: text1[i] == text2[j], dp[i-1][j-1] + 1; text1[i] != text2[j], 对如下值取 max: dp[i-1][j] dp[i][j-1] dp[i-1][j-1] // 实际上这个情况已经被头两种包含了,可以不用写
-
- 初始化
- 需要考虑 i-1、j-1 的越界情况,我们可以对空串进行设置,可以有效的防止越界
- 这里在二维数组的前面多加一行和一列,分别表示两个数组为空串时候的 dp 值
- 注意访问原数组时的对齐
- 将空串也作为公共子序列,把多加的一行和一列都初始化为 0
- 填表顺序
- 从上往下填写每一行,每行从左往右填写
- 返回值
- dp[text1.size()][text2.size()],表的最右下角的值。
🐎代码如下:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n1 = text1.size() + 1;
int n2 = text2.size() + 1;
text1 = "-" + text1;
text2 = "-" + text2;
vector<vector<int>> dp(n1, vector<int>(n2));
for(int i = 1; i < n1; i++)
for(int j = 1; j < n2; 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[n1-1][n2-1];
}
};
4.2 不相交的线
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直
线需要同时满足满足:
nums1[i] == nums2[j] 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。以这种方法绘制线条,并返回可以绘制的最大连线数。
状态表示
- dp[i][j] 表示,text1 的 [0, i] 区间和 text2 的 [0, j] 区间中,的最大连线数
略…(这一题和 4.1 根本是一样的,不交叉的连线不就是公共子序列嘛)
🐎代码如下:
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int n1 = nums1.size() + 1;
int n2 = nums2.size() + 1;
vector<vector<int>> dp(n1, vector<int>(n2));
for(int i = 1; i < n1; i++)
for(int j = 1; j < n2; 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[n1-1][n2-1];
}
};
4.3 不同的子序列(hard)
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。
这一题 hard 在状态表示…
- 状态表示
- dp[i][j] 表示,s 的 [0, i] 区间中有多少个子序列,满足和 t 的 [0, j] 区间的子串相同
- 状态转移方程
-
分析区间内的 s 和 t 串,这里如果 s[i] 算作子序列的结尾 dp 是一种情况,如果 s[i] 不算作子序列的结尾 dp 又会是另一种情况,而最终这里的 dp 值应该是这两种情况相加。
dp[i][j] 的值有如下情况需要考虑: s[i] 算作结尾,而且 s[i] == t[j], dp[i-1][j-1] s[i] 不算作结尾, dp[i-1][j] 条件满足的情况下:dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
-
- 初始化
- 需要考虑 i-1、j-1 的越界情况,我们可以对空串进行设置,可以有效的防止越界
- 这里在二维数组的前面多加一行和一列,分别表示两个数组为空串时候的 dp 值
- 注意访问原数组时的对齐
- 将空串也作为公共子序列,dp[i][0] 位置都置 1,意思是 t 为空串的时候 s 最少也包含这一个空串
- 在 for 循环的时候初始化即可
- 填表顺序
- 从上往下填写每一行,每行从左往右填写
- 返回值
- dp[s.size()][t.size()],表的最右下角的值。
🐎代码如下:
class Solution {
public:
int numDistinct(string s, string t) {
int n1 = s.size() + 1;
int n2 = t.size() + 1;
vector<vector<double>> dp(n1, vector<double>(n2));
dp[0][0] = 1;
for(int i = 1; i < n1; i++)
{
dp[i][0] = 1;
for(int j = 1; j < n2; j++)
dp[i][j] = (s[i-1] == t[j-1]) ?
dp[i-1][j] + dp[i-1][j-1] :
dp[i-1][j];
}
return dp[n1-1][n2-1];
}
};
4.4 通配符匹配(hard)
给你一个输入字符串 (s) 和一个字符模式 (p) ,请你实现一个支持 ‘?’ 和 ‘*’ 匹配规则的通配符匹配:
- ‘?’ 可以匹配任何单个字符。
- ‘*’ 可以匹配任意字符序列(包括空字符序列)。
判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。
有了 4.3 的基础,状态表示就异曲同工,这题的关键点就是多种情况的考虑
- 状态表示
- dp[i][j] 表示,s 的 [0, i] 区间的子串是否能被 p 的 [0, j] 区间的子串匹配
- 状态转移方程
-
分析区间内的 s 和 p 串是否匹配,和 p 串最后一个位置的存放内容有关
dp[i][j] 的值有如下情况需要考虑: p[j] 是普通字符, p[j] == s[i] && dp[i-1][j-1] == true, true p[j] == '?', dp[i-1][j-1] == true, true p[i] == '*', 匹配空串, dp[i][j-1] == true, true 匹配一个字符, dp[i-1][j-1] == true, true 匹配两个字符, dp[i-2][j-1] == true, true ... 有一个为 true 即为 true
-
- 按照上述为 ‘*’ 通配符进行匹配的方式需要一个循环遍历,这样的整时间复杂度是很大的(n3),所以我们需要想办法优化
- 优化
-
针对 ‘*’ 号匹配符, 我们已知能推出的式子是这样的
在 p[j] == '*' 的前提条件下, ① dp[i][j] = dp[i][j-1] || dp[i-1][j-1] || dp[i-2][j-1] || ...
-
此时我们将 i 降一级,j 位置不变,所以仍是满足前提条件的,公式就可以写成如下:
② dp[i-1][j] = dp[i-1][j-1] || dp[i-2][j-1] || ...
-
将公式 ① 中的 dp[i][j-1] || dp[i-1][j-1] || dp[i-2][j-1] || …,由公式 ② 中的 dp[i-1][j] 代替,可以得出化简后的表达式
dp[i][j] = dp[i][j-1] || dp[i-1][j]
- 初始化
- 按照惯例,这里涉及到空串,为了方便我们填表,在表头补充一行和一列
- dp[0][0] 代表空串匹配空串,初始化为 true
- dp[0][x] 代表 s 为空串而 p 不为空串,考虑到 p 如果以 ‘’ 开头 也是可以匹配的,这里就需要判断,当 p 从开始是 '’ 时为 true,遇到非 ‘*’ 的字符后,都是 false
- dp[x][0] 代表 p 为空,全填 false
- 填表顺序
- 从上往下填写每一行,每行从左往右填写
- 返回值
- dp[s.size()][p.size()],表的最右下角的值。
class Solution {
public:
bool isMatch(string s, string p) {
int n1 = s.size() + 1;
int n2 = p.size() + 1;
s = '-' + s;
p = '-' + p;
vector<vector<bool>> dp(n1, vector<bool>(n2));
// 初始化
dp[0][0] = true;
for(int j = 1; j < n2; j++)
if(p[j] == '*')
dp[0][j] = true;
else break;
for(int i = 1; i < n1; i++)
for(int j = 1; j < n2; j++)
{
if((s[i] == p[j] || p[j] == '?') && dp[i-1][j-1] == true)
dp[i][j] = true;
if(p[j] == '*')
dp[i][j] = dp[i][j-1] || dp[i-1][j];
}
return dp[n1-1][n2-1];
}
};
4.5 正则表达式匹配(hard)
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配
- ‘.’ 匹配任意单个字符
- ‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
其中,保证每次出现字符 * 时,前面都匹配到有效的字符
-
状态表示
- dp[i][j] 表示,s 的 [0, i] 区间的子串是否能被 p 的 [0, j] 区间的子串匹配
-
状态转移方程
-
分析区间内的 s 和 p 串是否匹配,和 p 串最后一个位置的存放内容有关
dp[i][j] 的值有如下情况需要考虑: p[j] 是普通字符, p[j] == s[i] && dp[i-1][j-1] == true, true p[j] == '.', dp[i-1][j-1] == true, true p[j] == '*' 分如下情况: p[j-1] == 普通字符, 匹配空串, dp[i][j-2] p[j-1] == '.' , 匹配空串, dp[i][j-2] 匹配不确定个字符, dp[i-1][j] p[j-1] == s[i], , 匹配空串, dp[i][j-2] 匹配不确定个字符, dp[i-1][j]
-
-
初始化
- 按照惯例,这里涉及到空串,为了方便我们填表,在表头补充一行和一列
- dp[0][0] 代表空串匹配空串,初始化为 true
- dp[0][x] 代表 s 为空串而 p 不为空串,考虑到 p 如果以 “x*” 开头,并且只要偶数位置的 ‘x’ 连续出现就是可以匹配空串的,这些位置上设 true
- dp[x][0] 代表 p 为空,全填 false
-
填表顺序
- 从上往下填写每一行,每行从左往右填写
-
返回值
- dp[s.size()][p.size()],表的最右下角的值。
class Solution {
public:
bool isMatch(string s, string p) {
s = '-' + s;
p = '-' + p;
int n1 = s.size();
int n2 = p.size();
vector<vector<bool>> dp(n1, vector<bool>(n2));
// 初始化
dp[0][0] = true;
for(int j = 2; j < n2; j+=2)
if(p[j] == '*')
dp[0][j] = true;
else
break;
for(int i = 1; i < n1; i++)
for(int j = 1; j < n2; j++)
if(p[j] == '*')
dp[i][j] = dp[i][j-2] // 匹配空串
|| dp[i-1][j]
&& (p[j-1] == '.' || p[j-1] == s[i]); // 当 j-1 位置上是这两种情况时,匹配不确定个字符
// ps:&& 的优先级更高哦
else
dp[i][j] = (s[i] == p[j] || p[j] == '.') && dp[i-1][j-1];
return dp[n1-1][n2-1];
}
};
🥰如果本文对你有些帮助,欢迎👉 点赞 收藏 关注,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 若有差错恳请留言指正~~