Skip to content
ΣDSA Patterns
Menu
Language

Bitmask DP

Guide 1 of 6 · Path 1 of 6

PreviousNext →

Interactive

Mental model

A worked animation for this problem. Scrub steps or press space to pause; re-tell the invariant out loud.

Step 1 of 7
A
B
C

mask bit i = city i visited

n ≤ 20 and the state is “which items are used”: a bitmask. 3 cities, TSP-style.

Demo preview: These solutions pass the automated test suite but have not been human-reviewed. Treat trade-offs and prose as draft.

Find the Shortest Superstring

Problem (restated)

n ≤ 12 unique words. Return a shortest string that contains every word as a substring.

Intuition

A shortest superstring is a Hamiltonian path on the words. Edge i → j costs |j| - overlap(i,j), where overlap is the longest suffix of i that is a prefix of j. Drop any word contained in another first.

Approaches

TSP on overlap

Unverified
Time O(n^2 * 2^n * L)Space O(n * 2^n)

Idea. dp[mask][last] = min length of a superstring of subset mask that starts with last. Glue last in front of a superstring of mask \ {last} starting at nxt. Iterate last then nxt; on equal length keep the smaller nxt. Reconstruct from the full-mask start of min length (ties → smallest index), walk parent, glue leftovers.

Walkthrough. ["alex","loves","leetcode"] → "alexlovesleetcode". ["catg","ctaagt","gcta","ttca","atgcatc"] → "gctaagttcatgcatc".

Trade-offs. The parent/tie rules are required so TS/PY/C# emit the same string among equally short answers. n=12, 2^n n^2 is fine.

Solution
export function shortestSuperstring(words: string[]): string {
  const kept: string[] = [];
  for (let i = 0; i < words.length; i++) {
    const w = words[i]!;
    let contained = false;
    for (let j = 0; j < words.length; j++) {
      if (i !== j && words[j]!.includes(w)) {
        contained = true;
        break;
      }
    }
    if (!contained) kept.push(w);
  }
  words = kept;
  const n = words.length;
  if (n === 0) return "";
  const ov: number[][] = Array.from({ length: n }, () => new Array<number>(n).fill(0));
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      if (i === j) continue;
      const a = words[i]!, b = words[j]!;
      const mx = Math.min(a.length, b.length);
      for (let k = mx; k > 0; k--) {
        if (a.slice(-k) === b.slice(0, k)) {
          ov[i]![j] = k;
          break;
        }
      }
    }
  }
  const INF = 1e9;
  const N = 1 << n;
  const dp: number[][] = Array.from({ length: N }, () => new Array<number>(n).fill(INF));
  const parent: number[][] = Array.from({ length: N }, () => new Array<number>(n).fill(-1));
  for (let i = 0; i < n; i++) dp[1 << i]![i] = words[i]!.length;
  for (let mask = 0; mask < N; mask++) {
    for (let last = 0; last < n; last++) {
      if ((mask & (1 << last)) === 0) continue;
      const rest = mask ^ (1 << last);
      if (rest === 0) continue;
      for (let nxt = 0; nxt < n; nxt++) {
        if ((rest & (1 << nxt)) === 0) continue;
        const prev = dp[rest]![nxt]!;
        if (prev >= INF) continue;
        const nlen = words[last]!.length + prev - ov[last]![nxt]!;
        const p = parent[mask]![last]!;
        if (nlen < dp[mask]![last]! || (nlen === dp[mask]![last]! && (p < 0 || nxt < p))) {
          dp[mask]![last] = nlen;
          parent[mask]![last] = nxt;
        }
      }
    }
  }
  const full = N - 1;
  let best = 0;
  for (let last = 1; last < n; last++) {
    if (dp[full]![last]! < dp[full]![best]!) best = last;
  }
  const path: number[] = [];
  let mask = full, cur = best;
  while (cur >= 0) {
    path.push(cur);
    const nxt = parent[mask]![cur]!;
    mask ^= 1 << cur;
    cur = nxt;
  }
  let ans = words[path[0]!]!;
  for (let i = 1; i < path.length; i++) {
    const a = path[i - 1]!, b = path[i]!;
    ans += words[b]!.slice(ov[a]![b]!);
  }
  return ans;
}
export function shortestSuperstring(words: string[]): string {
  const kept: string[] = [];
  for (let i = 0; i < words.length; i++) {
    const w = words[i]!;
    let contained = false;
    for (let j = 0; j < words.length; j++) {
      if (i !== j && words[j]!.includes(w)) {
        contained = true;
        break;
      }
    }
    if (!contained) kept.push(w);
  }
  words = kept;
  const n = words.length;
  if (n === 0) return "";
  const ov: number[][] = Array.from({ length: n }, () => new Array<number>(n).fill(0));
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      if (i === j) continue;
      const a = words[i]!, b = words[j]!;
      const mx = Math.min(a.length, b.length);
      for (let k = mx; k > 0; k--) {
        if (a.slice(-k) === b.slice(0, k)) {
          ov[i]![j] = k;
          break;
        }
      }
    }
  }
  const INF = 1e9;
  const N = 1 << n;
  const dp: number[][] = Array.from({ length: N }, () => new Array<number>(n).fill(INF));
  const parent: number[][] = Array.from({ length: N }, () => new Array<number>(n).fill(-1));
  for (let i = 0; i < n; i++) dp[1 << i]![i] = words[i]!.length;
  for (let mask = 0; mask < N; mask++) {
    for (let last = 0; last < n; last++) {
      if ((mask & (1 << last)) === 0) continue;
      const rest = mask ^ (1 << last);
      if (rest === 0) continue;
      for (let nxt = 0; nxt < n; nxt++) {
        if ((rest & (1 << nxt)) === 0) continue;
        const prev = dp[rest]![nxt]!;
        if (prev >= INF) continue;
        const nlen = words[last]!.length + prev - ov[last]![nxt]!;
        const p = parent[mask]![last]!;
        if (nlen < dp[mask]![last]! || (nlen === dp[mask]![last]! && (p < 0 || nxt < p))) {
          dp[mask]![last] = nlen;
          parent[mask]![last] = nxt;
        }
      }
    }
  }
  const full = N - 1;
  let best = 0;
  for (let last = 1; last < n; last++) {
    if (dp[full]![last]! < dp[full]![best]!) best = last;
  }
  const path: number[] = [];
  let mask = full, cur = best;
  while (cur >= 0) {
    path.push(cur);
    const nxt = parent[mask]![cur]!;
    mask ^= 1 << cur;
    cur = nxt;
  }
  let ans = words[path[0]!]!;
  for (let i = 1; i < path.length; i++) {
    const a = path[i - 1]!, b = path[i]!;
    ans += words[b]!.slice(ov[a]![b]!);
  }
  return ans;
}

Template connection

Open TSP: mask = used words, extra dimension = start of the remaining path, cost = length after overlap.

Reflection