1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

《アルゴリズムを学ぶ》19.ヒープ

Last updated at Posted at 2025-03-24

Overview

ヒープ(Heap) は、最小値または最大値を効率的に取り出すためのデータ構造であり、優先度付きキュー(Priority Queue)を実現するために使われる。
Python では heapq モジュールを使うと、最小ヒープ(Min-Heap)を簡単に実装できる。
1.ヒープの基本
2.最大ヒープ(Max-Heap)の実装
3.ヒープの応用
4.典型問題を解いてみる

1. ヒープの基本

(1) ヒープとは?

ヒープは、次の性質を満たす 完全二分木 である。

  • 最小ヒープ(Min-Heap)
    • 常に最小の要素が heap[0] に保持される
    • 親ノードの値 ≤ 子ノードの値
  • 最大ヒープ(Max-Heap)
    • 常に最大の要素が heap[0] に保持される
    • 親ノードの値 ≥ 子ノードの値
    • ※Python の heapq は最大ヒープを提供しないため、
      最大ヒープを使いたい場合は値を負にするなどして擬似的に実現する

ここでいう「完全二分木」は次を意味する:

  • 各ノードは最大 2 つの子を持つ(二分木)
  • ノードは 左から順に詰めて 配置される
  • 最下段以外はすべて埋まっている

最小ヒープのイメージ(配列 [1, 3, 4, 7, 6, 5]):

          1
         / \
        3   4
       / \  /
      7   6 5

Python の heapq では、この完全二分木を 1 次元配列(リスト)で表現している。

  • 親:i
  • 左の子:2*i + 1
  • 右の子:2*i + 2

という対応になっている。

(2) Python の heapq の基本操作

import heapq

heap = []

# 要素を追加(push)
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 4)

print(heap)  # [1, 3, 4](最小値が先頭にくる)

# 最小値を取得(pop)
x = heapq.heappop(heap)  # 1 を取り出しつつ削除
print(x)     # 1 最小値を取得しながら削除
print(heap)  # [3, 4]

ポイント:

  • heap単なるリストだが、heapq 経由で操作することでヒープとして振る舞う
  • 常に最小要素だけが保証される(heap 全体がソートされているわけではない)

(3) 主な操作と計算量

操作名 説明 関数 計算量
追加(push) 要素をヒープに追加 heapq.heappush(h, x) O(log N)
取り出し(pop) 最小要素を取り出して削除 heapq.heappop(h) O(log N)
最小値の参照 削除せずに最小要素を見る h[0] O(1)
配列をヒープ化 リストをヒープ構造に変換 heapq.heapify(h) O(N)
最大 K 個・最小 K 個取得 上位 K 個の要素を取得 heapq.nlargest/nsmallest O(N log K)

2. 最大ヒープ(Max-Heap)の実装

Python の heapq最小ヒープのみ を提供する。
最大ヒープを使いたい場合は、値にマイナスを掛けて管理するのが定番テクニック。

import heapq

heap = []

# 負の値を追加(最大値を管理)
heapq.heappush(heap, -3)
heapq.heappush(heap, -1)
heapq.heappush(heap, -4)

# 最大値を取り出す
print(-heapq.heappop(heap))  # 4
print(-heapq.heappop(heap))  # 3
print(-heapq.heappop(heap))  # 1

ポイント:

  • heap の中では [-4, -1, -3] のように最小値(=元の値の最大)が先頭に来ている
  • 取り出すときに マイナスを掛けて元に戻す だけで最大ヒープとして使える
  • 「大きいものから順に処理したい」優先度付きキューでよく使うテクニック

3. ヒープの応用

(1) N 個の要素の中から K 番目に小さい要素を求める

import heapq

arr = [7, 10, 4, 3, 20, 15]
k = 3

heapq.heapify(arr)  # 配列をヒープ化
for _ in range(k - 1):
    heapq.heappop(arr)  # k-1 回 pop する

print(heapq.heappop(arr))  # 3番目に小さい値(7)

ポイント:

  • 「K 番目に小さい」=「小さい順に K 回取り出したときの最後の値」
  • 計算量はおおよそ O(N + K log N)

(2) N 個の要素の中から K 個の最大値を求める

import heapq

arr = [7, 10, 4, 3, 20, 15]
k = 3

top_k = heapq.nlargest(k, arr)
print(top_k)  # [20, 15, 10]

ポイント:

  • heapq.nlargest(k, arr) は内部的にヒープを使っている
  • 計算量は O(N log K) で、単純ソート(O(N log N))より効率的なことが多い

(3) 2つの最小値を合成する(最小コストで作る)

典型的な「最小コストで合成」問題(Huffman 木 / ロープを束ねる問題 など):

import heapq

N = int(input())  # 数列の要素数
A = list(map(int, input().split()))  # 数列

heapq.heapify(A)  # 配列を最小ヒープに変換

cost = 0  # 合成コストの合計
while len(A) > 1:
    x = heapq.heappop(A)  # 最小値を取り出す
    y = heapq.heappop(A)  # 2番目に小さい値を取り出す
    cost += x + y  # 合成コストを追加
    heapq.heappush(A, x + y)  # 合成した値をヒープに戻す

print(cost)  # 最小合成コスト

ポイント:

  • 毎回「最も小さい 2 つ」を合成するのが最適(貪欲法)
  • 各ステップで pop 2 回・push 1 回 → 1 回あたり O(log N)
  • 全体の計算量は O(N log N)

4. 典型問題を解いてみる

例題1. K 番目に小さい値を求める

問題: N 個の整数列A から K 番目に小さい値を求めよ。

回答 
import heapq

N, K = map(int, input().split())
A = list(map(int, input().split()))

heapq.heapify(A)
for _ in range(K - 1):
    heapq.heappop(A)

print(heapq.heappop(A))

例題2. K 個の最大値を求める

問題: N 個の整数列 A と整数 K が与えられる。最大値から順に K 個 の値を出力せよ。

回答 
import heapq

N, K = map(int, input().split())
A = list(map(int, input().split()))

ans = heapq.nlargest(K, A)
print(*ans)

例題3. 優先度付きキューのシミュレーション

問題: Q 個のクエリが与えられる。クエリは次の 2 種類:
 - insert x : x を追加する
 - pop : 最小値を 1 つ取り出して出力する
ヒープを使ってこれを実装せよ。

回答 
import heapq
import sys

input = sys.stdin.readline

Q = int(input())
heap = []

for _ in range(Q):
    query = input().split()
    if query[0] == "insert":
        x = int(query[1])
        heapq.heappush(heap, x)
    elif query[0] == "pop": 
        # この実装では「必ず要素が 1 つ以上ある状態で pop が呼ばれる」前提
        x = heapq.heappop(heap)
        print(x)

例題4. 2つの最小値を合成する

問題:
N 個の正の整数 A が与えられる。
以下の操作を、配列の要素が 1 個になるまで繰り返す:

  1. 配列から最小の 2 つの値 x, y を取り出す
  2. コストに (x + y) を加算する
  3. (x + y) を配列に追加する

最終的な 合計コスト を求めよ。

回答 
import heapq

N = int(input())
A = list(map(int, input().split()))
heapq.heapify(A)

cost = 0
while len(A) > 1:
    x = heapq.heappop(A)
    y = heapq.heappop(A)
    cost += x + y
    heapq.heappush(A, x + y)

print(cost)
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?