コーディング · アルゴリズム
貪欲法(グリーディアルゴリズム)とは?仕組み・コード・例題をわかりやすく解説
結論
貪欲法は、解を一歩ずつ組み立て、各ステップでその時点で最良に見える選択をし、以前の選択を後から変更しないアルゴリズムです。高速で単純ですが、正しいのは貪欲選択性(局所的に最良の選択が、常にある最適解に含まれること)をもつ問題に限られます。正しい貪欲法の代表例には、区間スケジューリング、ハフマン符号、ダイクストラ法、クラスカル法とプリム法による最小全域木などがあります。
貪欲法が解く問題
貪欲法の対象は最適化問題です。最も多くの活動を選ぶ、最少のコインを使う、最も安いネットワークを構築する、ファイルを最少のビット数に圧縮する、といったものです。貪欲法は、すべての組み合わせを調べる代わりに、選択肢をソートまたは優先順位づけして、各ステップで最良のものを確定させていきます。計算量は通常 O(n log n) で、多くの場合ソートのコストだけです。
難しいのはコードではなく証明です。自然に思えても誤っている貪欲なルールが多いので、ルールが正しいことを証明するか、全探索の解法と照らし合わせて検証する必要があります。
考え方を言葉で説明すると
- 選択肢に順位をつけるルールを見つけます。たとえば「終了時刻が早いものから」「最も軽い辺から」などです。
- そのルールで選択肢をソートするか、優先度付きキューに入れます。
- 選択肢を順に見ていき、制約がまだ許すものを採用します。
- 一度行った選択は取り消しません。
貪欲法が正しいのは、2つの性質が成り立つときです。貪欲選択性:最初の貪欲な選択を含む最適解が存在する。最適部分構造:その選択をした後に残る問題が、同じ種類のより小さな問題になる。証明には、交換論法がよく使われます。任意の最適解を取り、その選択の1つを貪欲な選択と入れ替えても悪くならないことを示します。
例題:区間スケジューリング
会議室が1つあり、次の予約リクエスト(開始、終了)があります。A (1, 4)、B (3, 5)、C (0, 6)、D (5, 7)、E (3, 9)、F (5, 9)、G (6, 10)、H (8, 11)。重ならない会議をできるだけ多く予約します。正しい貪欲なルールは、直前に予約した会議の終了後に始まる会議のうち、終了が最も早いものを常に選ぶことです。
| 会議(終了時刻順) | 開始、終了 | 直前の終了後に始まるか | 判断 | 直前の終了時刻 |
|---|---|---|---|---|
| A | 1, 4 | はい(予約なし) | 予約 | 4 |
| B | 3, 5 | いいえ(3 < 4) | 見送り | 4 |
| C | 0, 6 | いいえ | 見送り | 4 |
| D | 5, 7 | はい | 予約 | 7 |
| E | 3, 9 | いいえ | 見送り | 7 |
| F | 5, 9 | いいえ | 見送り | 7 |
| G | 6, 10 | いいえ | 見送り | 7 |
| H | 8, 11 | はい | 予約 | 11 |
結果は A、D、H の3件で、これが最大です。うまくいく理由:終了が最も早い会議は、他のすべての会議のために最も多くの時間を残すので、どの最適なスケジュールの最初の会議をそれに置き換えても、衝突は起きません。
Pythonでの実装
def max_meetings(meetings):
"""meetings: list of (name, start, end). Returns the booked names."""
booked, last_end = [], float("-inf")
for name, start, end in sorted(meetings, key=lambda m: m[2]):
if start >= last_end:
booked.append(name)
last_end = end
return booked
requests = [("A", 1, 4), ("B", 3, 5), ("C", 0, 6), ("D", 5, 7),
("E", 3, 9), ("F", 5, 9), ("G", 6, 10), ("H", 8, 11)]
print(max_meetings(requests)) # ['A', 'D', 'H']もう1つの定番の例が分数ナップサック問題です。品物の一部を取ってもよい場合は、重さあたりの価値が高い順に取るのが最適です。
def fractional_knapsack(items, capacity):
"""items: list of (value, weight). Returns the best total value."""
total = 0.0
for value, weight in sorted(items, key=lambda i: i[0] / i[1], reverse=True):
take = min(weight, capacity)
total += value * take / weight
capacity -= take
if capacity == 0:
break
return total
print(fractional_knapsack([(60, 10), (100, 20), (120, 30)], 50)) # 240.0貪欲法が失敗する場合:反例
| 問題 | ありがちな貪欲なルール | 反例 | 正しい方法 |
|---|---|---|---|
| コイン問題(コインは 1、3、4) | 最も大きいコインから取る | 金額 6:貪欲法は 4 + 1 + 1(3枚)、最適解は 3 + 3(2枚) | 動的計画法 |
| 0/1ナップサック問題(品物は丸ごとのみ) | 重さあたりの価値が最も高いものから | 容量 50、品物 (60, 10)、(100, 20)、(120, 30):貪欲法は 60 + 100 = 160、最適解は 100 + 120 = 220 | 動的計画法 |
| 区間スケジューリング | 最も短い会議から | (0, 5)、(4, 7)、(6, 11):貪欲法は (4, 7) だけを選び、最適解はほかの2つを選ぶ | 終了が最も早いものから |
| 負の辺がある最短経路 | ダイクストラ法の最も近いノードから | 負の辺によって、確定済みのノードがさらに安くなるグラフ | ベルマン–フォード法 |
時間計算量と空間計算量
| アルゴリズム | 時間 | 理由 |
|---|---|---|
| 区間スケジューリング | O(n log n) | 終了時刻でソートします。走査は O(n) です。 |
| 分数ナップサック問題 | O(n log n) | 重さあたりの価値でソートします。 |
| ハフマン符号 | O(n log n) | n − 1 回の併合で、それぞれヒープ操作が必要です。 |
| クラスカル法による最小全域木 | O(E log E) | 辺のソートと、ほぼ定数時間のUnion-Findです。 |
| ダイクストラ法による最短経路 | O((V + E) log V) | ヒープを使った優先度付きキューです。 |
空間計算量は、ソートしたコピーやヒープのために、通常 O(n) です。入力がすでにソート済みなら、多くの貪欲法は O(n) で動作します。
貪欲法を使う場面と実世界での用途
- 貪欲選択性を証明できる場合や、高速な近似解で十分な場合に貪欲法を使います。それ以外は動的計画法やバックトラッキングを使います。
- 圧縮:ハフマン符号は、出現頻度が最も低い2つのシンボルを繰り返し併合して、最適なプレフィックス符号を作ります。DEFLATE(ZIP、gzip、PNG)などの形式の内部で使われています。
- ネットワーク:最小全域木(クラスカル法、プリム法)と、最短経路ルーティング(ダイクストラ法)。
- スケジューリング:最早締切優先(EDF)や、会議室・機械の予約。
- 近似:集合被覆問題では、「まだ覆われていない要素を最も多く覆う集合を選ぶ」という貪欲なルールは最適ではありませんが、最適解の対数倍以内であることが保証されます。
- コーディング面接の問題:ジャンプゲーム、ガソリンスタンド、クッキーの割り当て、重ならない区間、風船を割る最小の矢の数、タスクスケジューラ。
よくある質問
貪欲法とは、簡単に言うとどういうものですか?
その時点で最良に見える選択肢を常に選び、後から見直さないアルゴリズムです。こうした局所的な選択が、常に全体の最良の答えにつながる問題でしか、正しくありません。
貪欲法が正しいことは、どうやって証明しますか?
通常は交換論法を使います。任意の最適解から始め、最初の選択を貪欲な選択に置き換えても悪くならないことを示し、残りの問題について繰り返します。
貪欲法と動的計画法のどちらを使うべきですか?
単純なルールで良さそうなら、まず貪欲法を試し、反例を探します。反例が見つかるなら、各部分問題ですべての選択を比較する動的計画法が必要です。
ダイクストラ法は貪欲法ですか?
はい。未確定のノードのうち最も近いものを常に確定します。辺の重みが非負なので、この貪欲な選択は正しいといえます。