题目描述
输入:字符串数组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 point
if ((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();
}
}