Hard

题目描述

给定一个字符串数组 words,返回一个包含数组中每个字符串作为子字符串的最小字符串。如果有多个长度最小的有效字符串,返回其中任意一个即可。

可以假设 words 中没有字符串是另一个字符串的子字符串。

示例 1:

输入:words = ["alex","loves","leetcode"]
输出:"alexlovesleetcode"
解释:"alex","loves","leetcode" 的所有排列都可以被接受。

示例 2:

输入:words = ["catg","ctaagt","gcta","ttca","atgcatc"]
输出:"gctaagttcatgcatc"

提示:

  • 1 <= words.length <= 12
  • 1 <= words[i].length <= 20
  • words[i] 由小写英文字母组成
  • words 的所有字符串都是唯一的

解题思路

这是一个典型的旅行商问题的变种,可以使用动态规划+状态压缩来解决。

核心思路:

  1. 预处理重叠信息:计算每两个字符串之间的最大重叠长度。如果字符串A的后缀与字符串B的前缀有重叠,记录重叠的长度。

  2. 状态设计:使用位掩码表示已经使用的字符串集合,dp[mask][i] 表示使用了 mask 集合中的字符串且以第 i 个字符串结尾的最短超级字符串长度。

  3. 状态转移:对于每个状态,尝试添加一个新的字符串,利用预计算的重叠信息来减少总长度。

  4. 路径记录:在DP过程中记录每个状态的前驱,最后通过回溯构造出最短超级字符串。

算法步骤:

  • 预计算任意两个字符串间的重叠长度
  • 使用状态压缩DP找到最短路径
  • 回溯构造最终的超级字符串

这种方法的时间复杂度是O(n²×2ⁿ),空间复杂度是O(n×2ⁿ),适合处理字符串数量不超过12的情况。

代码实现

class Solution {
public:
    string shortestSuperstring(vector<string>& words) {
        int n = words.size();
        
        // 计算重叠长度
        vector<vector<int>> overlap(n, vector<int>(n, 0));
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i != j) {
                    int maxLen = min(words[i].length(), words[j].length());
                    for (int k = maxLen; k >= 1; k--) {
                        if (words[i].substr(words[i].length() - k) == words[j].substr(0, k)) {
                            overlap[i][j] = k;
                            break;
                        }
                    }
                }
            }
        }
        
        // DP: dp[mask][i] = 使用mask集合中的字符串且以i结尾的最短长度
        vector<vector<int>> dp(1 << n, vector<int>(n, INT_MAX));
        vector<vector<int>> parent(1 << n, vector<int>(n, -1));
        
        // 初始化:只使用一个字符串
        for (int i = 0; i < n; i++) {
            dp[1 << i][i] = words[i].length();
        }
        
        for (int mask = 1; mask < (1 << n); mask++) {
            for (int u = 0; u < n; u++) {
                if (!(mask & (1 << u)) || dp[mask][u] == INT_MAX) continue;
                
                for (int v = 0; v < n; v++) {
                    if (mask & (1 << v)) continue;
                    
                    int newMask = mask | (1 << v);
                    int newLen = dp[mask][u] + words[v].length() - overlap[u][v];
                    
                    if (newLen < dp[newMask][v]) {
                        dp[newMask][v] = newLen;
                        parent[newMask][v] = u;
                    }
                }
            }
        }
        
        // 找到最优解
        int finalMask = (1 << n) - 1;
        int minLen = INT_MAX;
        int last = -1;
        
        for (int i = 0; i < n; i++) {
            if (dp[finalMask][i] < minLen) {
                minLen = dp[finalMask][i];
                last = i;
            }
        }
        
        // 重构路径
        vector<int> path;
        int mask = finalMask;
        int curr = last;
        
        while (curr != -1) {
            path.push_back(curr);
            int prev = parent[mask][curr];
            mask ^= (1 << curr);
            curr = prev;
        }
        
        reverse(path.begin(), path.end());
        
        // 构造结果字符串
        string result = words[path[0]];
        for (int i = 1; i < path.size(); i++) {
            int prev = path[i-1];
            int curr = path[i];
            result += words[curr].substr(overlap[prev][curr]);
        }
        
        return result;
    }
};
class Solution:
    def shortestSuperstring(self, words: List[str]) -> str:
        n = len(words)
        
        # 计算重叠长度
        overlap = [[0] * n for _ in range(n)]
        for i in range(n):
            for j in range(n):
                if i != j:
                    max_len = min(len(words[i]), len(words[j]))
                    for k in range(max_len, 0, -1):
                        if words[i].endswith(words[j][:k]):
                            overlap[i][j] = k
                            break
        
        # DP: dp[mask][i] = 使用mask集合中的字符串且以i结尾的最短长度
        dp = [[float('inf')] * n for _ in range(1 << n)]
        parent = [[-1] * n for _ in range(1 << n)]
        
        # 初始化:只使用一个字符串
        for i in range(n):
            dp[1 << i][i] = len(words[i])
        
        for mask in range(1, 1 << n):
            for u in range(n):
                if not (mask & (1 << u)) or dp[mask][u] == float('inf'):
                    continue
                
                for v in range(n):
                    if mask & (1 << v):
                        continue
                    
                    new_mask = mask | (1 << v)
                    new_len = dp[mask][u] + len(words[v]) - overlap[u][v]
                    
                    if new_len < dp[new_mask][v]:
                        dp[new_mask][v] = new_len
                        parent[new_mask][v] = u
        
        # 找到最优解
        final_mask = (1 << n) - 1
        min_len = float('inf')
        last = -1
        
        for i in range(n):
            if dp[final_mask][i] < min_len:
                min_len = dp[final_mask][i]
                last = i
        
        # 重构路径
        path = []
        mask = final_mask
        curr = last
        
        while curr != -1:
            path.append(curr)
            prev = parent[mask][curr]
            mask ^= (1 << curr)
            curr = prev
        
        path.reverse()
        
        # 构造结果字符串
        result = words[path[0]]
        for i in range(1, len(path)):
            prev = path[i-1]
            curr = path[i]
            result += words[curr][overlap[prev][curr]:]
        
        return result
public class Solution {
    public string ShortestSuperstring(string[] words) {
        int n = words.Length;
        
        // 计算重叠长度
        int[,] overlap = new int[n, n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i != j) {
                    int maxLen = Math.Min(words[i].Length, words[j].Length);
                    for (int k = maxLen; k >= 1; k--) {
                        if (words[i].Substring(words[i].Length - k) == words[j].Substring(0, k)) {
                            overlap[i, j] = k;
                            break;
                        }
                    }
                }
            }
        }
        
        // DP: dp[mask][i] = 使用mask集合中的字符串且以i结尾的最短长度
        int[,] dp = new int[1 << n, n];
        int[,] parent = new int[1 << n, n];
        
        for (int i = 0; i < (1 << n); i++) {
            for (int j = 0; j < n; j++) {
                dp[i, j] = int.MaxValue;
                parent[i, j] = -1;
            }
        }
        
        // 初始化:只使用一个字符串
        for (int i = 0; i < n; i++) {
            dp[1 << i, i] = words[i].Length;
        }
        
        for (int mask = 1; mask < (1 << n); mask++) {
            for (int u = 0; u < n; u++) {
                if ((mask & (1 << u)) == 0 || dp[mask, u] == int.MaxValue) continue;
                
                for (int v = 0; v < n; v++) {
                    if ((mask & (1 << v)) != 0) continue;
                    
                    int newMask = mask | (1 << v);
                    int newLen = dp[mask, u] + words[v].Length - overlap[u, v];
                    
                    if (newLen < dp[newMask, v]) {
                        dp[newMask, v] = newLen;
                        parent[newMask, v] = u;
                    }
                }
            }
        }
        
        // 找到最优解
        int finalMask = (1 << n) - 1;
        int minLen = int.MaxValue;
        int last = -1;
        
        for (int i = 0; i < n; i++) {
            if (dp[finalMask, i] < minLen) {
                minLen = dp[finalMask, i];
                last = i;
            }
        }
        
        // 重构路径
        List<int> path = new List<int>();
        int mask = finalMask;
        int curr = last;
        
        while (curr != -1) {
            path.Add(curr);
            int prev = parent[mask, curr];
            mask ^= (1 << curr);
            curr = prev;
        }
        
        path.Reverse();
        
        // 构造结果字符串
        StringBuilder result = new StringBuilder(words[path[0]]);
        for (int i = 1; i < path.Count; i++) {
            int prev = path[i-1];
            int current = path[i];
            result.Append(words[current].Substring(overlap[prev, current]));
        }
        
        return result.ToString();
    }
}
var shortestSuperstring = function(words) {
    const n = words.length;
    
    // Calculate overlap between word i and word j (when i comes before j)
    const overlap = Array(n).fill().map(() => Array(n).fill(0));
    
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            if (i === j) continue;
            let maxOverlap = 0;
            const minLen = Math.min(words[i].length, words[j].length);
            for (let k = 1; k <= minLen; k++) {
                if (words[i].slice(-k) === words[j].slice(0, k)) {
                    maxOverlap = k;
                }
            }
            overlap[i][j] = maxOverlap;
        }
    }
    
    // DP with bitmask
    // dp[mask][i] = minimum length to visit all words in mask ending at word i
    const dp = Array(1 << n).fill().map(() => Array(n).fill(Infinity));
    const parent = Array(1 << n).fill().map(() => Array(n).fill(-1));
    
    // Initialize: start with each word
    for (let i = 0; i < n; i++) {
        dp[1 << i][i] = words[i].length;
    }
    
    for (let mask = 1; mask < (1 << n); mask++) {
        for (let i = 0; i < n; i++) {
            if (!(mask & (1 << i))) continue;
            if (dp[mask][i] === Infinity) continue;
            
            for (let j = 0; j < n; j++) {
                if (mask & (1 << j)) continue;
                
                const newMask = mask | (1 << j);
                const newLength = dp[mask][i] + words[j].length - overlap[i][j];
                
                if (newLength < dp[newMask][j]) {
                    dp[newMask][j] = newLength;
                    parent[newMask][j] = i;
                }
            }
        }
    }
    
    // Find the ending word that gives minimum length
    const finalMask = (1 << n) - 1;
    let minLength = Infinity;
    let lastWord = -1;
    
    for (let i = 0; i < n; i++) {
        if (dp[finalMask][i] < minLength) {
            minLength = dp[finalMask][i];
            lastWord = i;
        }
    }
    
    // Reconstruct the path
    const path = [];
    let mask = finalMask;
    let curr = lastWord;
    
    while (curr !== -1) {
        path.push(curr);
        const prev = parent[mask][curr];
        mask ^= (1 << curr);
        curr = prev;
    }
    
    path.reverse();
    
    // Build the result string
    let result = words[path[0]];
    for (let i = 1; i < path.length; i++) {
        const prev = path[i - 1];
        const curr = path[i];
        result += words[curr].slice(overlap[prev][curr]);
    }
    
    return result;
};

复杂度分析

复杂度类型分析
时间复杂度O(n² × 2ⁿ + n² × L) - 其中n是字符串数量,L是字符串的平均长度。预处理重叠信息需要O(n² × L),DP需要O(n² × 2ⁿ)
空间复杂度O(n × 2ⁿ + n²) - DP表需要O(n × 2ⁿ)空间,重叠矩阵需要O(n²)空间

相关题目