Day 070-Q1 — 弦グラフ・完全消去順序 + 木幅(Chordal Graph / PEO)

2026-06-23 赤色 Master / Phase 8+ ★★★★★★★★★ MCS・PEO検証・木幅計算

問題

$N$ 頂点 $M$ 辺の無向グラフ $G$ に対して以下の3つのクエリを順に処理せよ。

  • クエリ1: $G$ が弦グラフか判定する。弦グラフとは、長さ4以上のすべての単純サイクルに少なくとも1本の弦が存在するグラフ。
  • クエリ2: 弦グラフであれば完全消去順序(PEO)を出力。PEO とは頂点の順列 $v_1,\ldots,v_N$ で、各 $v_i$ の右隣接集合 $R(v_i)$ が完全グラフをなすもの。
  • クエリ3: 木幅 $= \max_i |R(v_i)|$ を出力。

制約

パラメータ範囲
$N$$1 \le N \le 2000$
$M$$0 \le M \le N(N-1)/2$
頂点番号$1 \le u, v \le N$(自己ループ・多重辺なし)

入出力例

入力例 1

6 9
1 2
1 3
2 3
2 4
3 4
3 5
4 5
4 6
5 6

出力例 1

YES
6 5 4 3 2 1
2

入力例 2

4 4
1 2
2 3
3 4
4 1

出力例 2

NO

入力例2は 4-サイクル(1-2-3-4-1)に弦がないため非弦グラフ。

概念図: MCS と PEO

弦グラフ判定: Maximum Cardinality Search (MCS) 貪欲に「訪問済み隣接頂点数が最大」の頂点を選ぶ → 逆順が PEO 候補 → 実際に PEO か検証 検証: 各 v_i の右隣接集合の最左頂点 w が R(v_i)\{w} を全て隣接しているか確認 入力例1のグラフ(弦グラフ) 1 2 3 4 5 6 PEO 検証プロセス(入力例1) 順序: 6 → 5 → 4 → 3 → 2 → 1 v₁=6: R(6)={5,4} → {4,5}は互いに隣接 ✓ |R|=2 v₂=5: R(5)={4,3} → {3,4}は互いに隣接 ✓ |R|=2 v₃=4: R(4)={3,2} → {2,3}は互いに隣接 ✓ |R|=2 v₄=3: R(3)={2,1} → {1,2}は互いに隣接 ✓ |R|=2 木幅 = max|R(v_i)| = 2 ✓

ヒント(段階的開示)

ヒント1: 方向性
弦グラフの判定には Maximum Cardinality Search(MCS)を使う。MCS は貪欲に「既訪問頂点との隣接数が最大の頂点」を選ぶアルゴリズムで、弦グラフであれば逆順が PEO になる。
ヒント2: アプローチ
  1. MCS でPEO候補 $v_1, \ldots, v_N$ を生成
  2. 各 $v_i$ の右隣接集合 $R(v_i)$ を計算
  3. $R(v_i)$ 内で最も pos の小さい頂点 $w$ について、$R(v_i) \setminus \{w\}$ が $w$ の隣接集合の部分集合か確認
  4. 条件が成立すれば弦グラフ
ヒント3: コード骨格
# MCS による PEO 候補生成
cnt = [0] * (N + 1)  # 訪問済み隣接数
visited = [False] * (N + 1)
order = []
for _ in range(N):
    v = max((cnt[i], i) for i in range(1,N+1) if not visited[i])[1]
    visited[v] = True
    order.append(v)
    for u in adj[v]:
        if not visited[u]:
            cnt[u] += 1
peo = order[::-1]

# PEO 検証
pos = {v: i for i, v in enumerate(peo)}
for i, v in enumerate(peo):
    R = [u for u in adj[v] if pos[u] > i]
    if R:
        w = min(R, key=lambda u: pos[u])
        if not set(R) - {w} <= adj[w]:
            print("NO"); return

模範解答 (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 = [set() for _ in range(N + 1)]
    for _ in range(M):
        u = int(data[idx]); idx += 1
        v = int(data[idx]); idx += 1
        adj[u].add(v)
        adj[v].add(u)

    # MCS で PEO 候補を取得 O(N^2)
    cnt = [0] * (N + 1)
    visited = [False] * (N + 1)
    order = []
    for _ in range(N):
        v = -1; best = -1
        for i in range(1, N + 1):
            if not visited[i] and cnt[i] > best:
                best = cnt[i]; v = i
        visited[v] = True
        order.append(v)
        for u in adj[v]:
            if not visited[u]:
                cnt[u] += 1
    peo = order[::-1]

    # PEO 検証
    pos = [0] * (N + 1)
    for i, v in enumerate(peo):
        pos[v] = i

    is_chordal = True
    treewidth = 0
    for i, v in enumerate(peo):
        R = [u for u in adj[v] if pos[u] > i]
        treewidth = max(treewidth, len(R))
        if R:
            w = min(R, key=lambda u: pos[u])
            if not (set(R) - {w}).issubset(adj[w]):
                is_chordal = False; break

    if not is_chordal:
        print("NO")
    else:
        print("YES")
        print(*peo)
        print(treewidth)

solve()

Step-by-Step 解説

Step 1: 弦グラフの特徴

弦グラフは以下の等価条件を持つ: 長さ4以上のサイクルに弦が存在完全消去順序 (PEO) が存在木幅 = 最大クリークサイズ - 1

Step 2: MCS アルゴリズム

贪欲に「訪問済み隣接頂点数が最大」の頂点を選ぶ。時間計算量 $O(N^2)$(バケットキューで $O(N+M)$ まで高速化可能)。

Step 3: PEO 検証条件

PEO 候補 $v_1,\ldots,v_N$ が真の PEO か確認: 各 $v_i$ の右隣接集合 $R(v_i)$ の最左頂点 $w$ に対し、$R(v_i) \setminus \{w\}$ が $\mathrm{adj}(w)$ の部分集合である必要がある。これが成立 ⟺ $R(v_i)$ は完全グラフ。

Step 4: 木幅の計算

弦グラフの木幅 $= \max_i |R(v_i)|$(最大クリークサイズ - 1 に等しい)。

Step 5: 計算量分析

処理計算量
MCS(シンプル)$O(N^2)$
PEO 検証$O(N + M)$
木幅計算$O(N + M)$
全体$O(N^2)$($N \le 2000$ で十分)

よくあるミス

ミス原因正しい書き方
PEO の向きを間違えるMCS の出力はそのままではなく逆順が PEOorder.reverse() を忘れない
$R(v_i)$ の完全グラフ検証の条件$w$ の全隣接ではなく $R(v_i)\setminus\{w\}$ が $\text{adj}(w)$ の部分集合(set(R) - {w}).issubset(adj[w])
弦グラフでない場合に続けて処理早期リターンの欠如if not is_chordal: print("NO"); return
adj を list で持ち部分集合チェックが O(M)issubset が $O(|R| \cdot |adj|)$ になるadjset で持つ

次のステップ

  • 発展問題: 弦グラフの最大重みクリーク探索(PEO 順に貪欲 DP)
  • k-tree からの弦グラフ生成と木分解構築
  • 弦グラフ上のグラフ彩色(クリーク数 = 彩色数が成立)
  • 一般グラフの木幅計算(NP困難・FPTアルゴリズム)

自己評価