一、最长递增子序列(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]; }