一、最长递增子序列(LIS)

  给定数组arr,返回arr的最长递增子序列。例如,arr={2,1,5,3,6,4,8,9,7},返回的最长递增子序列为{1,3,4,5,8,9}

  1.时间复杂度为O(N2)的方法

  第一步:生成长度为N的数组dp,dp[i]表示在以arr[i]这个数结尾的情况下,arr[0...i]中的最大递增子序列长度,其中arr[0]=1

  第二步:要计算dp[i],如果最长递增子序列以arr[i]结尾,那么在arr[0...i]中所有比arr[i]小的数都可以作为倒数第二个数。在这么多倒数第二个数中,以哪个数结尾的最大递增子序列最大,就选那个数作为倒数第二个数,即dp[i] = max{dp[j] + 1(0 <= j < i,arr[j]<arr[i])}。如果arr[0...i]中所有的数都不比arr[i]小,令dp[i]=1即可。可以求出的dp={1,1,2,2,3,3,4,5,4}

    public int[] getdp1(int[] arr) {
int[] dp = new int[arr.length];
for (int i = 0; i < dp.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
return dp;
}

  第三步:目前已经求得的dp数组为dp={1,1,2,2,3,3,4,5,4},下面分为4步找到最长递增子序列

  (1)遍历dp数组,找到最大值以及位置。最大值的位置就是最长子序列的结尾。例如,最大值为5,位置为7,则arr[7]=9是结尾。

  (2)从arr数组的位置7开始从右向左遍历,如果对于某一个arr[i]<arr[7]并且dp[i] = dp[7] - 1,那么说明arr[i]可以作为arr[7]的前面一个数,例如arr[6] < arr[7]并且dp[6] = dp[7] - 1,因此arr[6] = 8应该作为倒数第二个数,目前确定了8 9

  (3)对于有重复的数字,例如arr[5]和arr[4]都满足小于8,并且dp[5]和dp[4]的值都是3,因此选择这两个数中其中一个都可以

  (4)重复上述过程,直到遍历完所有的数即可。

    public int[] generateLIS(int[] arr, int[] dp) {
int len = 0, index = 0;
for (int i = 0; i < dp.length; i++) {
if (dp[i] > len) {
len = dp[i]; // 找到最长的长度
index = i; // 找到结尾的数的位置
}
}
int[] lis = new int[len];
lis[--len] = arr[index];
for (int i = index; i >= 0; i--) {
if (arr[i] < arr[index] && dp[i] == dp[index] - 1) {
lis[--len] = arr[i];
index = i;
}
}
return lis;
}

  第四步:写出总的方法

    public int[] lis1(int[] arr) {
if (arr == null || arr.length == 0) return null;
int[] dp = getdp1(arr);
return generateLIS(arr, dp);
}

  计算dp数组过程的时间复杂度为O(N2),而根据dp数组得到最长递增子序列的时间复杂度为O(N),因此整个过程的时间复杂度为O(N2)。如果想让时间复杂度达到O(NlogN),那么只需要让计算dp数组的过程达到O(NlogN)即可,而根据dp计算出最长递增子序列的过程是一样的。

  

  2.时间复杂度为O(NlogN)的二分查找算法

  生成一个长度为N的数组ends,初始时ends[0]=arr[0],其他位置上为0。生成整型变量righ,初始时right=0。从左到右遍历时,ends[0...right]为有效区,ends[right+1...N-1]为无效区。ends[3]=7的意思就是,在所有长度为4的递增序列中,最小的结尾数就是7。如果来的数大于7,那么长度一定可以达到5,如果来的树大于4小于7,那么长度还是4。

arr = [2,1,5,3,6,4,8,9,7],
0.arr[0]=2,dp=[1],ends=[2]
1.arr[1]=1,dp=[1,1],ends=[1],1<2,2对应的dp为1,因此也是1,
2.arr[2]=5,dp=[1,1,2],ends=[1,5],没有发现比5大的数,dp为原dp+1为2
3.arr[3]=3,dp=[1,1,2,2],ends=[1,3],发现3比5小,5对应的dp是2,因此也是2
4.arr[4]=6,dp=[1,1,2,2,3],ends=[1,3,6],没有发现比6大的数,dp为原dp+1为3
5.arr[5]=4,dp=[1,1,2,2,3,3],ends=[1,3,4],发现4比6小,6对应的dp是3,因此也是3
6.arr[6]=8,dp=[1,1,2,2,3,3,4],ends=[1,3,4,8],没有发现比8大的数,dp为原dp+1为4
7.arr[7]=9,dp=[1,1,2,2,3,3,4,5],ends=[1,3,4,8,9],没有发现比9大的数,dp为原dp+1为5,
8.arr[8]=7,dp=[1,1,2,2,3,3,4,5,4],ends=[1,3,4,7,9],发现7比8小,8对应的dp是4,因此也为4

  利用这种方法得到的代码:例如在第5步时arr[5]=6,此时ends=[1,3,6],则right=2,然后经过二分查找,l=2,right=2,ends[2]=4,dp[5]=2+1=3

    public int[] getdp2(int[] arr) {
int[] dp = new int[arr.length];
int[] ends = new int[arr.length];
ends[0] = arr[0];
dp[0] = 1;
int right = 0;
int l = 0, r = 0, m = 0;
for (int i = 1; i < arr.length; i++) {
l = 0;
r = right;
while (l <= r) {
m = (l + r) / 2;
if (arr[i] > ends[m]) {
l = m + 1;
} else {
r = m - 1;
}
}
right = Math.max(right, l);
ends[l] = arr[i];
dp[i] = l + 1;
}
return dp;
}

  二、最长公共子序列(LCSE)

  问题:给定两个字符串str1和str2,返回两个字符串的最长公共子序列。

  例如str1=“1A2C3D4B56”,str2=“B1D23CA45B6A”,其中“123456”和“12C4B6”都是最长公共子序列,返回哪一个都行。

  如果str1的长度为M,str2的长度为N,生成大小为M×N的矩阵dp,dp[i][j]的含义是str1[0...i]与str2[0...j]的最长公共子序列的长度。

  第一步:矩阵dp的第一列,即dp[0...M-1][0]的含义是str1[0...i]与str2[0]的最长公共子序列长度,如果str1[i]==str2[0],令dp[i][0]=1,且dp[i+1...M-1][0]也都为1,例如,如果str1[0...M-1]=“ABCDE”,并且str2[0]=“B”,那么str1[0]为“A”,不和“B”相等,因此dp[0][0]=0,而str1[1]=B,所以dp[1][0]=1,那么ABC、ABCD、ABCDE和str2的最长公共子序列长度一定为1,所以dp[2...4][0]都等于1

  第二步:矩阵dp的第一行,和第一列同理,如果str1[0] == str2[j],那么dp[0][j]=1,并且dp[0][j+1...N-1]=1

  第三步:对于其他的位置(i,j)来说,只可能存在下面的三种情况(也可以分为两种情况str1[i] == str2[j]和str1[i] != str2[j]),在这三种情况中选择较大的dp[i][j]值即可。

  • dp[i-1][j]:也就是str1[0...i-1]和str2[0...j]的最长公共子序列长度。例如,str1=“A1BC2”,str2=“AB34C”,dp[3][4]=3,因为“A1BC”和“AB34C”的最长公共子序列为“ABC”,而“A1BC2”和“AB34C”的公共子序列也是“ABC”,因此dp[4][4]也等于3
  • dp[i][j-1],例如,str1=“A1B2C”,str2=“AB3C4”,dp[4][3]=3,但是dp[4][4]也等于3
  • 如果str1[i] == str2[j],还可能是,比如:str1=“ABCD”,str2=“ABCD”,dp[2][2]=3,由于str1[3] == str2[3],于是dp[3][3]=4

  简化成一个模型来分析:A对应dp[i-1][j-1],B对应dp[i-1][j],C对应dp[i][j-1],如果要求D也就是dp[i][j]的值:

  如果str1[i]不等于str2[j],那么只需比较B和C即可,选择其中较大的;如果str1[i]等于str2[j],此时还需要比较A+1、B和C

A B
C D

  获得dp的代码为:

    public int[][] getdp(char[] str1, char[] str2){
int[][] dp = new int[str1.length][str2.length];
dp[0][0] = str1[0] == str2[0] ? 1 : 0;
for (int i = 1; i < str1.length; i++) dp[i][0] = Math.max(dp[i-1][0], str1[i] == str2[0] ? 1 : 0);
for (int j = 1; j < str2.length; j++) dp[0][j] = Math.max(dp[0][j-1], str1[0] == str2[j] ? 1 : 0);
for (int i = 1; i < str1.length; i++) {
for (int j = 1; j < str2.length; j++) {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
if (str1[i] == str2[j]) dp[i][j] = Math.max(dp[i][j], dp[i-1][j-1] + 1);
}
}
return dp;
}

  得到的dp矩阵为:

   B 1 D 2 3 C A 4 5 B 6 A
1 0 1 1 1 1 1 1 1 1 1 1 1
A 0 1 1 1 1 1 2 2 2 2 2 2
2 0 1 1 2 2 2 2 2 2 2 2 2
C 0 1 1 2 2 3 3 3 3 3 3 3
3 0 1 1 2 3 3 3 3 3 3 3 3
D 0 1 2 2 3 3 3 3 3 3 3 3
4 0 1 2 2 3 3 3 4 4 4 4 4
B 1 1 2 2 3 3 3 4 4 5 5 5
5 1 1 2 2 3 3 3 4 5 5 5 5
6 1 1 2 2 3 3 3 4 5 5 6 6

  第4步:dp矩阵的右下角的值就代表str1和str2的最长公共子序列长度。如何根据dp矩阵求得最长公共子序列

  (1)从矩阵的右下角开始,有三种移动方式:上,左和左上,假设在移动的过程中,i表示此时的行数,j表示此时的列数,同时用一个变量res表示最长公共子序列。

  (2)如果dp[i][j]大于dp[i-1][j]和dp[i][j-1],说明在之前计算dp[i][j]的时候,一定是选择了dp[i-1][j-1]+1,可以确定的是str[i]等于str[j],并且这个字符一定是属于最长公共子序列,把这个字符放进res,然后向左上方移动。

  (3)如果dp[i][j]等于dp[i-1][j],说明在之前计算dp[i][j]的时候,dp[i-1][j-1]+1不是必须选择的,此时向上方移动即可,同理,如果dp[i][j]等于dp[i][j-1],此时向左方移动即可,如果dp[i][j]同时等于dp[i-1][j]和dp[i][j-1],那么此时向上还是向左移动都可以。总体来说,就是求dp矩阵的逆过程。(上图dp矩阵中黑体的部分就是

    public String lcse(String str1, String str2) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) return "";
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int[][] dp = getdp(chs1, chs2);
int m = chs1.length - 1;
int n = chs2.length - 1;
char[] res = new char[dp[m][n]];
int index = res.length - 1;
while (index >= 0) {
if (n > 0 && dp[m][n] == dp[m][n-1]) {
n--;
} else if (m > 0 && dp[m][n] == dp[m-1][n]) {
m--;
} else {
res[index--] = chs1[m];
m--;
n--;
}
}
return String.valueOf(res);
}

  总结:计算dp矩阵时,时间复杂度为O(M×N),空间复杂度为O(M×N),通过dp矩阵得到最长公共子序列的时间复杂度为O(M+N),空间复杂度为O(M×N),因此,总的时间复杂度为O(M×N),空间复杂度为O(M×N)

  三、最长公共子串(LCST)

  题目:给定两个字符串str1和str2,返回两个字符串的最长公共子串。

  举例:str1=“1AB2345CD”,str2=“12345EF”,返回“2345”

  要求:如果str1的长度为M,str2的长度为N,时间复杂度为O(M×N),空间复杂度为O(1)

  1.空间复杂度为O(M×N)的算法

  首先生成一个大小为M×N的矩阵dp。dp[i][j]的含义是,在必须把str1[i]和str2[j]当作公共子串最后一个字符的情况下,公共子串最长能有多长。例如:str1=“A1234B”,str2=“CD1234”,dp[3][4]表示在必须把str1[3]也就是‘3’和str2[4]也就是‘3’当作公共子串最后一个子串的情况下,公共子串最长能有多长,由于最长公共子串为“123”,因此dp[3][4]=3。如果不能构成公共子串,则令其为0。

  第一步:矩阵第一列,即dp[0...M-1][0]。对于某个位置(i,0)来说,如果str1[i]==str2[0],则令dp[i][0]=1,否则为0。

  第二步:矩阵第一行,即dp[0][0...M-1]。对于某个位置(0,j)来说,如果str1[0]==str2[j],则令dp[0][j]=1,否则为0。

  第三步:其他位置从左往右,从上往下计算。dp[i][j]的值只能有两种情况:

  • 如果str1[i] != str2[j],则说明必须把str1[i]和str2[j]当作公共子串最后一个字符是不可能的,令dp[i][j]=0
  • 如果str1[i] == str2[j],则说明可以把str1[i]和str2[j]当作公共子串的最后一个字符,那么此时最长公共子串的长度是什么呢?如果dp[i-1][j-1]的长度不等于0,则说明前一对已经相等了,于是长度为dp[i-1][j-1]+1,如果前一对不相等,即dp[i-1][j-1]=0,那么现最长长度为1,也就是dp[i-1][j-1]+1。总的来说,dp[i][j] = dp[i-1][j-1]+1

  str1=“A1234B”,str2=“CD1234”得到的dp矩阵为:

0 0 0 0 0 0
0 0 1 0 0 0
0 0 0 2 0 0
0 0 0 0 3 0
0 0 0 0 0 4
0 0 0 0 0 0

  此时的计算dp的代码为:

    public int[][] getdp(char[] str1, char[] str2){
int[][] dp = new int[str1.length][str2.length];
for (int i = 0; i < str1.length; i++) {
if (str1[i] == str2[0]) dp[i][0] = 1;
}
for (int j = 0; j < str2.length; j++) {
if (str2[j] == str1[0]) dp[0][j] = 1;
}
for (int i = 1; i < str1.length; i++) {
for (int j = 1; j < str2.length; j++) {
if (str1[i] == str2[j]) {
dp[i][j] = dp[i-1][j-1] + 1;
}
}
}
return dp;
}

  第四步:根据dp找到最长公共子串,也就是依次找到最大值及其位置(例如上图dp矩阵中的1-2-3-4)即可。

    public String lcst(String str1, String str2) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) return "";
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int[][] dp = getdp(chs1, chs2);
int end = 0;
int max = 0;
for (int i = 0; i < chs1.length; i++) {
for (int j = 0; j < chs2.length; j++) {
if (dp[i][j] > max) {
end = i;
max = dp[i][j];
}
}
}
return str1.substring(end - max + 1, end + 1);
}

  2.空间复杂度为O(1)的算法

  在之前的dp矩阵中,可以看到有非常多的0,然而这些位置上的0其实都是可以不要的,实际在计算每一个dp[i][j]的时候,最多只需要其左上方dp[i-1][j-1]的值,也就是每一条斜线。每一条斜线在计算之前生成整型变量len,len表示左上方的值,初始时len=0,假设计算到(i,j),此时len表示(i-1,j-1)的值,如果str1[i]=str2[j],那么位置(i,j)的值为len+1,如果不相等,则len=0。用变量max记录所有的len中的最大值,然后用end记录其位置即可

0 0 0 0 0 0                                   0           0
0 0 1 0 0 0 0 0 1
0 0 0 2 0 0 0 0 0 2
0 0 0 0 3 0 0 0 0 0 3
0 0 0 0 0 4 0 0 0 0 0 4
0 0 0 0 0 0 0 0 0 0 0 0 ...

  此时,空间复杂度为O(1)的代码为:

    public String lcst1(String str1, String str2) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) return "";
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int row = 0; // 斜线开始位置的行
int col = chs2.length - 1; // 斜线开始位置的列
int max = 0; // 记录最大长度
int end = 0; // 最大长度更新时,记录子串的结尾位置
while (row < chs1.length) {
int i = row;
int j = col;
int len = 0;
// 从(i,j)开始向右下方遍历
while (i < chs1.length && j < chs2.length) {
if (chs1[i] != chs2[j]) {
len = 0;
} else {
len++;
}
// 记录最大位置,以及结束字符的位置
if (len > max) {
end = i;
max = len;
}
i++;
j++;
}
if (col > 0) { // 斜线开始的位置的列先向左移动
col--;
} else { // 列移动到最左之后,行向下移动
row++;
}
}
return str1.substring(end - max + 1, end + 1);
}

  四、最长连续序列 

  问题:给定无序数组arr,返回其中最长的连续序列的长度

  举例:arr=[100,4,200,1,3,2],最长的连续序列为[1,2,3,4],所以返回4

  解答:可以利用哈希表实现时间复杂度为O(N),空间复杂度为O(N)的方法:

  第一步:生成哈希表,其中key代表遍历过的某个数,value代表key这个数所在的最长连续序列的长度。

  第二步:从左到右遍历arr,假如遍历到arr[i],如果arr[i]之前出现过,直接遍历下一个数。如果没有出现过,首先在map中加入(arr[i],1),代表目前arr[i]单独作为一个连续序列。然后看map中是否含有arr[i]-1,如果有,则说明arr[i]-1所在的序列可以和arr[i]合并,合并后记为A序列。利用map可以得到A序列的长度,即为lenA,最小值记为leftA,最大值记为rightA,只在map中更新与leftA和rightA有关的记录,更新成(left,lenA)和(right,rightA),然后看map中是否有arr[i]+1,如果有和A同理的操作。

    public int merge(Map<Integer, Integer> map, int less, int more) {
int left = less - map.get(less) + 1;
int right = more + map.get(more) - 1;
int len = right - left + 1;
map.put(left, len);
map.put(right, len);
return len;
}

  第三步:遍历的过程中用全局变量max记录每次合并出的序列的长度最大值,最后返回max。

    public int longestConsecutive(int[] arr) {
if (arr == null || arr.length == 0) return 0;
int max = 1;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (!map.containsKey(arr[i])) map.put(arr[i], 1);
if (map.containsKey(arr[i] - 1)) max = Math.max(max, merge(map, arr[i] - 1, arr[i]));
if (map.containsKey(arr[i] + 1)) max = Math.max(max, merge(map, arr[i], arr[i]+1));
}
return max;
}

  整个过程中,只是每个连续序列最小值和最大值在map中的记录有意义,中间的记录不再更新,因为再也不会使用到。这是因为我们只处理之前没出现过的数,如果一个没出现的数能够把某个连续区间扩大,或把两个连续区间连在一起,就只需要更新连续区间最小值和最大值就可以了。

以arr=[100,4,200,1,3,2]为例,初始时max=1
100进map,此时map中{100=1}
4进map,此时map中{100=1, 4=1}
200进map,此时map中{100=1, 4=1, 200=1}
1进map,此时map中{1=1, 100=1, 4=1, 200=1}
3进map,此时因为map中有4,于是合并3和4,更新max=2,此时map中{1=1, 3=2, 100=1, 4=2, 200=1}
2进map,首先合并1和2,1=2,2=2,max更新为3,然后合并2和3,1=4,4=4(不关心2和3),max更新为4,最后map有{1=4, 2=2, 3=2, 100=1, 4=4, 200=1

  代码为:主要理解merge方法中的只顾连续序列的首尾而不管首尾中间的值的技巧。

    public int longestConsecutive(int[] arr) {
if (arr == null || arr.length == 0) return 0;
int max = 1;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (!map.containsKey(arr[i])) map.put(arr[i], 1);
if (map.containsKey(arr[i] - 1)) max = Math.max(max, merge(map, arr[i] - 1, arr[i]));
if (map.containsKey(arr[i] + 1)) max = Math.max(max, merge(map, arr[i], arr[i]+1));
}
return max;
}

  

  五、最小编辑代价(Minimum edit cost)

  问题:给定两个字符串str1和str2,在给定三个整数ic、dc和rc,分别代表插入、删除和替换一个字符的代价,返回将str1编辑成str2的代价。

  举例:str1=“abc”,str2=“adc”,ic=5,dc=3,rc=2,那么把“b”替换成“d”的代价是最小的,为2.

     str1=“abc”,str2=“adc”,ic=5,dc=3,rc=100,那么先删除b,然后插入d的代价是最小的,为8

  1.空间复杂度为O(M×N)的算法

  动态规划表dp:大小为(M+1)×(N+1),因为字符串中“”也是其子串,dp[i][j]表示str1[0...i-1]编辑成str2[0...j-1]的最小代价。

  第一步:dp[0][0]表示str1空的子串编辑成str2空的子串的代价为0

  第二步:dp矩阵的第一列,即dp[0...M-1][0]。dp[i][0]表示str1[0...i-1]编辑成空子串的最小代价,显然是把str1[0...i-1]全部删掉,因此dp[i][0]=dc*i

  第三步:dp矩阵的第一行,即dp[0][0...N-1]。dp[0][j]表示空串编辑成str2[0...j-1]的最小代价,显然是把str2[0...j-1]全部插入,因此dp[0][j]=ic*i

  第四步:其他位置按照从左到右,从上到下计算,dp[i][j]的值只能来自下面的四种情况

  • str1[0...i-1]可以先编辑成str1[0...i-2],也就是删除字符str1[i-1],然后由str1[0...i-2]编辑成str2[0...j-1],而dp[i-1][j]表示str1[0...i-2]编辑成str2[0...j-1]的最小代价,那么此时dp[i][j]=dc+dp[i-1][j]
  • str1[0...i-1]可以先编辑成str2[0...j-2],这一过程可以用dp[i][j-1]表示,然后再由str2[0...j-2]插入一个str2[j-1]变成str2[0...j-1],那么此时dp[i][j]=ic+dp[i][j-1]
  • 如果str1[i-1] != str2[j-1],也就是str1的最后一位和str2的最后一位不相等,那么可以先把str1[0...i-2]部分变成str2[0...j-2],可以用dp[i-1][j-1]表示,然后把str1的最后一位替换成str2的最后一位即可,此时dp[i][j] = dp[i-1][j-1] + rc
  • 如果str1[i-1] == str2[j-1],也就是str1的最后一位已经和str2的最后一位相等,那么根据上面的分析,此时dp[i][j]就直接是dp[i-1][j-1]

  第五步:在上面的四种情况中,选择最小的值作为dp[i][j],然后dp右下角的值就是最终的结果。

假设:str1 = "ab12cd3",str2 = "abcdf",ic = 5, dc = 3, rc = 2;则dp矩阵为
"" a b c d f
"" 0 5 10 15 20 25
a 3 0 5 10 15 20
b 6 3 0 5 10 15
1 9 6 3 2 7 12
2 12 9 6 5 4 9
c 15 12 9 6 7 6
d 18 15 12 9 6 9
3 21 18 15 12 9 8

  空间复杂度为O(M×N)的代码为:

    public int minCost1(String str1, String str2, int ic, int dc, int rc) {
if (str1 == null || str2 == null) return 0;
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int row = chs1.length + 1;
int col = chs2.length + 1;
int[][] dp = new int[row][col];
for (int i = 1; i < row; i++) dp[i][0] = dc * i;
for (int j = 1; j < col; j++) dp[0][j] = ic * j;
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (chs1[i-1] == chs2[j-1]) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = dp[i-1][j-1] + rc;
}
dp[i][j] = Math.min(dp[i][j], dp[i][j-1] + ic);
dp[i][j] = Math.min(dp[i][j], dp[i-1][j] + dc);
}
}
return dp[row-1][col-1];
}

  2.空间复杂度为O(min{M,N})的算法

  在使用空间压缩的方法时,和之前“矩阵的最小路径和”的使用dp数组不同的是,之前的dp[i][j]仅依赖两个位置的值dp[i-1][j]和dp[i][j-1],滚动数组时求dp[j]的时候,dp[j]没有更新之前相等于dp[i-1][j]的值,而dp[j-1]的值又已经更新过相当于dp[i][j-1]的值。而这里的dp[i][j]还依赖dp[i-1][j-1],因此还需要一个变量来保存dp[j-1]没更新之前的值,也就是dp[i-1][j-1]。另外,对于dp数组长度问题,还是使用str1和str2中较短的一个作为列对应的字符串,长度较长的作为行对应的字符串,如果此时str1做了列对应的字符串,那么也就是由str2如何变到str1,str1变成str2的插入过程,就对应着str2变成str1的删除过程,于是此时把插入代价和删除代价交换就可以了。  

    public int minCost2(String str1, String str2, int ic, int dc, int rc) {
if (str1 == null || str2 == null) return 0;
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
char[] longstr = chs1.length >= chs2.length ? chs1 : chs2;
char[] shortstr = chs1.length < chs2.length ? chs1 : chs2;
if (chs1.length < chs2.length) { // 如果str2较长,那么就交换ic到dc
int tmp = ic;
ic = dc;
dc = tmp;
}
int[] dp = new int[shortstr.length + 1];
for (int i = 1; i < shortstr.length; i++) dp[i] = ic*i;
for (int i = 1; i <= longstr.length; i++) {
int pre = dp[0]; // pre保存左上角的值
dp[0] = dc * i;
for (int j = 1; j <= shortstr.length; j++) {
int tmp = dp[j]; // 将没更新之前的dp[j]先保存下来
if (longstr[i-1] == shortstr[j-1]) {
dp[j] = pre;
} else {
dp[j] = pre + rc;
}
dp[j] = Math.min(dp[j], dp[j-1] + ic);
dp[j] = Math.min(dp[j], tmp + dc);
pre = tmp; // pre变成下一轮dp[j]没更新前的值
}
}
return dp[shortstr.length];
}

  六、交错字符串(Staggered String)

  问题:给定三个字符串str1、str2和aim,如果aim包含且仅包含来自str1和str2的所有字符,并且aim中属于str1和str2的字符都和在原字符串中顺序一致,那么aim是str1和str2的交错字符串。判断aim是否是由str1和str2交错形成。

  举例:str1=“AB”,str2=“12”,那么“AB12”、“A1B2”、“A12B”、“1A2B”和“1AB2”等都是str1和str2的交错字符串。

  1.空间复杂度为O(M×N)的算法

  动态规划矩阵dp:大小为(M+1)×(N+1),dp[i][j]表示aim[0...i+j-1]能否被str1[0...i-1]和str[j-1]交错组成。dp[M][N]表示aim整体能够被str1整体和str2整体交错组成。

  第一步:dp[0][0] = true,也就是aim为空串时,当然可以被str1为空串和str2为空串交错组成

  第二步:dp矩阵的第一列,即dp[0...M-1][0],dp[i][0]表示aim[0...i-1]能够只被str1[0...i-1]交错组成,那也就是如果aim[0...i-1]等于str1[0...i-1],则dp[i][0]=true,否则返回false

  第三步:dp矩阵的第一行,即dp[0][0...N-1],dp[0][j]表示aim[0...j-1]能够只被str2[0...j-1]交错组成,那也就是如果aim[0...j-1]等于str2[0...j-1],则dp[0][j]=true,否则返回false

  第四步:对于其他位置(i,j),dp[i][j]的值由下面的几种情况决定:

  • dp[i-1][j]代表aim[0...i+j-2]能够被str1[0...i-2]和str2[0..j-1]交错形成,如果可以,那么如果再有str1[i-1]等于aim[i+j-1],说明str1[i-1]又可以作为交错形成aim[0...i+j-1]的最后一个字符,令dp[i][j]=true
  • dp[i][j-1]代表aim[0...i+j-2]能够被str1[0...i-1]和str2[0...j-2]交错形成,如果可以,那么如果再有str2[j-1]等于aim[i+j-1],说明str2[j-1]又可以作为交错形成aim[0...i+j-1]的最后一个字符,令dp[i][j]=true
  • 如果上面两种情况都不满足,则令dp[i][j]=false
假设:str1 = "AB", str2 = "12", aim = "1A2B"
'' '1' '2'
'' true true false
'A' false true true
'B' false false true

  空间复杂度为O(M×N)的代码为:

    public boolean isCross1(String str1, String str2, String aim) {
if (str1 == null || str2 == null || aim == null) return false;
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
char[] chaim = aim.toCharArray();
if (chaim.length != chs1.length + chs2.length) return false;
boolean[][] dp = new boolean[chs1.length + 1][chs2.length + 1];
dp[0][0] = true;
for (int i = 1; i <= chs1.length; i++) {
if (chs1[i - 1] != chaim[i - 1]) break;
dp[i][0] = true;
}
for (int j = 1; j <= chs2.length; j++) {
if (chs2[j - 1] != chaim[j - 1]) break;
dp[0][j] = true;
}
for (int i = 1; i <= chs1.length; i++) {
for (int j = 1; j <= chs2.length; j++) {
if ((chs1[i-1] == chaim[i+j-1] && dp[i-1][j]) || (chs2[j-1] == chaim[i+j-1] && dp[i][j-1])){
dp[i][j] = true;
} }
}
return dp[chs1.length][chs2.length];
}

  2.空间复杂度为O(min{M,N})的算法

  由于dp[i][j]只与dp[i-1][j]和dp[i][j-1]有关,因此和常规的空间压缩方法一样,比较str1和str2的长度,选择长度较小的那个作为列对应的字符串,然后生成和较短字符串长度一样的一维数组dp,滚动更新(即从左往右,从上往下)即可

    public boolean isCross2(String str1, String str2, String aim) {
if (str1 == null || str2 == null || aim == null) return false;
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
char[] chaim = aim.toCharArray();
if (chaim.length != chs1.length + chs2.length) return false;
char[] longstr = chs1.length >= chs2.length ? chs1 : chs2;
char[] shortstr = chs1.length < chs2.length ? chs1 : chs2;
boolean[] dp = new boolean[shortstr.length + 1];
dp[0] = true;
for (int i = 1; i <= chs1.length; i++) {
if (shortstr[i - 1] != chaim[i - 1]) break;
dp[i] = true;
}
for (int i = 1; i <= longstr.length; i++) {
dp[0] = dp[0] && longstr[i - 1] == chaim[i - 1];
for (int j = 1; j <= shortstr.length; j++) {
if ((longstr[i-1] == chaim[i+j-1] && dp[i]) ||
shortstr[j-1] == chaim[i+j-1] && dp[j-1]) {
dp[j] = true;
} else {
dp[j] = false;
} }
}
return dp[shortstr.length];
}

  

OptimalSolution(1)--递归和动态规划(3)数组和字符串问题的更多相关文章

  1. OptimalSolution(1)--递归和动态规划(2)矩阵的最小路径和与换钱的最少货币数问题

    一.矩阵的最小路径和 1 3 5 9 1 4 9 18 1 4 9 18 8 1 3 4 9 9 5 8 12 5 0 6 1 14 14 5 11 12 8 8 4 0 22 22 13 15 12 ...

  2. OptimalSolution(1)--递归和动态规划(1)斐波那契系列问题的递归和动态规划

    一.斐波那契数列 斐波那契数列就是:当n=0时,F(n)=0:当n=1时,F(n)=1:当n>1时,F(n) = F(n-1)+F(n-2). 根据斐波那契数列的定义,斐波那契数列为(从n=1开 ...

  3. OptimalSolution(1)--递归和动态规划(4)其他问题

    一.汉诺塔问题(包括chapter 1中的汉诺塔问题) 二. 三. 四. 五. 六. 七. 八.

  4. 算法 递归 迭代 动态规划 斐波那契数列 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  5. Python学习笔记 之 递归、二维数组顺时针旋转90°、正则表达式

    递归.二维数组顺时针旋转90°.正则表达式 1.   递归算法是一种直接或间接调用自身算法的过程. 特点: 递归就是在过程或函数里调用自身 明确的递归结束条件,即递归出口 简洁,但是不提倡 递归次数多 ...

  6. 70. Climbing Stairs【leetcode】递归,动态规划,java,算法

    You are climbing a stair case. It takes n steps to reach to the top. Each time you can either climb ...

  7. js递归遍历多维数组并在修改数组的key后返回新的多维数组

    我司最近正在用VUE做一个基于用户权限显示不同左侧菜单的后台管理系统,接口会根据用户的权限不同返回不同的菜单名称.URL等,前端要将这些菜单名称及URL动态添加到系统的左侧,这里就用到了vue-rou ...

  8. PHP递归方式把一个数组里面的null转换为空字符串”的方法

    在一些接口的调用中,直接查询数据库出来的字段可能为null字段,但是为了简便前端的判断,需要把null转换成空字符串'',这个时候就需要递归的方式进行.直接上代码如下: //递归方式把数组或字符串 n ...

  9. 动态规划-划分数组的最大和 Split Array Largest Sum

    2019-10-14 22:13:18 问题描述: 问题求解: 解法一:动态规划 这种数组划分的题目基本都可以使用dp来解决,核心的思路就是先维护低的划分,再在中间找分割点加入新的划分. public ...

随机推荐

  1. Elastic Static初识(01)

    写在前面 Elastic Static 是指由Elasticsearch,Logstash,Kibana,Beats等组件结合起来而构成的一个数据收集,分析,可视化的一个架构.我们经常听说过的ELK就 ...

  2. idea创建javaweb原生项目

    使用idea创建javaweb项目 idea还是写框架项目比较爽,原生的javaweb项目不是特别方便,这篇文章就是记录一下创建的过程 图较多注意流量 选择创建web项目 配置tomcat服务器 配置 ...

  3. 升级@Scheduled-分布式定时任务

    最近我在对项目的定时任务服务升级,希望改造成分布式,原本是利用@Scheduled注解实现,然而它并不支持分布式,如果改成quartz或者Spring Cloud Task,感觉对于自己这个简单的项目 ...

  4. springboot 配置mybatis打印sql

    方式 一: ###########################################################配置打印sql############################ ...

  5. freemarker常用属性

    1.th:action 定义后台控制器的路径,类似<form>标签的action属性. 示例如下. <form id="login" th:action=&quo ...

  6. 难题解决:Mycat数据库中间件+Mybatis批量插入数据并返回行记录的所有主键ID

     一.mybatis的版本必须为3.3.1及其以上 项目所依赖的mybatis的版本必须为3.3.1及其以上,低版本的不行,保证hap项目的依赖的mybatis的jar的版本必需为需要的版本: 二.在 ...

  7. MongoDB4.0支持事务管理

    ​ 背景 最后我们看一下MongoDB的事务管理,本来是没这一篇的,因为网上大部分资料太老,都为MongoDB之前的版本,的确在MongoDB 4.0版本之前是没有事务管理,但是今天年初MongoDB ...

  8. Linux下几种常见压缩方式测试对比

    目录 Linux下几种常见压缩方式测试对比 参考 简介 测试 总结 Linux下几种常见压缩方式测试对比

  9. git checkout 提示 “error: The following untracked working tree files would be overwritten by checkout” 解决

    问题描述 Windows 或者 macOS 操作系统中,文件名是不区分大小写的.对于已经提交到仓库中的文件修改文件名的大小写,然后又用 git rm 命令将老文件从 Git 仓库删除掉,并保存下新的文 ...

  10. 每个新手程序员都必须知道的Python技巧

    当下,Python 比以往的任何时候都更加流行,人们每天都在实践着 Python 是多么的强大且易用. 我从事 Python 编程已经有几年时间了,但是最近6个月才是全职的.下面列举的这些事情,是我最 ...