动态规划:双序列型
双序列型,就是有两个子序列/字符串,每个序列本身是一维的,可以转换为二维dp,序列型开数组开n+1,双序列型也是开n+1。
突破口:看串A和串B的最后一个字符是否匹配,是否需要串A/串B的最后一个字符,来缩减规模。
两种类型:计数型:情况1+情况2+…以及最值型min/max{情况1,情况2…}
初始条件:要特别当心空串的处理。
1、LintCode 77 Longest Common Subsequence
【问题】最长公共子序列。给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。
【分析】字符串A的长度为m
,字符串B的长度为n
,要组成最长公共子串一定是一个个对子,不能交叉,要按照顺序来,假设现在得到了最长公共子序列,有这么几种情况:
- 字符串A的最后一个字符不在这个LCS中,那最长公共子串就是A中下标为
0~m-2
与B中下标为0~n-1
的字符串的最长公共子序列
。 - 字符串B的最后一个字符不在这个LCS中,那最长公共字串就是B中下标为
0~n-2
与A中下标为0~m-1
的字符串的最长公共子序列
- 字符串A中的最后一个字符与B中的一个字符正好是一对,那最长公共字串就是A中下标为
0~m-2
与B中下标为0~n-2
的字符串的最长公共子序列+A[m-1]
【转移方程】dp[i] [j]代表A中前i个字符和B中前j个字符
dp[i][j] = max{dp[i-1][j], dp[i][j-1], dp[i-1][j-1] + 1|A[i-1]=B[j-1]}
时间复杂度O(MN),空间复杂度O(MN)
public int longestCommonSubsequence(String A, String B) {
int n = A.length();
int m = B.length();
if (n == 0 || m == 0) {
return 0;
}
int[][] dp = new int[n + 1][m + 1]; //双序列型的本质还是序列型
//初始化第0行和第0列
for (int i = 0; i <= m; i++) {
dp[0][i] = 0;
}
for (int i = 0; i <= n; i++) {
dp[n][0] = 0;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
if (A.charAt(i - 1) == B.charAt(j - 1)) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
}
return dp[n][m];
}
可以用滚动数组优化空间复杂度至O(N)
Plus:要求打印所有路径
private static void LCS(String A, String B) {
int m = A.length();
int n = B.length();
int[][] dp = new int[m + 1][n + 1];
//初始化
int i, j;
for (j = 0; j <= n; j++) {
dp[0][j] = 0;
}
for (i = 0; i <= m; i++) {
dp[i][0] = 0;
}
for (i = 1; i <= m; i++) {
for (j = 1; j <= n; j++) {
//如果A的最后一个不在其中,或者是B的最后一个不在其中的情况
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
//如果最后一个都在其中
if (A.charAt(i - 1) == B.charAt(j - 1)) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1); //+1 !!!
}
}
}
//获得了dp数组,dfs获取结果
Set<String> set = new TreeSet<>();
dfs("", m, n, A, B, dp, set);
//打印结果
for (String s : set) {
System.out.println(s);
}
}
private static void dfs(String temp, int i, int j, String A, String B, int[][] dp, Set<String> set) {
if (temp.length() == dp[A.length()][B.length()]) {
set.add(new StringBuilder(temp).reverse().toString());
return;
}
if (A.charAt(i - 1) == B.charAt(j - 1)) { //只有相等时才添加
temp += A.charAt(i - 1);
dfs(temp, i - 1, j - 1, A, B, dp, set);
} else {
//上边更大
if (dp[i - 1][j] >= dp[i][j - 1]) {
dfs(temp, i - 1, j, A, B, dp, set);
}
//左边更大
if (dp[i][j - 1] >= dp[i - 1][j]) {
dfs(temp, i, j - 1, A, B, dp, set);
}
}
}
2、LintCode 29 Interleaving String
【问题】交错字符串。给出三个字符串:s1、s2、s3,判断s3是否由s1和s2交叉构成。
输入:s1=“aabcc” s2=“dbbac”, s3=“aadbbcbcac” 输出:True( s3=“aadbbcbcac” )
【分析】首先如果s3的长度不等于s1+s2的长度,直接输出false,设s1的长度为n,s2的长度为m,s3的长度为n+m,从最后一步出发,假设s3是由s1和s2交错构成的,那么s3的最后一个字符,要么是s1的最后一个字符,要么是s2的最后一个字符。这就是两种情况:
- 如果是s1的最后一个字符,那么
s3[0...n+m-2]
是由s1[0..n-2]与s2[0..m-1]
交错形成的 - 如果是s2的最后一个字符,那么
s3[0...n+m-2]
是由s1[0..n-1]与s2[0..m-2]
交错形成的
这两种情况只要一种成立即可。
【状态】dp[s][i][j]
为s3前s个字符是否由A前i个字符A[0..i-1]
和B前j个字符B[0..j-1]
交错形成,这是最直观的,由于s = i + j
,便可以开成两维,设dp[i][j]
为s3前i+j个字符是否由A前i个字符 A[0..i-1]
和B前j个字符B[0..j-1]
交错形成。
【转移方程】dp[i][j] = (dp[i-1] [j] && s1[i] == s3[i+j-1]) || (dp[i][j-1] && s2[j] == s3[]i+j-1)
【初始条件】空串本身可以由s1的空串和s2的空串交错形成,dp[0][0] = true
【边界情况】如果i=0,不考虑情况一,因为没有s1[i-1];如果j=0,不考虑情况二,因为没有s2[j-1]
【计算顺序】
- f[0] [0], f[0] [1], …, f[0] [m]
- f[1] [0], f[1] [1], …, f[1] [m]
- …
- f[n] [0], f[n] [1], …, f[n] [m]
时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化
public boolean isInterleave(String s1, String s2, String s3) {
int n = s1.length();
int m = s2.length();
int l = s3.length();
if (l != n + m) {
return false;
}
boolean[][] dp = new boolean[n + 1][m + 1];
//初始化
dp[0][0] = true;
//需要把空串也纳入考虑
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
//如果是s1中最后一个字符
if (i > 0 && dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)) {
dp[i][j] = true;
}
//如果是s2中最后一个字符
if (j > 0 && dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1)) {
dp[i][j] = true;
}
}
}
return dp[n][m];
}
3、LintCode 119 Edit Distance
【问题】编辑距离。给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。你总共三种操作方法:插入一个字符、删除一个字符、替换一个字符。
输入: "horse", "ros",输出: 3 解释: horse -> rorse (替换 'h' 为 'r')、rorse -> rose (删除 'r')、rose -> ros (删除 'e')
【分析】要变成一模一样,一定要有个顺序的概念,不然会做起来很麻烦,比如从左往右的顺序。A长度为m,B长度为n,编辑过后A长度为n且与B的字符顺序一样。从最后一步出发,最后一步就是让A的最后一个字符变为B的最后一个字符,一共有三种操作,每种操作考虑一番,得到以下四种情况。
- 情况一:A最后插入B[n-1],才能转换为B,剩下要做的就是要先将A[0…m-1](前面不动)变成B[0…n-2]
- 情况二:A最后一个字符替换为B[n-1],才能转换为B,剩下要做的就是要先将A[0…m-2]变成B[0…n-2]
- 情况三:A删去最后一个字符,才能转换为B,剩下要做的就是要先将A[0…m-2]变成B[0…n-2]
- 情况四:A和B最后一个字符相等,就是要先将A[0…m-2]变成B[0…n-2]
【状态】dp[i][j]
代表A中前i个字符和B中前j个字符的最小编辑距离
【转移方程】dp[i][j] = min{dp[i][j-1]+1,dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1] && A[i-1] = B[j-1]}
- 增加
dp[i][j-1]+1
- 替换
dp[i-1][j-1]+1
- 删除
dp[i-1][j]+1
【初始条件】一个空串和一个长度为L的串的最小编辑距离是L
【计算顺序】
- f[0] [0], f[0] [1], …, f[0] [m]
- f[1] [0], f[1] [1], …, f[1] [m]
- …
- f[n] [0], f[n] [1], …, f[n] [m]
时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化
public int minDistance(String A, String B) {
int n = A.length();
int m = B.length();
int[][] dp = new int[n + 1][m + 1];
int i, j;
//初始化,空串到任意非空串的编辑距离
for (j = 0; j <= m; j++) {
dp[0][j] = j;
}
for (i = 0; i <= n; i++) {
dp[i][0] = i;
}
for (i = 1; i <= n; i++) {
for (j = 1; j <= m; j++) {
dp[i][j] = Math.min(dp[i][j - 1] + 1, Math.min(dp[i - 1][j - 1] + 1, dp[i - 1][j] + 1));
if (A.charAt(i - 1) == B.charAt(j - 1)) {
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - 1]);
}
}
}
return dp[n][m];
}
4、LintCode 118 Distinct Subsequences
【问题】给定字符串 S
和 T
,计算 S
的所有子序列中有多少个 T
。子序列字符串是原始字符串删除一些(或零个)字符之后得到的字符串,并且要求剩下的字符的相对位置不能改变。(比如 "ACE"
是 ABCDE
的一个子序列, 而 "AEC"
不是)
输入: S = "rabbbit", T = "rabbit" 输出: 3 解释: 你可以删除 S 中的任意一个 'b', 所以一共有 3 种方式得到 T. 输入: S = "abcd", T = "" 输出: 1 解释: 只有删除 S 中的所有字符这一种方式得到 T
【分析】给定序列A和B,问B在A中出现多少次,可以不连续。相当于A和B的LCS是B,但这的侧重点是B。 从最后一步出发,就是B的最后一个字符,设A的长度为n,B的长度为m,有两种情况:
B[m-1] != A[n-1]
,需要考虑A[0…n-2]与B[0…m-1]B[m-1] = A[n-1]
,只需考虑A[0…n-2]与B[0…m-2]- 问次数,就是考虑加法,无重复无遗漏。
【转移方程】dp[i][j] = dp[i-1][j] + dp[i-1][j-1] && A[i-1]=B[i-1]
【初始条件】考虑空串
- 若A是空串,B不是空串,B在A中出现次数为0,
dp[0][j] = 0
- 若B是空串,B在A中出现次数是1(A可以是空串),就是把A中的字符都删掉
dp[i][0] = 1
【计算顺序】
- f[0] [0], f[0] [1], …, f[0] [m]
- f[1] [0], f[1] [1], …, f[1] [m]
- …
- f[n] [0], f[n] [1], …, f[n] [m]
时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化成O(N)
public int numDistinct(String A, String B) {
int n = A.length();
int m = B.length();
int[][] dp = new int[n + 1][m + 1];
int i, j;
//初始化:若A是空串而B不是空串,则出现次数为0
for (j = 1; j <= m; j++) {
dp[0][j] = 0;
}
//初始化:若B是空串,则出现次数为1
for (i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (i = 1; i <= n; i++) {
for (j = 1; j <= m; j++) {
dp[i][j] = dp[i - 1][j];
if (A.charAt(i - 1) == B.charAt(j - 1)) {
dp[i][j] += dp[i - 1][j - 1];
}
}
}
return dp[n][m];
}
5、LintCode 154 Regular Expression Matching
【问题】正则表达式匹配。实现支持.
和*
的正则表达式匹配。.
匹配任意一个字母。*
匹配零个或者多个前面的元素。匹配应该覆盖整个输入字符串,而不仅仅是一部分。
需要实现的函数是:bool isMatch(string s, string p) isMatch("aa","a") → false isMatch("aa","aa") → true isMatch("aaa","aa") → false isMatch("aa", "a*") → true isMatch("aa", ".*") → true isMatch("ab", ".*") → true isMatch("aab", "c*a*b") → true
【分析】从最后一步出发,关注最后进来的字符。假设A的长度为n,B的长度为m,关注正则表达式B的最后一个字符是谁,它有三种可能,正常字符
、*
、.
-
如果B的最后一个字符是
正常字符
,那就是看A[n-1]
是否等于B[m-1]
,相等则看A[0..n-2]
与B[0..m-2]
,不等则是不能匹配,break -
如果B的最后一个字符是
.
,它能匹配任意字符,直接看A[0..n-2]
与B[0..m-2]
-
如果B的最后一个字符是
*
它代表B[m-2]=c
可以重复0次或多次,它们是一个整体c*
- 情况一:A[n-1]是0个c,B最后两个字符废了,能否匹配取决于A[0…m-1]和B[0…n-3]是否匹配
- 情况二:A[n-1]是多个c中的最后一个(这种情况必须
A[n-1]=c
或者c='.'
),所以A匹配完往前挪一个,B继续匹配,因为可以匹配多个,继续看A[0…n-2]和B[0…m-1]是否匹配。
【转移方程】dp[i] [j]
代表A的前i个和B的前j个能否匹配
-
对于1和2,可以合并成一种情况
dp[i][j] = dp[i-1][j-1] (if A[i-1]=B[j-1] || B[j-1]='.')
-
对于3,分为不看
c*
和看c*
两种情况- 不看:直接砍掉
dp[i][j] = dp[i][j-2]
- 看:
dp[i][j] = dp[i-1][j](if A[i-1]=B[j-2] || B[j-2]='.')
- 不看:直接砍掉
【初始条件】考虑空串空正则
- 空串和空正则是匹配的,
dp[0][0] = true
- 非空串和空正则必不匹配,
dp[1][0]=...=dp[n][0]=false
- 空串和非空正则,不能直接定义true和false,必须要计算出来。(在1、2中不能计算,在3中
dp[i][j] = dp[i][j-2]
可能出现,比如A=""
,B=a*b*c*
) - 大体上可以分为空正则和非空正则两种
【计算顺序】
- f[0] [0], f[0] [1], …, f[0] [m]
- f[1] [0], f[1] [1], …, f[1] [m]
- …
- f[n] [0], f[n] [1], …, f[n] [m]
时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化成O(N)
public boolean isMatch(String A, String B) {
int n = A.length();
int m = B.length();
boolean[][] dp = new boolean[n + 1][m + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
//分为空正则与非空正则两种讨论
if (j == 0) {
if (i == 0) {
dp[i][j] = true;
} else {
dp[i][j] = false;
}
} else {
//非空正则,大致分为最后一个是不是 *
if (B.charAt(j - 1) != '*') {
if (i > 0 && (A.charAt(i - 1) == B.charAt(j - 1) || B.charAt(j - 1) == '.')) {
dp[i][j] = dp[i - 1][j - 1];
}
} else {
//最后一个是 * ,分为不看和看两种情况
//不看
if (j >= 2) {
dp[i][j] |= dp[i][j - 2];
}
//看
if (i >= 1 && j >= 2 && (A.charAt(i - 1) == B.charAt(j - 2) || B.charAt(j - 2) == '.')) {
dp[i][j] |= dp[i - 1][j];
}
}
}
}
}
return dp[n][m];
}
6、LintCode 192 Wildcard Matching
【问题】通配符匹配,上一题是正则表达式匹配。判断两个可能包含通配符?
和*
的字符串是否匹配。匹配规则如下:?
可以匹配任何单个字符,*
可以匹配任意字符串(包括空字符串)。两个串完全匹配才算匹配成功。
【分析】通配符匹配和正则表达式匹配很像,正则表达式中的.
与通配中的?
作用是一样的,不同的是*
,正则表达式中的*
能匹配零个或者多个前面的元素,通配中的*
能匹配0个或多个任意字符,实际上通配的情况要比正则表达式中的情况简单得多。仍然从B的最后一个字符出发,有三种可能:正常字符
、?
、*
,讨论如下:(前两条情况和正则表达式一样)
-
如果B的最后一个字符是
正常字符
,那就是看A[m-1]是否等于B[n-1],相等则看A[0…m-2]与B[0…n-2],不等则是不能匹配,break -
如果B的最后一个字符是
?
,它能匹配任意字符,直接看A[0…m-2]与B[0…n-2] -
如果B的最后一个字符是
*
,他能匹配0个或多个任意字符,那就分为两种情况- 匹配0个:就是这个
*
直接废了,需要看A[0..n-1]
与B[0..m-2]
- 匹配多个:则需要看
A[0..n-2]
与B[0..m-1]
- 匹配0个:就是这个
【转移方程】dp[i] [j]
代表A的前i个和B的前j个能否匹配
- 对于1和2,可以合并成一种情况
dp[i][j] = dp[i-1][j-1] (if A[i-1]=B[j-1] || B[j-1]='?')
- 对于3,分为不看
c*
和看c*
两种情况- 匹配0个,就是不看,直接砍掉:
dp[i][j] = dp[i][j-1]
- 匹配多个:
dp[i][j] = dp[i-1][j](if B[j-1]='*')
- 匹配0个,就是不看,直接砍掉:
【初始条件】大体上依旧是分为空正则和非空正则两种
- 空正则和空串匹配
- 空正则和非空串必不匹配
- 非空正则和空串需要看情况
【计算顺序】
- f[0] [0], f[0] [1], …, f[0] [m]
- f[1] [0], f[1] [1], …, f[1] [m]
- …
- f[n] [0], f[n] [1], …, f[n] [m]
时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化成O(N)
public boolean isMatch(String A, String B) {
int n = A.length();
int m = B.length();
boolean[][] dp = new boolean[n + 1][m + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
//空正则与非空正则两种情况讨论
if (j == 0) {
if (i == 0) {
dp[i][j] = true;
} else {
dp[i][j] = false;
}
} else {
//分为最后一个字符是不是 * 的两种情况
if (B.charAt(j - 1) != '*') {
if (i > 0 && (A.charAt(i - 1) == B.charAt(j - 1) || B.charAt(j - 1) == '?')) {
dp[i][j] = dp[i - 1][j - 1];
}
} else {
//分为匹配0个和匹配多个两种情况,这里j必定>1
//匹配0个
dp[i][j] |= dp[i][j - 1];
if (i > 0) {
dp[i][j] |= dp[i - 1][j];
}
}
}
}
}
return dp[n][m];
}
7、LintCode 668 Ones And Zeroes
【问题】假设你分别是 m个 0
和 n个 1
的统治者。 另一方面, 有一个只包含 0
和 1
的字符串构成的数组。现在你的任务是找到可以由 m个 0
和 n个 1
构成的字符串的最大个数。每一个 0
和 1
均只能使用一次
输入:["10", "0001", "111001", "1", "0"] 5 3 输出:4 解释:这里总共有 4 个字符串可以用 5个 0s 和 3个 1s来构成, 它们是 "10", "0001", "1", "0"。 输入:["10", "0001", "111001", "1", "0"] 7 7 输出:5 解释:所有字符串都可以由7个 0s 和 7个 1s来构成.
【分析】如果没有0,只有1,这就相当于背包问题。这边只是多了个0,用背包思路考虑,看最后一个物品有没有进去,就是分为放和不放两种情况:
- 情况一:不放,最后一个字符串(物品)没有进去,一共给定了T个串,那就是去看前T-1个串中,用给的0和1最多能组成多少个01串
- 情况二:放,最后一个字符串(物品)进去了,最后一个串中有多少个0和1,那么就在m和n中减去,比如最后一个串中有j个0,k个1,那么剩下0就是m-j,剩下1就是n-k,看这些剩下的在前T-1个串中最多能组成多少个。
【转移方程】用dp[i][j][k]代表前i个串最多能有多少个被j个0和k个1组成
dp[i][j][k] = max{dp[i-1][j][k],dp[i-1][j-a][k-b]}
,a代表放的这个01串中0的个数,b代表放的这个01串中1的个数。
【转移方程】前0个串,最多组成0个
f[0][0~m][0~n] = 0
【答案】dp[T][m][n]
,len为字符串的个数
时间复杂度:O(Tmn),空间复杂度:O(Tmn),可以用滚动数组优化至 O(mn)
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
if (len == 0) {
return 0;
}
int[][][] dp = new int[len + 1][m + 1][n + 1];
int i, j, k;
//初始化
for (j = 0; j <= m; j++) {
for (k = 0; k <= n; k++) {
dp[0][j][k] = 0;
}
}
for (i = 1; i <= len; i++) {
for (j = 0; j <= m; j++) {
for (k = 0; k <= n; k++) {
//不放
dp[i][j][k] = dp[i - 1][j][k];
//放
String s = strs[i - 1];
char[] chs = s.toCharArray();
int count0 = 0, count1 = 0;
for (int l = 0; l < chs.length; l++) {
if (chs[l] == '0') {
count0++;
} else {
count1++;
}
}
if (j >= count0 && k >= count1) {
dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - count0][k - count1] + 1);
}
}
}
}
return dp[len][m][n];
}
滚动数组优化,当前的i之和前一个i-1有关联,空间复杂度O(mn)
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
if (len == 0) {
return 0;
}
int[][][] dp = new int[2][m + 1][n + 1];
int i, j, k;
//初始化
for (j = 0; j <= m; j++) {
for (k = 0; k <= n; k++) {
dp[0][j][k] = 0;
}
}
int old = 0, now = 0;
for (i = 1; i <= len; i++) {
old = now;
now = 1 - now;
for (j = 0; j <= m; j++) {
for (k = 0; k <= n; k++) {
//不放
dp[now][j][k] = dp[old][j][k];
//放
String s = strs[i - 1];
char[] chs = s.toCharArray();
int count0 = 0, count1 = 0;
for (int l = 0; l < chs.length; l++) {
if (chs[l] == '0') {
count0++;
} else {
count1++;
}
}
if (j >= count0 && k >= count1) {
dp[now][j][k] = Math.max(dp[now][j][k], dp[old][j - count0][k - count1] + 1);
}
}
}
}
return dp[now][m][n];
}