Day 110-Q4 — 彩色数(独立集合の部分集合DPによる厳密計算)

2026-08-02 赤色 Master / Phase 8+ ★★★★★★★★★ bitmask DP + 部分集合列挙 O(3^N)

問題

$N$頂点$M$辺の単純無向グラフの彩色数(隣接頂点が異なる色になる最小の色数)を求めよ。

入力形式

N M
u_1 v_1
...
u_M v_M

制約

$1 \le N \le 15$
$0 \le M \le N(N-1)/2$
多重辺なし

入出力例

入力例1

4 4
1 2
2 3
3 4
4 1

出力例1

2

4サイクルは偶数長→二部グラフ→2色で塗れる(頂点1,3をA、2,4をB)。

入力例2

4 3
1 2
2 3
1 3

出力例2

3

頂点1,2,3は三角形(クリーク)なので3色必要。頂点4は孤立。

概念図: グラフを独立集合の層に分割する

4サイクル 1-2-3-4-1 を2つの独立集合に分割 1 2 3 4 色A = {1,3}(独立集合) 色B = {2,4}(独立集合) dp[S] = Sを塗るのに必要な最小色数 = 1 + min(dp[S\T]) (Tは独立部分集合)

ヒント(段階的開示)

ヒント1: 方向性
「$k$色で塗り分け可能か」を$k=1,2,\dots$と増やして初めて可能になった$k$を答えとする。この判定をどう高速化するかが本質。
ヒント2: アプローチ
$k$色で塗れることは、頂点集合を高々$k$個の独立集合に分割できることと同値。$dp[S]=1+\min_{T\subseteq S,\ T\text{は独立集合}}dp[S\setminus T]$ という部分集合DPで解ける。独立集合判定は$O(2^N)$で前計算でき、部分集合の部分集合列挙は全体で$O(3^N)$。$N\le15$なら$3^{15}\approx1.4\times10^7$で十分高速。
ヒント3: 誘導(コード骨格)
is_independent = [False] * (1 << n)
is_independent[0] = True
for S in range(1, 1 << n):
    low = S & (-S)
    v = low.bit_length() - 1
    rest = S ^ low
    is_independent[S] = is_independent[rest] and (adj_mask[v] & rest) == 0

dp = [INF] * (1 << n); dp[0] = 0
for S in range(1, 1 << n):
    T = S
    while T > 0:
        if is_independent[T]:
            dp[S] = min(dp[S], dp[S ^ T] + 1)
        T = (T - 1) & S

模範解答 (Python)

import sys

def solve():
    data = sys.stdin.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    m = int(data[idx]); idx += 1

    adj_mask = [0] * n
    for _ in range(m):
        u = int(data[idx]) - 1; idx += 1
        v = int(data[idx]) - 1; idx += 1
        adj_mask[u] |= (1 << v)
        adj_mask[v] |= (1 << u)

    full = 1 << n

    is_independent = [False] * full
    is_independent[0] = True
    for S in range(1, full):
        low = S & (-S)
        v = low.bit_length() - 1
        rest = S ^ low
        is_independent[S] = is_independent[rest] and (adj_mask[v] & rest) == 0

    INF = float("inf")
    dp = [INF] * full
    dp[0] = 0
    for S in range(1, full):
        best = INF
        T = S
        while T > 0:
            if is_independent[T] and dp[S ^ T] + 1 < best:
                best = dp[S ^ T] + 1
            T = (T - 1) & S
        dp[S] = best

    print(dp[full - 1])

solve()
計算量: 独立集合判定の前計算 $O(2^N)$、部分集合DP本体 $O(3^N)$。$N\le15$で$3^{15}\approx1.4\times10^7$。入力例1(4-サイクル→2)、入力例2(三角形+孤立点→3)と一致することを確認済み。ランダム小規模グラフ($N\le6$)での全数塗り分け探索とも一致。

Step-by-Step 解説

1彩色数=独立集合への最小分割数
$k$色で塗る=頂点集合を$k$個の独立集合(同色グループ)に分割することと同値。
2独立集合判定を高速に前計算する
最下位ビット$v$を除いた残りが独立集合、かつ残りに$v$と隣接する頂点がないことと同値な漸化式で埋める。
3部分集合の部分集合を列挙する定番イディオム
T=S; while T>0: ...; T=(T-1)&Sで$S$の空でない部分集合を全列挙する。
4DPの意味
$dp[S]$は集合$S$だけを塗るのに必要な最小色数。最後に塗った色(独立集合$T$)を全探索する。
5全頂点集合に対するDP値が答え
dp[full-1]がグラフ全体の彩色数。

よくあるミス

ミス原因正しい書き方
部分集合の部分集合列挙で無限ループT>=0として空集合の後にオーバーフロー的挙動を起こすwhile T>0とする
最下位ビットの取り出し方を誤るS.bit_length()-1(最上位ビット)と混同low=S&(-S)で最下位ビットを取りlow.bit_length()-1でインデックス化
$O(3^N)$を$O(4^N)$等と誤解し制約設計を誤る部分集合の部分集合列挙が$O(3^N)$になる理由(二項定理)を理解していない$\sum_S 2^{|S|}=3^N$の関係を踏まえて$N$の上限を設計する
隣接情報をリストのリストで持ちビット演算の速さを活かせないビットマスク隣接リストの利点を理解していない隣接情報は整数のビットマスクとして持ちAND演算で判定する

次のステップ

  • 発展: Björklund–Husfeldt–Koivisto の包除原理による$O(2^N N)$彩色数計算に挑戦する
  • 発展: 塗り分け方の総数(彩色多項式)を数える拡張に取り組む
  • 発展: 貪欲法+DSATURによる近似彩色と厳密解の差を比較する

自己評価

自分の回答

気づき・メモ