コンテンツにスキップ

第10回:ヒープ + グラフとその表現方法

前回までで、平衡二分探索木(AVL)なら、どんな順で入れても検索も追加も最悪 O(log n) を守れることを見た。「全部きれいに並んだ状態」を常に保つから、好きな順に取り出せる。

でも、欲しいのが「いちばん小さい 1 個」だけのとき、わざわざ全体を整列して保つのはやりすぎな場面がある。

今日の問い: 常に「いちばん小さい 1 個」を最速で取り出したい。例えばタスク管理(締切が近いものから処理する)。新しいデータもどんどん入ってくる(追加も頻繁)。全部を並べ替えずに、取り出しも追加も速い入れ物は作れるか?


ヒープのルール(親 ≤ 子)

全部をきれいに並べる必要はない。保証するのは「てっぺんが必ず最小」だけ。そのための約束:

  • どの節も 親 ≤ 自分の子(兄弟同士は比べない)
  • 親より小さい子がいない → だから根(てっぺん)が必ず最小

二分探索木の「左の子孫 < 自分 < 右の子孫(全部)」より、ずっと弱いルール。弱い = 守るのが簡単

形のルール

木は 上から、左から、びっしり詰める(=完全二分木)。途中を飛ばさない。だから高さは必ず ≈ log n

graph TD
    A((1)) --> B((3))
    A --> C((2))
    B --> D((7))
    B --> E((4))
    C --> F((5))

この木は親 ≤ 子を全部の節で満たしている。根の 1 が最小。

演習10-1

下の木はヒープのルール(親 ≤ 子)を満たすか? 満たさないなら、違反している親子をすべて挙げよ。さらに、満たすための最短の入れ替え操作を実施せよ。

graph TD
    A((2)) --> B((4))
    A --> C((1))
    B --> D((5))
    B --> E((3))

考えるヒント

各親子について「親 ≤ 子」が成り立つかをひと組ずつ確認する。違反している組を見つけたら、その親子を入れ替えると何が起きるかをイメージする。


追加(push)

新しい値は、左詰めの次の場所(=完全二分木を保つ位置)に置く。そこから:

  1. 親と比べる
  2. 親より小さければ入れ替えて 1 段上へ(浮上
  3. 親より大きくなったら止まる

動くのは根までの道のりだけ高さ分O(log n)

取り出し(pop)

根が答え(最小)。空いた根に末尾の値を持ってくる。そこから:

  1. 小さい方の子と比べる
  2. 子より大きければ入れ替えて 1 段下へ(沈降
  3. 子より小さくなったら止まる

やはり高さ分O(log n)

演習10-2

次の配列は最小値ヒープではない(左詰めで木に並べたと思って読む)。それぞれ、たった 1 組だけ入れ替えて最小値ヒープにできるか? できるならどこを、できないなら理由を一言。

  • (a) [1, 5, 2, 7, 4]
  • (b) [9, 1, 2, 3, 4, 5, 6]

考えるヒント

まずそれぞれを木として描いてみよう(添字 0 が根、左から順に番号を振る)。違反している親子はいくつあるか? 1 組の入れ替えで全部直せるか?


何を捨てて速いのか(対決)

ヒープは「任意のキー検索」を捨てている(そこは苦手)。代わりに「最小の取り出し」と「追加」だけを、両方 O(log n) でこなす。

最小を取り出す 追加 任意のキーで検索 全体を整列
ソート済み配列 O(1)(先頭) O(n)(ずらす) O(log n)(二分探索) 済み
平衡二分探索木 O(log n)(最左) O(log n) O(log n) O(n)
ヒープ O(log n)(見るだけなら O(1) O(log n) O(n)(整列なし) O(n log n)

速さの正体は「全部並べないこと」

  • 二分探索木/AVL: 通読すれば整列が出てくる = 全順序(すべての要素に上下がついている)
  • ヒープ: 親 ≤ 子 だけ、兄弟は無順序 = 部分順序(一部にしか上下がない)

全部を並べる仕事をしないから速い。これが今日の問いの答え。第9回「全部きれいに保つ」の、ちょうど裏返し


添字だけで親子が決まる

完全二分木(隙間なし)なら、上から左へ番号を振るだけで木が表せる。ポインタも枝も要らない。配列の添字で親子が分かる(0 始まり):

  • i左の子 = 2i + 1
  • 右の子 = 2i + 2
  • 親 = (i − 1) // 2

// は整数除算

// は整数除算(商を求める)。5 // 22(4 - 1) // 21

木と配列の対応

先ほどの形のルールで描いた木(値 [1, 3, 2, 7, 4, 5])を例に:

添字: 0  1  2  3  4  5
値 : [1, 3, 2, 7, 4, 5]
  • 添字 4(値 4)の親 = (4 − 1) // 2 = 添字 1(値 3)。図と一致
  • 添字 1(値 3)の子 = 2·1 + 1, 2·1 + 2 = 添字 3, 4(値 7, 4
  • (子を持たない節)は添字 ⌊n/2⌋ 以降(この例 n = 6 なら添字 3 以降)

なぜ配列だと得か

  • 隙間がない → 配列が詰まって連続(メモリの無駄ゼロ)
  • 上下移動が足し算・割り算だけ(ポインタを辿らない)
  • BST/AVL は節をポインタで繋ぐ必要があった。ヒープは要らない

優先度付きキュー

ヒープの正体は「次に処理すべき 1 個を、最速で出す」入れ物。並んだ順ではなく、優先度の高い(小さい)順に出てくるキュー。

  • 救急のトリアージ: 来た順でなく重症度の高い人から
  • 仕事の山: 締切が近いタスクから取り出す

第4回の FIFO キュー(先入れ先出し)の「優先度版」。中身がヒープになっている。

ヒープソート と、この先

全部ヒープに入れて最小を 1 個ずつ取り出すと、出る順が整列になる(=ソート)。

  • 取り出し O(log n) × n 回 = O(n log n)

そして最短経路探索の心臓部:

  • 「いまいちばん近い点」を取り出して確定、を繰り返す
  • = まさに優先度付きキュー

ヒープ(優先度付きキュー)は「次に確定すべき 1 個」を最速で出す。これは最短経路(駅から駅、第12回)の心臓部になる。

でも、その前に問題が 1 つ。「駅と線路のつながり(路線図)」を、どうコンピュータに覚えさせる?


グラフとは

点(頂点 / node) と、点どうしを結ぶ 線(辺 / edge) だけでできる図。「つながり」を表す道具:

  • 路線図、地図の交差点、配管、作業の依存関係、ソーシャルネットワーク……

木で出てきた「根・節・葉」も、もとはこの点と辺の話だった。

グラフで表現・問題解決できる例

  • 乗り換え案内の最短経路(各ノード間は x 分……)
  • 運転見合わせ時の迂回経路(上りのみ停止……)

路線図はそのまま、駅=頂点/線路=辺のグラフとして扱える。各辺に「所要時間」や「料金」を載せれば、最短経路や最安経路の問題になる。

グラフの種類(2 つの軸)

向きがあるか:

  • 無向: 双方向(線路・通路)
  • 有向: 一方向(一方通行の道・「A は B の前提科目」)

重みがあるか:

  • 重みなし: つながっているかだけ
  • 重みあり: 辺に距離・時間・料金などの数がつく(最短経路で使われる)

木は、グラフの特殊例

木(第8〜9回)も、実はグラフの一種。制約がついているだけ:

  • どの節も親は 1 つ
  • そして輪(ループ)がない

グラフはこの制約を外した、もっと自由な「つながり」。

次数(degree)

ある点に辺が何本ついているか = その点の 次数

  • 有向グラフでは、入る本数(入次数)出る本数(出次数) を分けて数える

あとで「どの点から手をつけるか」を考えるときに効いてくる。


どうやって覚えさせるか

つながり(グラフ)をコンピュータに持たせる方法は、大きく 2 通り。

表現①:隣接行列

n × n の表。点 i と点 j がつながっていれば 1(重みありならその値)、なければ 0

  • 無向グラフなら左右対称i→jj→i が同じ)
  • 物理アナロジー: クラスの総当たり表
  • 長所: 2 点がつながっているか一発で分かる(1 マス見るだけ)
  • 短所: マスが 個。線が少ない(疎な)グラフだと 0 だらけで無駄

隣接行列の例

    A  B  C  D  E
A   0  1  1  0  0
B   1  0  0  1  0
C   1  0  0  1  0
D   0  1  1  0  1
E   0  0  0  1  0
  • 行(or 列)の 1 の数 = その点の次数。D の行は 1 が 3 つ → 次数 3
  • 有向なら対称が崩れ、重みありなら 1 のかわりに距離が入る

表現②:隣接リスト

各点ごとに「つながっている点のリスト」だけ持つ。

  • 物理アナロジー: 連絡網(自分の連絡先だけを各自が持つ)
  • 長所: 線が少ない(疎)なら省メモリ。ある点の隣を全部見るのが速い
  • 短所: 2 点がつながっているかの確認は、リストをたどる必要がある

隣接リストの例

A : B, C
B : A, D
C : A, D
D : B, C, E
E : D
  • リストの長さ = その点の次数
  • 重みありなら各項目に「相手と距離」を持たせる(例: D : (B,4), (C,2), (E,7)

どちらを選ぶ(疎か密か)

辺の数を e、点の数を n とすると、辺は最大で約 n² / 2 本まで。

  • e に近い)→ 隣接行列(どうせ埋まる)
  • en よりちょっと多い程度)→ 隣接リスト(0 を持たずに済む)

地図・路線図・配管はたいてい疎(1 点に辺は数本)→ 隣接リストが標準。


演習10-3

次の隣接行列で表されるグラフについて答えよ。

    P  Q  R  S
P   0  1  1  0
Q   1  0  1  1
R   1  1  0  0
S   0  1  0  0
  • (a) これを隣接リストに書き直せ
  • (b) このグラフの辺は何本か(数え方も一言)
  • (c) 次数がいちばん高い点はどれか

考えるヒント

(b) 行の 1 を全部数えると、無向グラフでは 1 本の辺が i→jj→i の 2 回数えられている。何で割ればいい?

演習10-4

次の状況をグラフで表したい。それぞれ 〔頂点は何か/辺は何か/向きは要るか/重みは要るか〕 を決めよ。

  • (a) パーティで握手した二人をリストアップ
  • (b) 大学構内の建物と通路。建物間の徒歩の分数を気にする
  • (c) 履修の前提条件(科目 A を取らないと科目 B が取れない、など)
  • (d) 一方通行が混じる道路網。目的地まで最短時間を知りたい

考えるヒント

  • 「行き来できるか」が片方向だけ/双方向かで、向きの有無が決まる
  • 「単につながっているか知りたい」のか「距離・時間・コストも知りたい」のかで、重みの有無が決まる

演習10-5

下のグラフ(無向)について答える。まず図に描くとやりやすい。

A : B, D
B : A, C
C : B, E
D : A, E
E : C, D, F
F : E
  • (a) A から F行けるか? 行けるなら、通る点を並べて経路を 1 つ書け
  • (b) A から F へ、最短で何本の辺をたどるか?
  • (c) 同じ点を 2 度通らずに一周して戻る道(閉路)はあるか? あれば 1 つ書け

考えるヒント

  • 隣接リストをそのまま図に起こす(頂点 6 つ、辺は左の関係をたどって 1 本ずつ)
  • (b) は (a) で見つけた経路がそのまま最短とは限らない。本数を数えて比較する
  • (c) は「同じ点を 2 度通らない」「最初の点に戻る」両方が必要

まとめ

  • ヒープ: 親 ≤ 子。根が必ず最小。push も pop も O(log n)
    • 配列の添字だけで木を表せる・ポインタ不要
    • 速さの正体は全部を並べないこと(部分順序)。任意検索は捨てて、最小取り出し+追加に尖る
  • 優先度付きキュー
    • 「次の 1 個」を最速で(キューの優先度版)
    • ヒープソートは O(n log n)
  • グラフ: 点と辺で「つながり」を表す
    • 有向/無向・重み・次数
    • 持ち方は隣接行列(密)/隣接リスト(疎)
    • 何を頂点・辺にするかは問題が決める

次回予告

今日:つながり(グラフ)を、行列やリストで覚えさせるところまで来た。でもまだ「覚えた」だけ。次はその上を実際に歩いて、点から点へ探しに行く。

  • ある駅から別の駅へ、どうやってたどり着ける?(迷路の出口を見つける、と同じ)

探し方は大きく 2 通り:

  • 手前から順に、近いところから輪を広げるBFS: 幅優先探索)
  • 一本道を奥までしらみ潰しに進み、行き止まりで戻るDFS: 深さ優先探索)

探し方が違うと、見つかる順番が変わる。「つながっているか(連結)」「輪があるか(閉路)」も、この探索で分かる。

次回:グラフ探索(DFS・BFS)