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

De Bruijnグラフ:ゲノムなしでグラフを構築

🎯 この講義で学ぶこと

最終ゴール: k-mer構成から直接De Bruijnグラフを構築し、ユニバーサル文字列問題との関連を理解する

でも、ちょっと待ってください。前回はゲノムから作ったけど、実際はゲノムが分からないのでは?

🤔 ステップ0:重要な気づき

私たちの「浮気」を暴く

前回の講義で、私たちはDe Bruijnグラフをゲノムから作成しました。しかし...

def reveal_the_cheat():
"""前回の「浮気」を明らかにする"""

print("前回のアプローチ:")
print("1. ゲノムがあった(知っていた)")
print("2. ゲノムからk-merを抽出")
print("3. De Bruijnグラフを構築")
print()
print("でも実際は:")
print("1. ゲノムは不明!")
print("2. 分かるのはリード(k-mer)だけ")
print("3. k-merからゲノムを再構築する必要がある")
print()
print("質問: k-mer構成だけからDe Bruijnグラフを作れるか?")
print("答え: できる!(これから証明)")

reveal_the_cheat()

📚 ステップ1:k-mer構成からDe Bruijnグラフを構築

1-1. 2つのアプローチの比較

def compare_approaches():
"""ゲノムからvsゲノム構成からのグラフ構築"""

# 例のゲノムとk-mer
genome = "TAATGCCATGGGATGTT"
kmers = ["TAA", "AAT", "ATG", "TGC", "GCC", "CCA", "CAT",
"ATG", "TGG", "GGA", "GAT", "ATG", "TGT", "GTT"]

print("=== アプローチ1: ゲノムから(前回)===")
print(f"ゲノム: {genome}")
print("1. ゲノムを経路として表現")
print("2. k-merをエッジとして配置")
print("3. 同じラベルのノードを接着")
print()

print("=== アプローチ2: k-mer構成から(今回)===")
print(f"k-mer構成: {kmers}")
print("1. 各k-merをエッジとして表現")
print("2. 各エッジの端点を(k-1)-merでラベル付け")
print("3. 同じラベルのノードを接着")
print()
print("結果: 同じDe Bruijnグラフができる!")

compare_approaches()

1-2. ステップバイステップの構築

def build_from_kmers_step_by_step():
"""k-mer構成からDe Bruijnグラフを構築する詳細プロセス"""

kmers = ["TAA", "AAT", "ATG", "TGC", "GCC"]

print("Step 1: 各k-merをエッジとして表現")
print("-" * 40)

edges = []
for kmer in kmers:
prefix = kmer[:-1] # 最初のk-1文字
suffix = kmer[1:] # 最後のk-1文字
edges.append((prefix, suffix, kmer))
print(f"k-mer: {kmer} → エッジ: {prefix} --[{kmer}]--> {suffix}")

print("\nStep 2: 同じラベルのノードを識別")
print("-" * 40)

from collections import defaultdict
node_appearances = defaultdict(list)

for prefix, suffix, kmer in edges:
node_appearances[prefix].append(f"start of {kmer}")
node_appearances[suffix].append(f"end of {kmer}")

for node, appearances in sorted(node_appearances.items()):
if len(appearances) > 1:
print(f"'{node}': {len(appearances)}回出現")
for app in appearances:
print(f" - {app}")

print("\nStep 3: 同じラベルのノードを接着")
print("-" * 40)
print("接着後、De Bruijnグラフが完成!")

# 最終的なグラフ構造
graph = defaultdict(list)
for kmer in kmers:
prefix = kmer[:-1]
suffix = kmer[1:]
graph[prefix].append(suffix)

print("\n最終的なグラフ:")
for node, neighbors in sorted(graph.items()):
print(f" {node}{neighbors}")

build_from_kmers_step_by_step()

🔧 ステップ2:アルゴリズムの実装

2-1. シンプルな2行アルゴリズム

def de_bruijn_from_kmers(kmers):
"""k-mer構成からDe Bruijnグラフを構築する2行アルゴリズム"""

from collections import defaultdict

# 1行目: すべてのk-merを接頭辞と接尾辞の間のエッジとして表現
# 2行目: 同じラベルのノードを接着(defaultdictが自動的に処理)
graph = defaultdict(list)
for kmer in kmers:
graph[kmer[:-1]].append(kmer[1:])

return dict(graph)

# 例
kmers = ["AAT", "ATG", "TGC", "GCA", "CAT", "ATG", "TGG", "GGA"]
graph = de_bruijn_from_kmers(kmers)

print("De Bruijnグラフ(2行で構築!):")
for node, edges in sorted(graph.items()):
print(f" {node}{edges}")

2-2. 等価な定義

def equivalent_definitions():
"""De Bruijnグラフの2つの等価な定義"""

print("定義1: エッジとノードの構築")
print("-" * 40)
print("1. k-merを接頭辞と接尾辞の間のエッジとして表現")
print("2. 同じラベルのノードを接着")
print()

print("定義2: ノードとエッジの直接構築")
print("-" * 40)
print("1. すべての(k-1)-merからグラフのノードを形成")
print("2. 各k-merについて、接頭辞ノードと接尾辞ノードをエッジで接続")
print()
print("両方の定義は同じグラフを生成する!")

equivalent_definitions()

🎨 ステップ3:De Bruijnとユニバーサル文字列問題

3-1. De Bruijnの純粋数学的問題

def universal_string_problem():
"""ユニバーサル文字列問題の説明"""

print("De Bruijnの問題(1946年):")
print("=" * 50)
print()
print("すべてのバイナリk-merを1回だけ含む円形文字列を見つけよ")
print()

# k=3の例
k = 3
total_kmers = 2**k

print(f"k={k}の場合:")
print(f"バイナリ{k}-merの総数: {total_kmers}")

binary_kmers = []
for i in range(total_kmers):
kmer = format(i, f'0{k}b')
binary_kmers.append(kmer)

print(f"すべてのバイナリ{k}-mer:")
print(f" {binary_kmers}")
print()
print("ユニバーサル文字列: 00010111")
print("(円形なので最初と最後がつながる)")

# 検証
universal = "00010111"
print("\n検証:")
for i in range(len(universal)):
kmer = universal[i:i+3] if i <= 5 else universal[i:] + universal[:3-(8-i)]
print(f" 位置{i}: {kmer}")

universal_string_problem()

3-2. De Bruijnグラフによる解法

def solve_universal_string():
"""De Bruijnグラフを使ってユニバーサル文字列を構築"""

k = 3

# Step 1: すべてのバイナリk-merを生成
kmers = []
for i in range(2**k):
kmers.append(format(i, f'0{k}b'))

print(f"すべてのバイナリ{k}-mer: {kmers}")
print()

# Step 2: De Bruijnグラフを構築
from collections import defaultdict
graph = defaultdict(list)

for kmer in kmers:
prefix = kmer[:-1]
suffix = kmer[1:]
graph[prefix].append(suffix)

print("De Bruijnグラフ:")
for node, edges in sorted(graph.items()):
print(f" {node}{edges}")
print()

# Step 3: オイラーサイクルを見つける
def find_eulerian_cycle(graph):
"""簡単なオイラーサイクル探索"""
import copy
g = copy.deepcopy(graph)

cycle = []
stack = ['00'] # 任意のノードから開始

while stack:
v = stack[-1]
if v in g and g[v]:
u = g[v].pop(0)
stack.append(u)
else:
cycle.append(stack.pop())

return cycle[::-1]

cycle = find_eulerian_cycle(dict(graph))
print(f"オイラーサイクル: {' → '.join(cycle)}")

# Step 4: ユニバーサル文字列を構築
universal = cycle[0]
for node in cycle[1:-1]: # 最後のノードは最初と同じ
universal += node[-1]

print(f"\nユニバーサル文字列: {universal}")

# 検証
print("\n含まれるk-mer:")
for i in range(len(universal)):
kmer = universal[i:i+3] if i <= len(universal)-3 else universal[i:] + universal[:3-(len(universal)-i)]
print(f" {kmer}", end=" ")
print()

solve_universal_string()

🔮 ステップ4:より大きなkへの拡張

4-1. スケーラビリティの分析

def scalability_analysis():
"""より大きなkに対するユニバーサル文字列"""

print("ユニバーサル文字列のスケール:")
print("=" * 50)

for k in range(2, 11):
num_kmers = 2**k
string_length = 2**k # ユニバーサル文字列の長さ
num_nodes = 2**(k-1) # De Bruijnグラフのノード数

print(f"\nk={k}:")
print(f" バイナリk-mer数: {num_kmers}")
print(f" ユニバーサル文字列長: {string_length}")
print(f" De Bruijnグラフのノード数: {num_nodes}")

if k <= 5:
print(f" → 実用的に計算可能")
elif k <= 10:
print(f" → 計算可能だが時間がかかる")
else:
print(f" → メモリと時間の制約が厳しい")

scalability_analysis()

4-2. なぜDe Bruijnグラフは効率的か

def why_de_bruijn_efficient():
"""De Bruijnグラフの効率性の理由"""

print("De Bruijnグラフが効率的な3つの理由:")
print("=" * 50)
print()

print("1. ノード数の削減")
print(" • k-merの数に関わらず、最大4^(k-1)ノード")
print(" • 実際はもっと少ない(重複を接着)")
print()

print("2. オイラー経路の効率的な探索")
print(" • 線形時間O(E)で解ける")
print(" • ハミルトニアン経路(NP完全)と違い効率的")
print()

print("3. メモリ効率")
print(" • 隣接リスト表現で省メモリ")
print(" • 大規模なゲノムでも処理可能")
print()

# 具体例
print("例: ヒトゲノムの場合")
print("-" * 30)
genome_size = 3_000_000_000
k = 31 # 典型的なk値

print(f" ゲノムサイズ: {genome_size:,} bp")
print(f" k-mer長: {k}")
print(f" 理論最大ノード数: 4^{k-1} = 非常に大きい")
print(f" 実際のノード数: ゲノムサイズ程度(重複により大幅削減)")

why_de_bruijn_efficient()

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

レベル1:基礎理解

  • k-mer構成から直接De Bruijnグラフを構築できる
  • ゲノムを知らなくてもグラフを作れる
  • 2行の簡単なアルゴリズムで実装可能

レベル2:理論的背景

  • De Bruijnはユニバーサル文字列問題を解くためにこのグラフを考案
  • すべてのバイナリk-merを1回ずつ含む円形文字列
  • オイラーサイクルを見つけることで解決

レベル3:実践的意義

  • ゲノムアセンブリへの応用
  • 効率的なアルゴリズム(線形時間)
  • 大規模データにも対応可能

🚀 次回予告

次回は、オイラーサイクル/経路を効率的に見つけるアルゴリズムを学びます:

  • Hierholzerのアルゴリズム
  • グラフの条件とオイラー経路の存在
  • 実際のゲノムアセンブラーでの応用

「理論を実装へ」- アルゴリズムの詳細をお楽しみに!