Day 093-Q4 — Huffman符号化(貪欲法 + 優先度付きキュー)

2026-07-16 赤色 Master / Phase 8+ ★★★★★★★★★ Huffman Coding・最小重み二分木

問題

$N$ 個のシンボルの出現頻度 $f_1,\dots,f_N$ が与えられる。ハフマン符号を構築するときの符号長の重み付き総和(総マージコスト)の最小値を求めよ。頻度の集合から2つを選んで足し合わせ(そのコストを加算し)、新しい要素として集合に戻す操作を、要素が1個になるまで繰り返す。

制約

パラメータ範囲備考
$N$$1 \le N \le 10^5$シンボル数
$f_i$$1 \le f_i \le 10^9$出現頻度

入出力例

入力例1

4
1 2 3 4

出力例1

19

$1+2=3$(コスト3)→$\{3,3,4\}$→$3+3=6$(累計9)→$\{4,6\}$→$4+6=10$(累計19)。

入力例2

1
100

出力例2

0

概念図

最小2つを繰り返しマージ(f=1,2,3,4) 10 6 4 3 3 1 2 総コスト = 3+6+10 = 19 (内部ノードの値の総和)

ヒント

ヒント1(方向性)

毎回2つを選んで合体させ合計コストを最小化する問題は、常にその時点で最小の2つを選ぶのが最適(貪欲法)。木の深さとの関係から理由を考えよ。

ヒント2(アプローチ)

「その時点の最小値」を高速に取り出すには優先度付きキュー(最小ヒープ)を使う。heapqで毎回2つ取り出し、和をコストに加算し再度ヒープに戻す。

ヒント3(ほぼ答え)
import heapq
heapq.heapify(freq)
total_cost = 0
while len(freq) > 1:
    x = heapq.heappop(freq)
    y = heapq.heappop(freq)
    merged = x + y
    total_cost += merged
    heapq.heappush(freq, merged)
print(total_cost)

模範解答

import sys
import heapq


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    freq = list(map(int, data[1:1 + n]))

    heapq.heapify(freq)

    total_cost = 0
    while len(freq) > 1:
        x = heapq.heappop(freq)
        y = heapq.heappop(freq)
        merged = x + y
        total_cost += merged
        heapq.heappush(freq, merged)

    print(total_cost)


main()

計算量: ヒープ操作が $N-1$ 回、各操作 $O(\log N)$ で全体 $O(N\log N)$。

Step-by-Step 解説

Step 1: 最小ヒープの構築

heapq.heapify(freq)で$O(N)$で最小ヒープに変換。

Step 2: 最小2要素を取り出してマージ

heappopを2回呼び、最小の2つを取得。ハフマン木の新しい内部ノードに対応。

Step 3: マージコストを加算しヒープへ戻す

合体後の値は「このマージのコスト」であり同時に「新しい仮想シンボルの頻度」でもある。

Step 4: 要素が1個になるまで繰り返す

累積したtotal_costが答え。頻度最小のシンボルほど深い位置に置くのが最適という交換論法で証明できる。

よくあるミス

ミス原因正しい書き方
毎回全体をソートし直す($O(N^2\log N)$)配列のソートを使い回すheapqで$O(N\log N)$に
N=1で誤動作while len(freq)>1を書き忘れるN=1ならループ0回で0のまま正しく終了
マージ後の値をヒープに戻し忘れる2要素popだけで満足heappush(freq, merged)を忘れない
初期コストをsum(freq)にする合計コストと初期頻度の総和を混同コストは0から開始しマージのたびに加算

次のステップ

  • 発展: 実際のハフマン木を構築し各シンボルの符号語(0/1列)を復元
  • 発展: 符号長に上限Lを課すLength-Limited Huffman Coding(Package-Merge法)
  • 次回予告: 区間篩(Segmented Sieve・大区間の素数カウント)

自己評価

理解度: / /

自分の回答:

気づき・メモ: