0943. Find the Shortest Superstring - kumaeki/LeetCode GitHub Wiki

0943. Find the Shortest Superstring


Given an array of strings words, return the smallest string that contains each string in words as a substring. If there are multiple valid strings of the smallest length, return any of them.

You may assume that no string in words is a substring of another string in words.

Example 1:

Input: words = ["alex","loves","leetcode"]
Output: "alexlovesleetcode"
Explanation: All permutations of "alex","loves","leetcode" would also be accepted.

Example 2:

Input: words = ["catg","ctaagt","gcta","ttca","atgcatc"]
Output: "gctaagttcatgcatc"

Constraints:

  • 1 <= words.length <= 12
  • 1 <= words[i].length <= 20
  • words[i] consists of lowercase English letters.
  • All the strings of words are unique.

解法1

这个是真的难. 这个标准解法是真的苟.

class Solution {
    public String shortestSuperstring(String[] words) {
        int n = words.length, len = 1<<n;
        
        // ovarlaps[i][j] 表示word[i] , word[j]的重叠部分长度(i在前, j在后)
        // 如果没有重叠部分,则记为0(初始值)
        int[][] overlaps = new int[n][n];
        for(int i = 0; i < n; i++){
            String s1 = words[i];
            for(int j = 0; j < n; j++){
                if(i == j)
                    continue;
                String s2 = words[j];
                int m = Math.min(s1.length(), s2.length());
                // 判断是否有重叠, 因为没有包含的情况, 所以从m-1开始计算
                for(int k = m - 1; k >=0; k--)
                    if(s1.endsWith(s2.substring(0, k))){
                        overlaps[i][j] = k;
                        break;
                    }
            }
        }
        
        // mask的二进制形式表示当前的重叠状态,最多只有20个字符串,所以int(32位)够用
        // 比如, 00010011, 表示一共8个字符串,其中有3个字符串(第4,7,8位)可以前后重叠拼接起来
        // dp[mask][j],保存拼接状态为mask(的二进制形式), 而最后的字符串为words[j]时,各个字符串之间重叠部分的总长度
        // 如果没有重叠部分,则记为0(初始值)
        int[][] dp = new int[len][n];
        // parent[mask][j],保存表示拼接状态为mask(的二进制形式), 而最后的字符串为words[j]时,j的前一个字符在words中的位置i
        int[][] parent = new int[len][n];;
        
        // 遍历mask所有可能的组合
         for(int mask = 0; mask < len; mask++){
             // 初始状态, parent中对应的前一个字符的位置为-1
             Arrays.fill(parent[mask], -1);
             
             // 计算mask的二进制形式的每一位
             for(int j = 0; j < n; j++)
                 
                 // 如果当前位为1(有用到当前字符串)
                 if(((mask >> j) & 1) > 0){
                     
                     // pmask的二进制形式,表示去掉当前位(j)之后剩下的拼接状态
                     // 比如, mask = 01001,j = 3, 计算得到 pmask = 00001
                     // 比如, mask = 01000,j = 3, 计算得到 pmask = 00000
                     int pmask = mask ^ (1 << j);
                     // 如果为0,表示已经没有剩下的字符,跳过当前j
                     if(pmask == 0)
                         continue;
                     
                     // 计算dp[mask][j]
                     // 在mask和j确定的情况下,遍历pmask,求得最大长度,并更新parent中对应的值
                     // 第一个问题, 为什么是最大值而不是最小值?????????????????????????
                     //            因为dp保存的是重叠部分的长度和, 重叠越多, 拼接后的字符串越短
                     // 第二个问题, dp[pmask][i] + overlaps[i][j], words[i]的字符可能会有双重计算的问题.
                     //            没有问题. 因为都是保存的重叠部分的长度, 就算有, 也可以通过计算消除
                     for(int i = 0; i < n; i++)
                         // 如果当前位为1(有用到当前字符串)
                         if(((pmask >> i) & 1) > 0){
                             int val = dp[pmask][i] + overlaps[i][j];
                             if(val > dp[mask][j]){
                                 dp[mask][j] = val;
                                 parent[mask][j] = i;
                             }
                         }
                 }
         }
        // 保存字符串的排列顺序
        int[] perm = new int[n];
        // 标记字符串是否被访问
        boolean[] isVisited = new boolean[n];
        int t = n - 1, mask = len - 1;
        
        // 得到当所有字符串都连起来时,有最长重叠区域的末尾字符串的位置p
        int p = 0;
        for(int j = 0; j < n; j++)
            if(dp[len - 1][j] > dp[len - 1][p])
                p = j;
        
        // 从最后一个字符串位置p,逆向得到所有字符串的位置,并存入perm中
        while(p != -1){
            perm[t--] = p;
            // 标记为已访问
            isVisited[p] = true;
            int p2 = parent[mask][p];
            // 去掉当前位置p,得到新的mask
            mask ^= 1 << p;
            p = p2;
        }
        
        // 如果有没有访问到的字符串,依次加在前面
        for(int i = 0; i < n; i++)
            if(!isVisited[i])
                perm[t--] = i;
        
        // 得到结果
        StringBuilder res = new StringBuilder(words[perm[0]]);
        for(int i = 1; i < n; i++){
            int pre = perm[i - 1], cur = perm[i];
            int overlap = overlaps[pre][cur];
            res.append(words[cur].substring(overlap));
        }
        
        return res.toString();
    }
}