Day 062-Q1 — オイラー標数・Genus計算(埋め込みグラフ / 回転システム)

2026-06-15 赤色 Master / Phase 8+ ★★★★★★★★★ 位相幾何 / Genus / Euler Characteristic / Rotation System

問題

$N$ 頂点 $M$ 辺の単純連結無向グラフ $G$ が与えられる。各頂点の回転システム(rotation system: 時計回りの隣接辺順序)が入力として与えられる。

以下を計算して出力せよ:

  • 面の数 $F$(回転システムから算出)
  • オイラー標数 $\chi = V - E + F$
  • 埋め込みの種数(Genus)$g$($\chi = 2 - 2g$ より)

制約

パラメータ範囲
$N$$3 \le N \le 1000$
$M$$N-1 \le M \le 3000$
グラフ連結・単純

入出力例

入力例 1

4 6
1 2
1 3
1 4
2 3
2 4
3 4
1: 2 3 4
2: 1 4 3
3: 1 2 4
4: 1 3 2

出力例 1

V=4 E=6 F=4 chi=2 g=0

$K_4$(完全グラフ4頂点)の平面埋め込み。オイラー標数 $\chi=2$、種数 $g=0$ → 平面グラフ。

概念図: Dart・回転システム・面の列挙

有向辺(Dart)と回転システム 1 2 3 4 面の列挙(Dart ループ) 各 dart = (u→v) から next_dart[(u,v)] = (v, w) を辿る (w = v の回転で u の次の頂点) Face 1: (1→2) → (2→4) → (4→1) → ... Face 2: (1→3) → (3→2) → (2→1) → ... Face 3 / 4: 残り dart から 2面を算出 $\chi = V - E + F = 4 - 6 + 4 = 2$ $g = (2 - \chi) / 2 = 0$ → 平面グラフ(球面への埋め込み)

ヒント(段階的開示)

ヒント1: 方向性
回転システム(combinatorial embedding)から面を列挙する。各無向辺を2本の有向辺(dart)に分解し、$\text{next\_dart}$ を定義する。
ヒント2: アプローチ
  • $\text{twin}(u,v) = (v,u)$
  • $\text{next\_dart}(u,v) = (v, w)$ ここで $w$ は $v$ の回転で $u$ の次の頂点
  • 面 = dart から $\text{next\_dart}$ をループして戻るまでのサイクル
  • 全 dart を列挙し未訪問から面を数える
ヒント3: コード骨格
next_dart = {}
for v in range(1, N+1):
    rot = rotation[v]
    for i, u in enumerate(rot):
        w = rot[(i+1) % len(rot)]
        next_dart[(u, v)] = (v, w)

visited = set(); F = 0
for start in next_dart:
    if start not in visited:
        F += 1
        d = start
        while d not in visited:
            visited.add(d); d = next_dart[d]

chi = N - M + F; g = (2 - chi) // 2

模範解答 (Python)

import sys
from collections import defaultdict
input = sys.stdin.readline

def main():
    line = input().split()
    N, M = int(line[0]), int(line[1])
    edges = []
    for _ in range(M):
        u, v = map(int, input().split())
        edges.append((u, v))

    rotation = defaultdict(list)
    for _ in range(N):
        line = input().split(':')
        v = int(line[0].strip())
        neighbors = list(map(int, line[1].split()))
        rotation[v] = neighbors

    twin = {}
    next_dart = {}
    for u, v in edges:
        twin[(u, v)] = (v, u)
        twin[(v, u)] = (u, v)

    for v in range(1, N + 1):
        rot = rotation[v]
        d = len(rot)
        for i, u in enumerate(rot):
            w = rot[(i + 1) % d]
            next_dart[(u, v)] = (v, w)

    all_darts = set(next_dart.keys())
    visited = set()
    F = 0
    for start in all_darts:
        if start not in visited:
            F += 1
            d = start
            while d not in visited:
                visited.add(d)
                d = next_dart[d]

    chi = N - M + F
    g = (2 - chi) // 2
    print(f"V={N} E={M} F={F} chi={chi} g={g}")

main()

Step-by-Step 解説

Step 1: dart の定義と回転システム

各無向辺 $(u,v)$ を $dart(u,v)$ と $dart(v,u)$ に分割(計 $2M$ 本)。頂点 $v$ の回転システム(隣接頂点の時計回り順)から $\text{next\_dart}[(u,v)] = (v,w)$ を定義する($w$ は $v$ の回転リストで $u$ の次の頂点)。

Step 2: 面の列挙

未訪問 dart からサイクルを辿る。各サイクルが1面に対応。計算量 $O(M)$。全 $2M$ dart を exactly 1 回ずつ訪問するため、面の総計は正確に $F$ となる。

Step 3: Euler 標数と種数

$\chi = V - E + F$。連結グラフでは $\chi = 2 - 2g$($g$ は向き付け可能曲面の種数)。平面グラフ($g=0$)では $\chi=2$(球面)、トーラス($g=1$)では $\chi=0$。

よくあるミス

ミス原因正しい書き方
next_dart の向きミスtwin と next を混同next_dart[(u,v)] = (v,w)
$g$ が分数になる非連結グラフに単純公式を適用$\chi = 2C - 2g$(C: 連結成分数)
訪問済み判定の漏れset に初期 dart がない全 dart を all_darts として生成後ループ

次のステップ

  • 発展問題: 非平面グラフ($K_5$, $K_{3,3}$)の最小 genus 計算(NP困難・近似法)
  • 関連: Kuratowski定理・平面グラフ双対・四色定理の計算機証明

自己評価