目录
1、最长公共子序列
1.1 算法原理
1.2 算法代码
2、不相交的线
2.1 算法原理
2.2 算法代码
3、不同的子序列
3.1 算法原理
3.2 算法代码
4、通配符匹配(hard ★★★)
4.1 算法原理
4.2 算法代码
5、正则表达式匹配(hard ★★★)
5.1 算法原理
5.2 算法代码
6、交错字符串
6.1 算法原理
6.2 算法代码
7、两个字符串的最小ASCII删除和
7.1 算法原理
7.2 算法代码
8、最长重复子数组【子数组问题】
8.1 算法原理
8.2 算法代码
1、最长公共子序列
. - 力扣(LeetCode)
1.1 算法原理
- 状态表示:
dp[i][j]:s1[0, i]区间以及s2[0, j]区间内的所有子序列中,最长公共子序列的长度
- 状态转移方程:
s[i] == s[j]:
dp[i][j] + 1
s[i] != s[j]:
max(dp[i-1][j],dp[i][j-1]);
- 初始化:
字符串存在空串情况,引入空串(多开辟一行,多开辟一列)
1. 方便初始化
2. 处理空串
- 建表顺序:
从上往下,从左往右
- 返回值:
dp[m][n]
1.2 算法代码
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
// 初始化
int[][] dp = new int[m + 1][n + 1];
// 处理下标映射关系
text1 = " " + text1;
text2 = " " + text2;
// 填表
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
if(text1.charAt(i) == text2.charAt(j)) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m][n];
}
}
2、不相交的线
. - 力扣(LeetCode)
2.1 算法原理
经分析,将两个数组最长的公共子序列相连,即可绘制的最大连线数。故,求最长公共子序列的长度即可。
- 状态表示:
dp[i][j]:n1的[0, i]区间内以及n2的[0, j]区间内的所有子序列中,最长公共子序列的长度
- 状态转移方程:
n1[i] == n2[j] --> dp[i][j] = dp[i-1][j-1] + 1
n1[i] != n2[j] --> dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- 初始化:
多开辟一行,多开辟一列
- 建表顺序:
从上往下,从左往右
- 返回值:
dp[m][n]
2.2 算法代码
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
// 即:最长公共子序列问题
int m = nums1.length, n = nums2.length;
int[][] dp = new int[m + 1][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] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m][n];
}
}
3、不同的子序列
. - 力扣(LeetCode)
3.1 算法原理
- 状态表示:
dp[i][j]:s[0, i]区间的所有子序列中,有多少个t[0, j]区间内的子串
- 状态转移方程:
dp[i][j] = dp[i-1][j-1](s[j] == t[i]时) + dp[i][j-1]
- 初始化:
上面多开辟一行,左边多开辟一列:
1. 引入空串
2. 里面的值要保证后续的填表是正确的(第一行代表t为空串,1;第一列代表s为空串,0)
3. 下标的映射关系
- 建表顺序:
从上往下每一行,每一行从左往右
- 返回值:
dp[m][n]
3.2 算法代码
class Solution {
public int numDistinct(String s, String t) {
int m = t.length();
int n = s.length();
// 处理下标映射关系
s = " " + s;
t = " " + t;
int[][] dp = new int[m + 1][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(s.charAt(j) == t.charAt(i)) dp[i][j] += dp[i - 1][j - 1];
}
}
return dp[m][n];
}
}
4、通配符匹配(hard ★★★)
. - 力扣(LeetCode)
4.1 算法原理
- 状态表示:
dp[i][j]:s的[0, i]区间内的子串,能否被p[0 , j]区间内的子串所匹配
- 状态转移方程:
1. p[j]是普通字符 --> (p[j]==s[i] && dp[i-1][j-1])==true --> true
2. p[j]是 '?' --> dp[i-1][j-1]
3. p[j]是 '*' --> 替换n个字符 --> dp[i-n][j-1]
故,p[j] == '*' --> dp[i][j]=dp[i][j-1] || dp[i-1][j-1] || dp[i-2][j-1] || ...
- 优化(当p[j] == '*'时):
dp[i][j]=dp[i][j-1] || dp[i-1][j]
- 建表顺序:
从上往下每一行,
从左往右每一列
- 返回值:
dp[m][n];
其中,m为s串的长度,n为p串的长度。
4.2 算法代码
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
// 初始化
dp[0][0] = true;
boolean check = false;
for(int j = 0; j < n; j++)
if(p.charAt(j) == '*') dp[0][j + 1] = true;
else break;
// 处理下标映射关系
s = " " + s;
p = " " + p;
// 填表
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
char ch = p.charAt(j);
if(ch != '*' && ch != '?') {
if(ch == s.charAt(i))
dp[i][j] = dp[i - 1][j - 1];
}else if(ch == '?'){
dp[i][j] = dp[i - 1][j - 1];
}else {
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
}
}
}
return dp[m][n];
}
}
5、正则表达式匹配(hard ★★★)
. - 力扣(LeetCode)
5.1 算法原理
- 状态表示:
dp[i][j]:s的[0, i]区间,是否可以用p的[0, j]区间匹配
- 状态转移方程:
1. p[j]是'a'~'z' --> dp[i][j] = p[j]==s[i] && dp[i-1][j-1]
2. p[j]是'.' --> dp[i][j] = dp[i-1][j-1]
3. p[j]是'*'
3.1 p[j - 1]是'.':
空串 --> dp[i][j] = dp[i][j-2]
一个字符 --> dp[i][j] = dp[i-1][j-2]
两个字符 --> dp[i][j] = dp[i-2][j-2]
三个字符 --> dp[i][j] = dp[i-3][j-2]
....
(时间复杂度会飙到O(N^3)...优化如下)
- 优化方式1(当p[j] == '*' && p[j-1] == '.' 时):
dp[i][j] = dp[i][j-2] || dp[i-1][j-2] || dp[i-2][j-2] || dp[i-3][j-2] || ...
dp[i-1][j] = dp[i-1][j-2] || dp[i-2][j-2] || dp[i-3][j-2] || dp[i-4][j-2] || ...
所以:dp[i][j] = dp[i][j-2] || dp[i-1][j]
- 优化方式2(当p[j] == '*' && p[j-1] == '.'时)优化状态转移方程:
'.*'匹配s的最后一个字符后,不丢弃 --> dp[i-1][j]
'.*'不用匹配了,丢弃(空串) --> dp[i][j-2]
所以:dp[i][j] = dp[i][j-2] || dp[i-1][j]
3.1 p[j - 1]是'a' ~ 'z':
dp[i][j] = dp[i][j - 2] || (p[j - 1] == s[i] && dp[i - 1][j])
- 建表顺序:
从上往下每一行,
从左到右每一列
- 返回值:
dp[m][n]
5.2 算法代码
class Solution {
public boolean isMatch(String ss, String pp) {
int m = ss.length();
int n = pp.length();
// dp[i][j]:s的[0, i]区间,是否可以用p的[0, j]区间匹配
boolean[][] dp = new boolean[m + 1][n + 1];
// 处理下标映射关系
ss = " " + ss; pp = " " + pp;
char[] s = ss.toCharArray();
char[] p = pp.toCharArray();
// 初始化
dp[0][0] = true;
for(int j = 1; j + 1 <= n; j += 2) {
if(p[j + 1] == '*') dp[0][j + 1] = true;
else break;
}
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
if(p[j] == s[i] || p[j] == '.') {// p[j]是'a'~'z'或者p[j]是'.'的情况下
dp[i][j] = dp[i - 1][j - 1] ? true : false;
}else if(p[j] == '*') {// p[j]是'*' --> 依赖p[j - 1]的值
if(p[j - 1] == '.') dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
else dp[i][j] = dp[i][j - 2] || (p[j - 1] == s[i] && dp[i - 1][j]);
}
}
}
return dp[m][n];
}
}
6、交错字符串
. - 力扣(LeetCode)
6.1 算法原理
- 预处理:
s1 = " "+s1; s2 = " "+s2; s3 = " "+s3;
- 状态表示dp[i][j]:
s1[1, i]区间内的子串,s2[1, j]区间内的子串,能否交错拼接成s3[1, i+j]内的子串
- 状态转移方程:
dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j]) || (s2[j] == s3[i + j] && dp[i][j - 1])
- 建表顺序:
从上往下每一行,
从左往右每一列
- 返回值:
dp[m][n]
6.2 算法代码
class Solution {
public boolean isInterleave(String ss1, String ss2, String ss3) {
int m = ss1.length();
int n = ss2.length();
if(m + n != ss3.length()) return false;
boolean[][] dp = new boolean[m + 1][n + 1];
ss1 = " " + ss1; ss2 = " " + ss2; ss3 = " " + ss3;
char[] s1 = ss1.toCharArray();
char[] s2 = ss2.toCharArray();
char[] s3 = ss3.toCharArray();
// 初始化第一个位置
dp[0][0] = true;
// 初始化第一列
for(int i = 1; i <= m; i++)
if(s1[i] == s3[i]) dp[i][0] = true;
else break;
// 初始化第一行
for(int j = 1; j <= n; j++)
if(s2[j] == s3[j]) dp[0][j] = true;
else break;
// 填表
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
if(s1[i] == s3[i + j] && dp[i - 1][j]) dp[i][j] = true;
else if(s2[j] == s3[i + j] && dp[i][j - 1]) dp[i][j] = true;
}
}
return dp[m][n];
}
}
7、两个字符串的最小ASCII删除和
. - 力扣(LeetCode)
7.1 算法原理
- 问题转换:(正难则反)
两个字符串的最小ASCII删除和 --> 公共子序列ASCII的最大和
- 状态表示:
dp[i][j]:s1[0, i]以及s2[0, j]的所有子序列中,公共子序列的ASCII最大和
包含s1[i],包含s2[j] --> s1[i]==s2[j] --> dp[i-1][j-1]+s[i / j]
包含s1[i],不包含s2[j] !--> dp[i][j-1]
不包含s1[i],包含s2[j] !--> dp[i-1][j]
不包含s1[i],不包含s2[j] --> dp[i-1][j-1]
- 建表顺序:
从上往下每一行,
从左往右每一列
- 返回值:
dp[m][n]
7.2 算法代码
class Solution {
public int minimumDeleteSum(String ss1, String ss2) {
int m = ss1.length(), n = ss2.length();
// dp[i][j]:s1[0, i]以及s2[0, j]的所有子序列中,公共子序列的ASCII最大和
int[][] dp = new int[m + 1][n + 1];
// 处理下标映射关系
ss1 = " " + ss1; ss2 = " " + ss2;
char[] s1 = ss1.toCharArray();
char[] s2 = ss2.toCharArray();
// 记录总和
int sum = 0;
for(int j = 1; j <= n; j++) sum += s2[j];
// 填表
for(int i = 1; i <= m; i++) {
sum += s1[i];
for(int j = 1; j <= n; j++) {
if(s1[i] == s2[j]) dp[i][j] = dp[i - 1][j - 1] + s1[i];
dp[i][j] = Math.max(dp[i][j], Math.max(dp[i][j - 1], dp[i - 1][j]));
}
}
// 返回最小删除和
return sum - 2 * dp[m][n];
}
}
8、最长重复子数组【子数组问题】
. - 力扣(LeetCode)
8.1 算法原理
注意:本题"子数组"相关的问题,并非子序列,子数组要求是连续的,故不能通过定义子序列问题的状态表示来定义子数组的状态表示
子序列:s1[0, i]区间与s2[0, j]区间所有的子序列中,......
子数组:n1中以i位置为结尾以及n2中以j位置为结尾的所有子数组中,......
- 状态表示dp[i][j]:
1. s1[0, i]区间内的所有子数组以及s2[0, j]区间内的所有子数组中,最长公共子数组的长度???(不可行,错误。因为子数组和子序列不同,子数组是连续的,该状态表示不能解决问题)
2. s1中以i位置为结尾以及s2中以j位置为结尾的所有子数组中,最长公共子数组的长度(可行,正确)
- 状态转移方程:
s1[i] != s2[j] --> dp[i][j] = 0
s1[i] == s2[j] --> dp[i][j] = dp[i-1][j-1]+1
- 初始化:
1. 里面的值,要保证后续的填表是正确的 --> 根据"空数组"的定义,确定虚拟节点的值
2. 下标映射
- 建表顺序:
从上往下
- 返回值:
dp表中的最大值
8.2 算法代码
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int[][] dp = new int[m + 1][n + 1];
int ret = 0;
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;
ret = Math.max(ret, dp[i][j]);
}
}
return ret;
}
}
END