Mímisbrunnr知恵の泉

← ネットワーク科学 一覧

🎓 レベル:基礎 | 重要度:A(必須)

📎 前提:グラフとは | 関連:次数・道・連結成分グラフラプラシアンとスペクトル

要点(BLUF)

概念:同じグラフ、2つの持ち方

グラフとは で定義したグラフを計算機で扱うには、データ構造に落とす必要があります。代表的なのが隣接行列隣接リスト。どちらも同じグラフを表しますが、メモリ使用量と「ある操作の速さ」が真逆のトレードオフになります。

数式による定義:隣接行列

ノードを 1,,n1,\dots,n と番号づけたとき、隣接行列 AA は次で定義される n×nn \times n 行列です。

Aij={1(i,j)E0otherwiseA_{ij} = \begin{cases} 1 & (i,j) \in E \\ 0 & \text{otherwise} \end{cases}

隣接行列の威力は線形代数が使えること。たとえば AkA^k(i,j)(i,j) 成分は、ii から jj への長さ kk の歩道(walk)の本数に等しい。後で学ぶ固有ベクトル中心性やスペクトル解析は、この行列の固有値・固有ベクトルを調べます(グラフラプラシアンとスペクトル)。

隣接リスト

各ノード vv について、隣接するノードの集合 N(v)={u:(v,u)E}N(v) = \{u : (v,u)\in E\} をリストとして持ちます。総メモリは O(n+m)O(n+m)。疎なグラフ(mmn2n^2 よりずっと小さい)では圧倒的に省メモリです。

コードで確認

import networkx as nx
import numpy as np

# 無向グラフを作る(5ノード・5エッジ)
G = nx.Graph()
G.add_edges_from([(0,1),(0,2),(1,2),(2,3),(3,4)])

# --- 隣接行列(密行列)---
A = nx.to_numpy_array(G, nodelist=[0,1,2,3,4], dtype=int)
print("隣接行列 A =")
print(A)

# --- 隣接リスト ---
for v in sorted(G.nodes()):
    print(f"ノード{v} の隣接: {sorted(G.neighbors(v))}")

# --- 疎性 ---
print(f"密度 = {nx.density(G):.3f}")
print(f"非ゼロ要素数 = {int(A.sum())} / 全要素数 = {A.size}")

実行結果:

隣接行列 A =
[[0 1 1 0 0]
 [1 0 1 0 0]
 [1 1 0 1 0]
 [0 0 1 0 1]
 [0 0 0 1 0]]
ノード0 の隣接: [1, 2]
ノード1 の隣接: [0, 2]
ノード2 の隣接: [0, 1, 3]
ノード3 の隣接: [2, 4]
ノード4 の隣接: [3]
密度 = 0.500
非ゼロ要素数 = 10 / 全要素数 = 25

行列が対称(Aij=AjiA_{ij}=A_{ji})であること、各行の和がそのノードの次数になっていることに注目してください。25 個のセルのうち非ゼロは 10 個。ノード数が増えるほど、この行列はスカスカ(疎)になっていきます。

メモリと計算量のトレードオフ

操作隣接行列隣接リスト
メモリO(n2)O(n^2)O(n+m)O(n+m)
i,ji,j は隣接か?」O(1)O(1)O(degi)O(\deg i)
ii の隣人を列挙O(n)O(n)O(degi)O(\deg i)
行列演算(固有値等)得意不向き
graph TD
    Q["どちらを使う?"] --> D{"グラフは密? 疎?"}
    D -->|"密 (m ~ n^2)"| M["隣接行列<br/>線形代数が活きる"]
    D -->|"疎 (m << n^2)"| L["隣接リスト<br/>省メモリ・隣人列挙が速い"]

数式の直観的意味

AijkA^k_{ij} が「長さ kk の歩道の本数」になるのは、行列積が「中継点をすべて足し合わせる」操作だからです。Aij2=lAilAljA^2_{ij} = \sum_l A_{il}A_{lj} は「ilji \to l \to j と2歩で行ける中継点 ll の数」。この性質ゆえに、グラフの到達可能性や経路の数え上げが行列のべき乗で計算できます。

⚠️ よくある誤解・落とし穴

対応シミュレーション

本文の Python コードがそのまま検証コードです(network-study/simulations/ に同等のスニペットを置けます)。

関連