943. Find the Shortest Superstring

题目描述

输入:字符串数组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();
    }

    
}

参考链接

花花酱
leetcode

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值