Day 038-Q3 — 最短スーパーストリング(TSP + ビット集合DP)

2026-05-21 赤色 Master / Phase 8+ ★★★★★★★★★ TSP / ビットDP / KMP

問題

$N$ 個の文字列 $S_1, \ldots, S_N$ が与えられる(どの文字列も他の部分文字列でない)。すべての文字列を部分文字列として含む最短の文字列(スーパーストリング)の長さを求めよ。

制約

パラメータ範囲
$N$$2 \le N \le 20$
$|S_i|$$1 \le |S_i| \le 100$
文字種英小文字のみ
制限時間 3 sec / メモリ 256MB

入出力例

入力例 1

4
CATGC
CTAAGT
GCTA
TTCA

出力例 1

11

例: "GCTAAGTTCATGC" → 長さ 13。最短は 11(最適順序で重複部分を最大化)。

概念図: TSP 型 ビット DP

文字列を「都市」に見立て、辺の重みを「次の文字列を置くときの追加文字数」とした最短 TSP 問題。

S0 CATGC S1 CTAAGT S2 GCTA S3 TTCA cost[0→1]=5 cost[0→2]=? cost[3→2]=? KMP Overlap ov[i][j] = Si末尾と Sj先頭の最大一致長 cost = |Sj| - ov[i][j] Bit DP dp[mask][i]: maskの文字列処理済み 最後がi の最小文字数 状態: O(2^N × N) 遷移: O(N) 初期: dp[1<<i][i] = |Si| → 遷移: dp[mask|(1<<j)][j] = min(dp[mask][i] + cost[i][j])

ヒント(段階的開示)

ヒント1: 方向性
各文字列を「都市」に見立てた TSP。辺の重み = 「$S_i$ の後に $S_j$ を置くときの追加文字数」= $|S_j| - \text{overlap}(S_i, S_j)$。全文字列を使う最短 Hamiltonian Path を求める。
ヒント2: KMP Overlap 計算
kmp_overlap(a, b) は文字列 b + '#' + a の KMP failure function の最後の値。これが「$a$ の末尾と $b$ の先頭の最大一致長」= overlap(a, b)。$O(|a|+|b|)$ で計算。
ヒント3: DP の遷移コード
INF = 10**9
dp = [[INF]*N for _ in range(1<<N)]
for i in range(N):
    dp[1<<i][i] = len(S[i])

for mask in range(1, 1<<N):
    for i in range(N):
        if dp[mask][i] == INF: continue
        if not (mask >> i & 1): continue
        for j in range(N):
            if mask >> j & 1: continue
            new_cost = dp[mask][i] + cost[i][j]
            new_mask = mask | (1 << j)
            dp[new_mask][j] = min(dp[new_mask][j], new_cost)

print(min(dp[(1<<N)-1]))

模範解答 (Python)

import sys
input = sys.stdin.readline

def kmp_overlap(a, b):
    """a の末尾が b の先頭にマッチする最大長 (KMP failure function)"""
    s = b + '#' + a
    n = len(s)
    fail = [0] * n
    j = 0
    for i in range(1, n):
        while j > 0 and s[i] != s[j]:
            j = fail[j-1]
        if s[i] == s[j]:
            j += 1
        fail[i] = j
    return fail[-1]

def solve():
    N = int(input())
    S = [input().strip() for _ in range(N)]

    ov = [[0]*N for _ in range(N)]
    for i in range(N):
        for j in range(N):
            if i != j:
                ov[i][j] = kmp_overlap(S[i], S[j])

    cost = [[0]*N for _ in range(N)]
    for i in range(N):
        for j in range(N):
            cost[i][j] = len(S[j]) - ov[i][j]

    INF = 10**9
    FULL = (1 << N) - 1
    dp = [[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(S[i])

    for mask in range(1, 1 << N):
        for i in range(N):
            if dp[mask][i] == INF:
                continue
            if not (mask >> i & 1):
                continue
            for j in range(N):
                if mask >> j & 1:
                    continue
                new_mask = mask | (1 << j)
                new_cost = dp[mask][i] + cost[i][j]
                if new_cost < dp[new_mask][j]:
                    dp[new_mask][j] = new_cost
                    parent[new_mask][j] = i

    ans = min(dp[FULL])
    print(ans)

    # 経路復元
    last = dp[FULL].index(ans)
    path = []
    mask = FULL
    while last != -1:
        path.append(last)
        prev = parent[mask][last]
        mask ^= (1 << last)
        last = prev
    path.reverse()

    result = S[path[0]]
    for k in range(1, len(path)):
        i, j = path[k-1], path[k]
        result += S[j][ov[i][j]:]
    # print(result)  # 文字列自体が必要な場合

solve()

Step-by-Step 解説

1Overlap 計算(KMP)
kmp_overlap(a, b): 文字列 b + '#' + a の failure function の最後の値 = $a$ の末尾と $b$ の先頭の最大一致長。$O(|a|+|b|)$。
2コスト行列
cost[i][j] = |S_j| - overlap[i][j]。$S_i$ の後に $S_j$ を追加するときに増える文字数。
3ビット DP の初期化
dp[1<<i][i] = |S_i|: 各文字列単独から始めた場合の文字数。
4DP の遷移
全 mask, 全 last = i で、未使用の j に対して dp を更新。状態数 $O(2^N \cdot N)$、遷移 $O(N)$、全体 $O(2^N \cdot N^2)$。$N=20$ で約 $4 \times 10^7$。
5経路復元
parent 配列を逆順にたどって文字列の順序を復元。各接続で重複分を除いて結合。

計算量

Overlap 計算: $O(N^2 \cdot L)$ — $L = \max |S_i|$
ビット DP: $O(2^N \cdot N^2)$
経路復元: $O(N)$
合計: $O(2^N \cdot N^2 + N^2 L)$ — $N=20, L=100$ で約 $4 \times 10^7$

よくあるミス

ミス原因正しい書き方
KMP の文字列順序ミスa + '#' + b にすると逆必ず b + '#' + a(先頭パターンが b)
cost に |S_i| を使う追加コストの定義ミスcost[i][j] = len(S[j]) - ov[i][j]
未使用ビット確認漏れ処理済みの文字列に遷移if mask >> j & 1: continue を忘れない
$N=20$ でメモリ不足dp 配列が $2^{20} \times 20 \times 8$ byte ≈ 160MBint で省メモリ化 or $N \le 20$ を確認

次のステップ

  • 発展問題: $N \le 25$ への拡張(分割 DP or 近似アルゴリズム)
  • 応用: DNA アセンブリ問題(Overlap-Layout-Consensus の核心)
  • 理論: セットカバーとの関係(多項式時間近似不可能性・APX-hard)

自己評価

自分の回答

気づき・メモ