メインコンテンツまでスキップ

ハミルトニアン経路問題:美しいが困難なアプローチ

🎯 この講義で学ぶこと

最終ゴール: ゲノムアセンブリをグラフ理論の問題として理解し、なぜハミルトニアン経路アプローチが魅力的だが実用的でないかを学ぶ

でも、ちょっと待ってください。「ハミルトニアン」って誰?なぜゲノムと関係があるの?

👨‍🔬 ステップ0:ウィリアム・ハミルトンと彼のゲーム

偉大な数学者のパズル

1857年、アイルランドの数学者ウィリアム・ローワン・ハミルトンは「Icosian Game」という数学パズルを発明しました。

ルール: すべての都市をちょうど1回ずつ訪問する経路を見つけよ

🧬 ステップ1:ゲノムアセンブリとグラフ理論の出会い

1-1. k-merをノードとして表現

def visualize_kmer_as_nodes():
"""k-merをグラフのノードとして視覚化"""

genome = "TAATGGCATGGATGTT"
k = 3

# k-merを抽出
kmers = []
for i in range(len(genome) - k + 1):
kmers.append(genome[i:i+k])

print(f"元のゲノム: {genome}")
print(f"長さ: {len(genome)}")
print()
print("k-mer(ノード):")
for i, kmer in enumerate(kmers):
print(f" ノード{i:2}: {kmer}")

print()
print("観察:")
print("• ゲノム = すべてのノードを通る経路")
print("• 各k-merは元の位置順に並んでいる")
print("• でも実際は位置情報がない!")

visualize_kmer_as_nodes()

1-2. オーバーラップをエッジとして表現

def create_overlap_edges():
"""k-mer間のオーバーラップをエッジとして表現"""

kmers = ["TAA", "AAT", "ATG", "TGG", "GGC", "GCA", "CAT", "ATG", "TGG", "GGA", "GAT", "ATG", "TGT", "GTT"]
k = 3
overlap_len = k - 1 # 2文字の重複

edges = []
for i, kmer1 in enumerate(kmers):
suffix = kmer1[-overlap_len:]
for j, kmer2 in enumerate(kmers):
if i != j: # 自己ループは除く
prefix = kmer2[:overlap_len]
if suffix == prefix:
edges.append((kmer1, kmer2))

print("オーバーラップグラフのエッジ(一部):")
for edge in edges[:10]:
print(f" {edge[0]}{edge[1]} (重複: {edge[0][-2:]})")

print(f"\n合計: {len(edges)}本のエッジ")
print("\nこのグラフで、すべてのノードを1回ずつ訪問する経路がゲノム!")

create_overlap_edges()

🎮 ステップ2:ハミルトニアン経路問題として解く

2-1. 問題の定式化

ハミルトニアン経路問題(Hamiltonian Path Problem):

入力: 有向グラフ G = (V, E)
V: ノード(k-merの集合)
E: エッジ(オーバーラップ関係)

出力: すべてのノードをちょうど1回ずつ訪問する経路
(存在しない場合は「解なし」)

ゲノムアセンブリへの適用:
- 各k-mer = 1つのノード
- オーバーラップ = エッジ
- ゲノム = ハミルトニアン経路

2-2. 小さな例で実演

def hamiltonian_path_example():
"""小さな例でハミルトニアン経路を見つける"""

# 簡単な例
graph = {
"TAA": ["AAT"],
"AAT": ["ATG", "ATC"],
"ATG": ["TGC"],
"ATC": ["TCA"],
"TGC": ["GCA"],
"TCA": ["CAT"],
"GCA": [],
"CAT": []
}

def find_hamiltonian_path(graph, current, path, visited):
"""再帰的にハミルトニアン経路を探索"""
if len(path) == len(graph):
return path # すべてのノードを訪問した

for neighbor in graph.get(current, []):
if neighbor not in visited:
visited.add(neighbor)
result = find_hamiltonian_path(graph, neighbor, path + [neighbor], visited)
if result:
return result
visited.remove(neighbor)

return None

# すべてのノードから開始を試す
for start in graph:
visited = {start}
path = find_hamiltonian_path(graph, start, [start], visited)
if path:
print("ハミルトニアン経路を発見!")
print(" → ".join(path))

# ゲノムを再構築
genome = path[0]
for node in path[1:]:
genome += node[-1] # 最後の文字だけ追加
print(f"\n再構築されたゲノム: {genome}")
return

print("ハミルトニアン経路が存在しません")

hamiltonian_path_example()

😰 ステップ3:なぜハミルトニアン経路は難しいのか?

3-1. 計算複雑性の悪夢

def complexity_nightmare():
"""ハミルトニアン経路問題の計算複雑性"""

import math

print("ハミルトニアン経路問題の複雑さ:")
print()

# ノード数と可能な経路数
for n in [5, 10, 20, 50, 100]:
# 最悪の場合、すべての順列をチェック
possibilities = math.factorial(n)

# 1秒に10億経路をチェックできるとして
seconds = possibilities / 1_000_000_000

if seconds < 60:
time_str = f"{seconds:.2f}秒"
elif seconds < 3600:
time_str = f"{seconds/60:.2f}分"
elif seconds < 86400:
time_str = f"{seconds/3600:.2f}時間"
elif seconds < 31536000:
time_str = f"{seconds/86400:.2f}日"
else:
years = seconds / 31536000
if years < 1000000:
time_str = f"{years:.2e}年"
else:
time_str = "宇宙の年齢を超える"

print(f" {n:3}ノード: {possibilities:.2e}通り → {time_str}")

print()
print("NP完全問題: 効率的なアルゴリズムは知られていない!")

complexity_nightmare()

3-2. NP完全問題とは?

def explain_np_complete():
"""NP完全問題の説明"""

print("NP完全問題の特徴:")
print()
print("1. 解の検証は簡単(多項式時間)")
print(" → 経路が与えられれば、それがハミルトニアン経路か")
print(" どうかはすぐ確認できる")
print()
print("2. 解を見つけるのは困難(指数時間)")
print(" → すべての可能性を試す必要がある")
print()
print("3. 他のNP完全問題に変換可能")
print(" → 巡回セールスマン問題、グラフ彩色問題など")
print()

# 視覚的な例
print("例: たった10個のk-merでも...")
print("```")
print(" 開始")
print(" ╱ │ ╲")
print(" ○ ○ ○ ← 10通りの選択")
print(" ╱│╲ ╱│╲ ╱│╲")
print(" ○○○ ○○○ ○○○ ← 各9通りの選択")
print(" ...")
print("```")
print(f"合計: 10! = 3,628,800通りの経路")

explain_np_complete()

🎯 ステップ4:ハミルトニアン経路の実際の困難さ

4-1. 実際のゲノムでの問題規模

def real_genome_difficulty():
"""実際のゲノムでハミルトニアン経路を見つける困難さ"""

import math

scenarios = [
("小さなウイルス", 5000, 10),
("大腸菌の一部", 10000, 100),
("大腸菌全体", 4600000, 100),
("ヒト1染色体", 50000000, 100)
]

print("実際のゲノムでのハミルトニアン経路問題:")
print()

for organism, genome_size, k in scenarios:
num_kmers = genome_size - k + 1

# 簡略化: ユニークなk-merの数を推定
unique_kmers = min(num_kmers, 4**k) # 最大4^k種類

print(f"{organism} (ゲノムサイズ: {genome_size:,}):")
print(f" k-mer数: 約{num_kmers:,}")
print(f" ユニークk-mer数(推定): 約{unique_kmers:,}")

if unique_kmers <= 20:
possibilities = math.factorial(unique_kmers)
print(f" 可能な経路数: {possibilities:,}")
else:
log_possibilities = unique_kmers * math.log10(unique_kmers)
print(f" 可能な経路数: 約10^{int(log_possibilities)}")

print(f" → {'解ける' if unique_kmers < 15 else '現実的に不可能'}")
print()

real_genome_difficulty()

4-2. さらなる問題:複数解の存在

def multiple_solutions_problem():
"""複数のハミルトニアン経路が存在する問題"""

# 同じk-mer集合から複数のゲノムが可能
kmers = ["AT", "TG", "GC", "CA", "AT"] # ATが2回

print("k-mer集合: ", kmers)
print()
print("可能なハミルトニアン経路:")
print()

# 可能な配列1
path1 = ["AT", "TG", "GC", "CA", "AT"]
genome1 = "ATGCAT"
print(f"経路1: {' → '.join(path1)}")
print(f"ゲノム1: {genome1}")
print()

# 可能な配列2(ATの位置を入れ替え)
path2 = ["AT", "TG", "GC", "CA", "AT"]
genome2 = "ATGCAT" # 同じに見えるが...

# 実際は異なる構造の可能性
print("しかし、リピートやエラーがあると:")
kmers_with_error = ["AT", "TG", "GG", "GC", "CA", "AT"]
print(f"k-mer(エラー含む): {kmers_with_error}")
print("→ 複数の異なるゲノムが可能!")
print()
print("問題: どれが正しいゲノムか判断できない")

multiple_solutions_problem()

🔄 ステップ5:なぜグラフ理論のアプローチなのか?

5-1. グラフ理論の美しさ

def graph_theory_beauty():
"""グラフ理論アプローチの利点と欠点"""

print("グラフ理論アプローチの魅力:")
print()
print("✅ 利点:")
print(" • 問題が明確に定式化される")
print(" • 数学的に厳密")
print(" • 既存のグラフアルゴリズムを活用可能")
print(" • 視覚的に理解しやすい")
print()
print("❌ ハミルトニアン経路の欠点:")
print(" • NP完全問題(効率的な解法なし)")
print(" • 実用的なサイズのゲノムでは解けない")
print(" • 近似解法も品質保証が困難")
print()
print("💡 必要なもの:")
print(" → もっと効率的に解けるグラフ問題への変換")
print(" → それがオイラー経路!(次回)")

graph_theory_beauty()

5-2. ハミルトニアンからオイラーへの道

def path_to_euler():
"""なぜオイラー経路が解決策になるか"""

print("2つのグラフ問題の比較:")
print()
print("ハミルトニアン経路:")
print(" • すべての頂点を1回ずつ訪問")
print(" • NP完全(難しい)")
print(" • 効率的なアルゴリズムなし")
print()
print("オイラー経路:")
print(" • すべての辺を1回ずつ通過")
print(" • 多項式時間で解ける(簡単)")
print(" • 効率的なアルゴリズムあり")
print()
print("革命的なアイデア:")
print("```")
print("k-merをノードにする代わりに...")
print("(k-1)-merをノードにして、")
print("k-merをエッジにする!")
print("```")
print()
print("これでハミルトニアン → オイラーに変換!")

path_to_euler()

🎯 まとめ:今日学んだことを整理

レベル1:基礎理解

  • ハミルトニアン経路: すべてのノードを1回ずつ訪問
  • k-mer = ノード、オーバーラップ = エッジ
  • ゲノム = ハミルトニアン経路

レベル2:問題の理解

  • NP完全問題: 効率的な解法が知られていない
  • ノード数が増えると指数的に難しくなる
  • 20個程度のノードでも現実的に解けない

レベル3:実践的な課題

  • 実際のゲノム: 数百万のk-mer
  • 複数解の存在(どれが正しいか不明)
  • より良いアプローチが必要 → オイラー経路

🚀 次回予告

次回は、革命的な解決策を学びます:

  • De Bruijnグラフ: k-merをエッジとして扱う
  • オイラー経路問題: 効率的に解ける!
  • 実際のゲノムアセンブラーの仕組み

「不可能を可能にする」数学的トリックをお楽しみに!