オイラーの定理:アリのアルゴリズム
🎯 この講義で学ぶこと
最終ゴール: オイラーサイクルの存在条件を理解し、それを見つけるアルゴリズムを学ぶ
でも、ちょっと待ってください。どんなグラフでもオイラーサイクルが存在するわけではないのでは?
🌉 ステップ0:オイラーサイクル vs オイラー経路
サイクルと経路の違い
def cycle_vs_path():
"""オイラーサイクルとオイラー経路の違い"""
print("オイラーサイクル:")
print(" • すべてのエッジを1回ずつ通過")
print(" • 開始点と終了点が同じ")
print(" • ケーニヒスベルクの元の問題")
print()
print("オイラー経路:")
print(" • すべてのエッジを1回ずつ通過")
print(" • 開始点と終了点が異なってもよい")
print(" • より一般的な問題")
print()
print("重要: サイクルを見つける方法を学べば、")
print(" 経路を見つける方法もすぐに分かる!")
cycle_vs_path()
🔍 ステップ1:オイラーサイクルの存在条件
1-1. バランスの取れたグラフ
def check_balanced_graph():
"""グラフがバランスしているかチェック"""
# 例のグラフ(隣接リスト)
graphs = [
{
'A': ['B'],
'B': ['C'],
'C': [] # 出口なし
},
{
'A': ['B', 'C'],
'B': ['C'],
'C': ['A'] # 入力1、出力1
},
{
'A': ['B', 'B'],
'B': ['C', 'C'],
'C': ['A', 'A'] # すべてバランス
}
]
for i, graph in enumerate(graphs, 1):
print(f"グラフ{i}:")
# 入次数と出次数を計算
in_degree = {}
out_degree = {}
for node in graph:
out_degree[node] = len(graph[node])
in_degree[node] = 0
for node in graph:
for neighbor in graph[node]:
in_degree[neighbor] = in_degree.get(neighbor, 0) + 1
# バランスをチェック
balanced = True
for node in set(list(in_degree.keys()) + list(out_degree.keys())):
ind = in_degree.get(node, 0)
outd = out_degree.get(node, 0)
print(f" {node}: 入次数={ind}, 出次数={outd}", end="")
if ind != outd:
print(" ❌ アンバランス")
balanced = False
else:
print(" ✓")
if balanced:
print(" → バランスが取れている(オイラーサイクルの可能性あり)")
else:
print(" → バランスが取れていない(オイラーサイクル不可能)")
print()
check_balanced_graph()
1-2. なぜバランスが必要か
def why_balance_needed():
"""なぜバランスが必要か視覚的に説明"""
print("オイラーサイクルでノードを訪問するとき:")
print("=" * 50)
print()
print("ノードAを考える:")
print(" 入ってくる → 出ていく")
print(" 入ってくる → 出ていく")
print(" 入ってくる → 出ていく")
print()
print("観察:")
print("• ノードに入る回数 = ノードから出る回数")
print("• だから: 入次数 = 出次数")
print()
print("もし入次数 ≠ 出次数なら:")
print("• 入次数 > 出次数: 入ったまま出られない!")
print("• 入次数 < 出次数: すべてのエッジを使えない!")
print()
print("→ オイラーサイクルは不可能")
why_balance_needed()
🐜 ステップ2:アリのアルゴリズム
2-1. アリの散歩戦略
def ant_algorithm_concept():
"""アリのアルゴリズムの概念"""
print("アリのランダムウォーク:")
print("=" * 50)
print()
print("ルール:")
print("1. アリはグラフをランダムに歩く")
print("2. 同じエッジを2度通らない")
print("3. 行き止まりになったら?")
print()
print("重要な観察:")
print("• バランスの取れたグラフでは...")
print("• アリは開始ノード以外で行き止まりにならない!")
print()
print("理由:")
print("• 各ノード(開始ノード以外)で:")
print(" 入るエッジ数 = 出るエッジ数")
print("• だから、入ったら必ず出られる")
print("• 開始ノードだけは最初に1つエッジを使用済み")
ant_algorithm_concept()
2-2. アリのアルゴリズム実装
def ant_algorithm_step_by_step():
"""アリのアルゴリズムをステップバイステップで実装"""
# 簡単なバランスの取れたグラフ
graph = {
'A': ['B', 'C'],
'B': ['C', 'D'],
'C': ['A', 'D'],
'D': ['A', 'B']
}
import copy
print("グラフ:")
for node, edges in graph.items():
print(f" {node} → {edges}")
print()
def ant_walk(g, start):
"""アリの一回の散歩"""
graph_copy = copy.deepcopy(g)
path = [start]
current = start
while current in graph_copy and graph_copy[current]:
# ランダムに(ここでは最初の)エッジを選択
next_node = graph_copy[current].pop(0)
path.append(next_node)
current = next_node
return path
# Step 1: 最初の散歩
print("Step 1: アリの最初の散歩")
print("-" * 30)
path1 = ant_walk(graph, 'A')
print(f"経路: {' → '.join(path1)}")
if path1[0] == path1[-1]:
print("✓ 開始ノードに戻った!")
else:
print("❌ 開始ノードに戻らなかった")
# 使用したエッジを記録
used_edges = []
for i in range(len(path1) - 1):
used_edges.append((path1[i], path1[i+1]))
print(f"使用したエッジ: {len(used_edges)}")
print(f"全エッジ数: {sum(len(e) for e in graph.values())}")
if len(used_edges) < sum(len(e) for e in graph.values()):
print("\n→ まだ未使用のエッジがある!")
print(" 未使用エッジがあるノードから再開...")
ant_algorithm_step_by_step()
🔄 ステップ3:完全なアリのアルゴリズム
3-1. 反復的な改良
def complete_ant_algorithm():
"""完全なアリのアルゴリズム(Hierholzerのアルゴリズム)"""
def find_eulerian_cycle(graph):
"""オイラーサイクルを見つける"""
import copy
# グラフをコピー
g = copy.deepcopy(graph)
# すべてのエッジを使うまで繰り返す
def find_cycle_from(start):
"""指定ノードからサイクルを見つける"""
cycle = [start]
current = start
while current in g and g[current]:
next_node = g[current].pop(0)
cycle.append(next_node)
current = next_node
return cycle
# 任意のノードから開始
start_node = next(iter(graph))
main_cycle = find_cycle_from(start_node)
# すべてのエッジを使うまで繰り返す
while any(g[node] for node in g):
# 未使用エッジがあるノードを探す
for i, node in enumerate(main_cycle[:-1]):
if node in g and g[node]:
# このノードから新しいサイクルを見つける
sub_cycle = find_cycle_from(node)
# メインサイクルに統合
main_cycle = main_cycle[:i] + sub_cycle + main_cycle[i+1:]
break
return main_cycle
# テスト
graph = {
'00': ['01', '10'],
'01': ['10', '11'],
'10': ['00', '11'],
'11': ['00', '01']
}
print("グラフ(バイナリ2-mer):")
for node, edges in graph.items():
print(f" {node} → {edges}")
print()
cycle = find_eulerian_cycle(graph)
print("オイラーサイクル:")
print(" → ".join(cycle))
# 検証
edge_count = sum(len(edges) for edges in graph.values())
print(f"\n検証:")
print(f" エッジ数: {edge_count}")
print(f" サイクルの長さ: {len(cycle) - 1}")
print(f" {'✓ 正しい' if len(cycle) - 1 == edge_count else '❌ 間違い'}")
complete_ant_algorithm()
3-2. アルゴリズムの視覚化
def visualize_ant_algorithm():
"""アリのアルゴリズムを視覚的に説明"""
print("アリのアルゴリズムの動作:")
print("=" * 50)
print()
# 反復を表示
iterations = [
{
"iteration": 1,
"start": "A",
"path": ["A", "B", "C", "A"],
"unused": ["A→D", "D→A"],
"note": "最初のサイクル(不完全)"
},
{
"iteration": 2,
"start": "A(未使用エッジあり)",
"path": ["A", "B", "C", "A", "D", "A"],
"unused": [],
"note": "完全なオイラーサイクル!"
}
]
for it in iterations:
print(f"反復 {it['iteration']}:")
print(f" 開始: {it['start']}")
print(f" 経路: {' → '.join(it['path'])}")
if it['unused']:
print(f" 未使用: {', '.join(it['unused'])}")
print(f" {it['note']}")
print()
print("アルゴリズムの要点:")
print("1. サイクルを見つける")
print("2. 未使用エッジがあるノードを探す")
print("3. そのノードから新しいサイクルを見つける")
print("4. サイクルを統合")
print("5. すべてのエッジを使うまで繰り返す")
visualize_ant_algorithm()
📐 ステップ4:オイラーの定理
4-1. 定理の記述
def euler_theorem():
"""オイラーの定理の正式な記述"""
print("オイラーの定理(1736年):")
print("=" * 50)
print()
print("【定理】")
print("連結な有向グラフGについて、")
print("Gがオイラーサイクルを持つ")
print("⇔")
print("Gがバランスしている(すべてのノードで入次数=出次数)")
print()
print("【系】オイラー経路について")
print("Gがオイラー経路を持つ")
print("⇔")
print("・入次数≠出次数のノードが最大2個")
print("・その場合、1つは入次数=出次数-1(開始点)")
print("・もう1つは入次数=出次数+1(終了点)")
print()
print("【計算複雑性】")
print("• 判定: O(V + E) - 次数を数えるだけ")
print("• 構築: O(E) - 各エッジを1回ずつ訪問")
euler_theorem()
4-2. 定理の証明(アリによる)
def proof_by_ant():
"""アリによるオイラーの定理の証明"""
print("オイラーの定理の証明(アリによる):")
print("=" * 50)
print()
print("【必要条件の証明】オイラーサイクル → バランス")
print("-" * 40)
print("オイラーサイクルが存在すると仮定")
print("→ 各ノードを訪問するとき:")
print(" ・入ってくる(エッジを1本使用)")
print(" ・出ていく(エッジを1本使用)")
print("→ 入る回数 = 出る回数")
print("→ 入次数 = 出次数")
print("∴ グラフはバランスしている ✓")
print()
print("【十分条件の証明】バランス → オイラーサイクル")
print("-" * 40)
print("グラフがバランスしていると仮定")
print("アリのアルゴリズム:")
print("1. アリは任意のノードから歩き始める")
print("2. バランスのため、開始ノード以外で行き止まりにならない")
print("3. 必ず開始ノードに戻る(サイクル完成)")
print("4. 未使用エッジがあれば、そこから再開")
print("5. すべてのエッジを使うまで繰り返す")
print("∴ オイラーサイクルが構築できる ✓")
proof_by_ant()
🎯 まとめ:今日学んだことを整理
レベル1:基礎理解
- バランスしたグラフ: すべてのノードで入次数=出次数
- オイラーサイクル: すべてのエッジを1回ずつ通るサイクル
- バランス ⇔ オイラーサイクルの存在
レベル2:アルゴリズム理解
- アリのアルゴリズム(Hierholzerのアルゴリズム)
- 開始ノードでしか行き止まりにならない
- サイクルを見つけて統合を繰り返す
レベル3:実践的意義
- 線形時間O(E)で解ける
- ゲノムアセンブリへの直接応用
- De Bruijnグラフでのオイラーサイクル探索
🚀 次回予告
次回は、実際のゲノムアセンブリでの課題を学びます:
- リード誤りの処理
- リピート配列の問題
- ペアエンドリードの活用
「理想から現実へ」- 実際のゲノムプロジェクトの課題をお楽しみに!