目录
- 题目描述
- 暴力搜索分析
- 暴力搜索优化
- 动态规划
- 参考链接
题目描述
输入:字符串数组String[] A
输出:一个字符串result,A中每一个字符串是result的子串,并且reuslt是符合这个条件的最短的字符串。
举例:
输入: [“alex”,“loves”,“leetcode”]
输出: “alexlovesleetcode”
输入: [“catg”,“ctaagt”,“gcta”,“ttca”,“atgcatc”]
输出: “gctaagttcatgcatc”
暴力搜索分析
分析:result中包含所有A中的字符串,那把A中字符串一次拼接起来肯定满足这个条件。A= [“catg”,“ctaagt”,“gcta”],那么"catgctaagtgcta"符合条件。当然这三个字符串的任意一个排列得到的字符串都符合。
分析条件2:要求result是最短的。例如A[0]A[1]拼接在一起,如果A[1]的前缀是A[0]的后缀,那么它们可以共用这部分字符串,result的长度就会降低。"gcta"和"ctaagt"拼接的时候,“cta”就是公共部分,拼接之后可以是“gctaagt”。
根据这些分析我们写一个暴力搜索的版本(套用排列的代码模板)。
class Solution {private String result = null;private String[] A;public String shortestSuperstring(String[] A) {result = null;this.A = A;boolean[] visited = new boolean[A.length];dfs(A.length,visited,new ArrayList<Integer>());return result;}/*** dfs递归调用* @param m* 还需要取几个元素* @param visited* 哪些元素已经被取了,不能再取* @param path* 按顺序访问的元素下标*/private void dfs(int m,boolean[] visited, ArrayList<Integer> path) {if(m == 0){//注意结果需要完全拷贝String str = contanctString(path);if(result == null || result.length() > str.length()){result = str;}return;}for(int i =0;i<A.length;i++){if(visited[i]==false){visited[i] = true;path.add(i);dfs(m-1,visited,path);path.remove(path.size()-1);visited[i] = false;}}}/***把路径中的字符串拼接起来*/private String contanctString(List<Integer> path){String str = A[path.get(0)];for(int i = 1; i< path.size();i++){str = contanctTwoString(str, A[path.get(i)]);}return str;}/***拼接两个字符串*/private String contanctTwoString(String str1 , String str2){int m = Math.min(str1.length(),str2.length());for(int i = m; i>0;i--){if(str1.endsWith(str2.substring(0,i))){return str1+str2.substring(i);}}return str1+str2;}}
时间复杂度O(n!)。
A的长度范围是[1,12]。这个时间复杂度是不能通过的(花花酱视频中的说明)。12!约等于4亿多。可以考虑剪枝策略和缓存策略。
暴力搜索优化

从递归树中我们可以看到相同位置的字符串拼接会有多次操作。例如路径[1,2,3]、[2,3,1]这两个,A[2]和A[3]就要拼接两次。是不是能提前计算出两两字符串拼接后的字符串,可以少一次。
我们要找的是长度最短的字符串,如果能提前把两两字符串拼接后的字符串的长度记录下来,在dfs过程中发现当前长度大于result(上一个最有结果)的长度就可以停止递归。这样我们需要计算一个数组cost[i][j],表示A[j]拼接在A[i]后面需要增加的长度。
例如 A= [“catg”,“ctaagt”,“gcta”], cost[0][0]=0。cost[0][2]=3,因为合并后的字符串catgcta,需要增加cta三个字符串。
代码链接。
动态规划
我们的目标是要将A中每一个字符串添加到result中。在实际操作过程中,每次添加一个字符串,并且前面的字符串怎么添加不影响后续字符串添加。可以使用动态规划。
定义int s 表示访问了哪些节点。例如s=3,表示访问了A[0],A[1]。对于A= [“catg”,“ctaagt”,“gcta”],s最大值等于7。
定义数组dp[s][i] = 经过路径s,到达i状态,并且是以i结尾,并且每个节点只访问一次的最短字符串长度。目标状态是dp[7][i],从dp[7][0]、dp[7][1]、dp[7][2]中选择最小值作为结果。
这里动态方程,不太好表示。可以使用从下向上的方式。
dp[7][0] = min(dp[6][2] + cost[2][0]
, dp[6][1] + cost[1][0]
, dp[5][0] + cost[0][1]
…)
dp[mask ^ (1<<j)][j] = min{dp[mask][i] + cost[i][j]}
初始化,添加每个单个的字符串到结果中。dp[0][0] = A[0].length(),dp[1][1]=A[1].length(),dp[4][2]=A[2].length()
时间复杂度(2^n)。时间复杂度降低很多。这个代码有很多难点。即使会了递归方程,要想实现出来还是有难度的。
难点1,用int 表示数组中每一位是否被选择 。
难点2,动态规划从s=1开始,逐步递增。
难点3,如果题目求最短字符串的长度的话,只要使用一维数组dp[]即可,这里还要请求输出字符串,所以需要记录下走不通路径到达i状态的长度。
难点4,记录路径需要用到parent数组。
class Solution {public String shortestSuperstring(String[] A) {int n = A.length;int[][] cost = new int[n][n];for(int i=0;i<n;i++){for(int j = 0; j<n;j++){cost[i][j] = minLength(A[i], A[j]);}}int[][] dp = new int[1<<n][n];int[][] parent = new int[1<<n][n];for(int s = 0; s < (1<<n); s++){Arrays.fill(dp[s],10000);Arrays.fill(parent[s],-1);}for(int i=0;i<n;i++){dp[1<<i][i] = A[i].length();}for(int s = 1; s < (1<<n); s++){ for(int j = 0;j<n;j++){//end pointif ((s & (1 << j)) ==0) continue;int pre = s - (1<<j);for(int i =0;i<n;i++){if(dp[pre][i] + cost[i][j] < dp[s][j]){dp[s][j] = dp[pre][i] + cost[i][j];parent[s][j] = i;}}}}int mask = (1<<n)-1;int minCost = dp[mask][0];int endIndex = 0;for(int j =1;j<n;j++){if(dp[mask][j] < minCost){minCost = dp[mask][j] ;endIndex = j;}}String result = "";int cur = endIndex;int s = (1<<n)-1;while(s > 0){int p = parent[s][cur];if(p<0){result = A[cur] + result;break;}else{result = A[cur].substring(A[cur].length()-cost[p][cur]) + result;}s &= ~(1 << cur);cur = p;}return result;}private int minLength(String str1, String str2){int m = Math.min(str1.length(),str2.length());for(int i = m; i>0;i--){if(str1.endsWith(str2.substring(0,i))){return str2.length()-i;}}return str2.length();}}
参考链接
花花酱,
leetcode