8.4. "heapq" --- ヒープキューアルゴリズム
*****************************************

バージョン 2.3 で追加.

**ソースコード:** Lib/heapq.py

======================================================================

このモジュールではヒープキューアルゴリズムの一実装を提供しています。優
先度キューアルゴリズムとしても知られています。

ヒープとは、全ての親ノードの値が、その全ての子の値以下であるようなバイ
ナリツリーです。この実装は、全ての *k* に対して、ゼロから要素を数えて
いった際に、"heap[k] <= heap[2*k+1]" かつ "heap[k] <= heap[2*k+2]" と
なる配列を使っています。比較のために、存在しない要素は無限大として扱わ
れます。ヒープの興味深い性質は、最小の要素が常にルート、つまり
"heap[0]" になることです。

以下の API は教科書におけるヒープアルゴリズムとは 2 つの側面で異なって
います: (a) ゼロベースのインデクス化を行っています。これにより、ノード
に対するインデクスとその子ノードのインデクスの関係がやや明瞭でなくなり
ますが、Python はゼロベースのインデクス化を使っているのでよりしっくり
きます。(b) われわれの pop メソッドは最大の要素ではなく最小の要素 (教
科書では "min heap:最小ヒープ" と呼ばれています; 教科書では並べ替えを
インプレースで行うのに適した "max heap:最大ヒープ" が一般的です)。

これらの 2 点によって、ユーザに戸惑いを与えることなく、ヒープを通常の
Python リストとして見ることができます: "heap[0]" が最小の要素となり、
"heap.sort()" はヒープ不変式を保ちます!

ヒープを作成するには、 "[]" に初期化されたリストを使うか、 "heapify()"
を用いて要素の入ったリストを変換します。

次の関数が用意されています:

heapq.heappush(heap, item)

   *item* を *heap* に push します。ヒープ不変式を保ちます。

heapq.heappop(heap)

   pop を行い、 *heap* から最小の要素を返します。ヒープ不変式は保たれ
   ます。ヒープが空の場合、 "IndexError" が送出されます。pop せずに最
   小の要素にアクセスするには、 "heap[0]" を使ってください。

heapq.heappushpop(heap, item)

   *item* を *heap* に push した後、pop を行って *heap* から最初の要素
   を返します。この一続きの動作を "heappush()" に引き続いて
   "heappop()" を別々に呼び出すよりも効率的に実行します。

   バージョン 2.6 で追加.

heapq.heapify(x)

   リスト *x* をインプレース処理し、線形時間でヒープに変換します。

heapq.heapreplace(heap, item)

   *heap* から最小の要素を pop して返し、新たに *item* を push します
   。ヒープのサイズは変更されません。ヒープが空の場合、 "IndexError"
   が送出されます。

   この一息の演算は "heappop()" に次いで "heappush()" を送出するよりも
   効率的で、固定サイズのヒープを用いている場合にはより適しています。
   pop/push の組み合わせは必ずヒープから要素を一つ返し、それを *item*
   と置き換えます。

   返される値は加えられた *item* よりも大きくなるかもしれません。それ
   を望まないなら、代わりに "heappushpop()" を使うことを考えてください
   。この push/pop の組み合わせは二つの値の小さい方を返し、大きい方の
   値をヒープに残します。

このモジュールではさらに3つのヒープに基く汎用関数を提供します。

heapq.merge(*iterables)

   複数のソートされた入力をマージ(merge)して一つのソートされた出力にし
   ます (たとえば、複数のログファイルの時刻の入ったエントリーをマージ
   します)。ソートされた値にわたる *iterator* を返します。

   "sorted(itertools.chain(*iterables))" と似ていますが、イテレータを
   返し、一度にはデータをメモリに読み込みまず、それぞれの入力が(最小か
   ら最大へ)ソートされていることを仮定します。

   バージョン 2.6 で追加.

heapq.nlargest(n, iterable[, key])

   *iterable* で定義されるデータセットのうち、最大値から降順に *n* 個
   の値のリストを返します。(あたえられた場合) *key* は、引数を一つとる
   、*iterable* のそれぞれの要素から比較キーを生成する関数を指定します
   : "key=str.lower" 以下のコードと同等です: "sorted(iterable,
   key=key, reverse=True)[:n]"

   バージョン 2.4 で追加.

   バージョン 2.5 で変更: オプションの *key* 引数が追加されました.

heapq.nsmallest(n, iterable[, key])

   *iterable* で定義されるデータセットのうち、最小値から昇順に *n* 個
   の値のリストを返します。(あたえられた場合) *key* は、引数を一つとる
   、*iterable* のそれぞれの要素から比較キーを生成する関数を指定します
   : "key=str.lower" 以下のコードと同等です: "sorted(iterable,
   key=key)[:n]"

   バージョン 2.4 で追加.

   バージョン 2.5 で変更: オプションの *key* 引数が追加されました.

後ろ二つの関数は *n* の値が小さな場合に最適な動作をします。大きな値の
時には "sorted()" 関数の方が効率的です。さらに、 "n==1" の時には
"min()" および "max()" 関数の方が効率的です。この関数を繰り返し使うこ
とが必要なら、iterable を実際のヒープに変えることを考えてください。


8.4.1. 基本的な例
=================

すべての値をヒープに push してから最小値を 1 つずつ pop することで、ヒ
ープソート を実装できます:

   >>> def heapsort(iterable):
   ...     h = []
   ...     for value in iterable:
   ...         heappush(h, value)
   ...     return [heappop(h) for i in range(len(h))]
   ...
   >>> heapsort([1, 3, 5, 7, 9, 2, 4, 6, 8, 0])
   [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

これは "sorted(iterable)" に似ていますが、 "sorted()" とは異なり、この
実装はステーブルソートではありません。

ヒープの要素はタプルに出来ます。これは、追跡される主レコードとは別に (
タスクの優先度のような) 比較値を指定するときに便利です:

   >>> h = []
   >>> heappush(h, (5, 'write code'))
   >>> heappush(h, (7, 'release product'))
   >>> heappush(h, (1, 'write spec'))
   >>> heappush(h, (3, 'create tests'))
   >>> heappop(h)
   (1, 'write spec')


8.4.2. 優先度キュー実装の注釈
=============================

優先度つきキュー は、ヒープの一般的な使い方で、実装にはいくつか困難な
点があります:

* ソート安定性: 優先度が等しい二つのタスクが、もともと追加された順序
  で 返されるためにはどうしたらいいでしょうか？

* Python 3 では、 (priority, task) ペアに対するタプルの比較は、
  priority が同じで task がデフォルトの比較順を持たないときに破綻しま
  す。

* あるタスクの優先度が変化したら、どうやってそれをヒープの新しい位置
  に 移動させるのでしょうか？

* 未解決のタスクが削除される必要があるとき、どのようにそれをキューか
  ら 探して削除するのでしょうか？

最初の二つの困難の解決策は、項目を優先度、項目番号、そしてタスクを含む
3 要素のリストとして保存することです。この項目番号は、同じ優先度の二つ
のタスクが、追加された順序で返されるようにするための同点決勝戦として働
きます。そして二つの項目番号が等しくなることはありませんので、タプルの
比較が二つのタスクを直接比べようとすることはありえません。

残りの困難は主に、未解決のタスクを探して、その優先度を変更したり、完全
に削除することです。タスクを探すことは、キュー内の項目を指し示す辞書に
よってなされます。

項目を削除したり、優先度を変更することは、ヒープ構造の不変関係を壊すこ
とになるので、もっと難しいです。ですから、可能な解決策は、その項目が無
効であるものとしてマークし、必要なら変更された優先度の項目を加えること
です:

   pq = []                         # list of entries arranged in a heap
   entry_finder = {}               # mapping of tasks to entries
   REMOVED = '<removed-task>'      # placeholder for a removed task
   counter = itertools.count()     # unique sequence count

   def add_task(task, priority=0):
       'Add a new task or update the priority of an existing task'
       if task in entry_finder:
           remove_task(task)
       count = next(counter)
       entry = [priority, count, task]
       entry_finder[task] = entry
       heappush(pq, entry)

   def remove_task(task):
       'Mark an existing task as REMOVED.  Raise KeyError if not found.'
       entry = entry_finder.pop(task)
       entry[-1] = REMOVED

   def pop_task():
       'Remove and return the lowest priority task. Raise KeyError if empty.'
       while pq:
           priority, count, task = heappop(pq)
           if task is not REMOVED:
               del entry_finder[task]
               return task
       raise KeyError('pop from an empty priority queue')


8.4.3. 理論
===========

ヒープとは、全ての *k* について、要素を 0 から数えたときに、"a[k] <=
a[2*k+1]" かつ  "a[k] <= a[2*k+2]" となる配列です。比較のために、存在
しない要素を無限大と考えます。ヒープの興味深い属性は "a[0]" が常に最小
の要素になることです。

上記の奇妙な不変式は、勝ち抜き戦判定の際に効率的なメモリ表現を行うため
のものです。以下の番号は "a[k]" ではなく *k* とします:

                                  0

                 1                                 2

         3               4                5               6

     7       8       9       10      11      12      13      14

   15 16   17 18   19 20   21 22   23 24   25 26   27 28   29 30

上の木構造では、各セル *k* は "2*k+1" および "2*k+2" を最大値としてい
ます。スポーツに見られるような通常の 2 つ組勝ち抜き戦では、各セルはそ
の下にある二つのセルに対する勝者となっていて、個々のセルの勝者を追跡し
ていくことにより、そのセルに対する全ての相手を見ることができます。しか
しながら、このような勝ち抜き戦を使う計算機アプリケーションの多くでは、
勝歴を追跡する必要はりません。メモリ効率をより高めるために、勝者が上位
に進級した際、下のレベルから持ってきて置き換えることにすると、あるセル
とその下位にある二つのセルは異なる三つの要素を含み、かつ上位のセルは二
つの下位のセルに対して "勝者と" なります。

このヒープ不変式が常に守られれば、インデクス 0 は明らかに最勝者となり
ます。最勝者の要素を除去し、"次の" 勝者を見つけるための最も単純なアル
ゴリズム的手法は、ある敗者要素 (ここでは上図のセル 30 とします) を 0
の場所に持っていき、この新しい 0 を濾過するようにしてツリーを下らせて
値を交換してゆきます。不変関係が再構築されるまでこれを続けます。この操
作は明らかに、ツリー内の全ての要素数に対して対数的な計算量となります。
全ての要素について繰り返すと、O(n log n) のソート(並べ替え)になります
。

このソートの良い点は、新たに挿入する要素が、その最に取り出す 0 番目の
要素よりも "良い値" でない限り、ソートを行っている最中に新たな要素を効
率的に追加できるというところです。この性質は、シミュレーション的な状況
で、ツリーで全ての入力イベントを保持し、"勝者となる状況" を最小のスケ
ジュール時刻にするような場合に特に便利です。あるイベントが他のイベント
群の実行をスケジュールする際、それらは未来にスケジュールされることにな
るので、それらのイベント群を容易にヒープに積むことができます。すなわち
、ヒープはスケジューラを実装する上で良いデータ構造であるといえます (私
は MIDI シーケンサで使っているものです :-)。

これまでスケジューラを実装するための様々なデータ構造が広範に研究されて
います。ヒープは十分高速で、速度もおおむね一定であり、最悪の場合でも平
均的な速度とさほど変わらないため良いデータ構造といえます。しかし、最悪
の場合がひどい速度になることを除き、たいていでより効率の高い他のデータ
構造表現も存在します。

ヒープはまた、巨大なディスクのソートでも非常に有用です。おそらくご存知
のように、巨大なソートを行うと、複数の "ラン (run)" (予めソートされた
配列で、そのサイズは通常 CPU メモリの量に関係しています) が生成され、
続いて統合処理 (merging) がこれらのランを判定します。この統合処理はし
ばしば非常に巧妙に組織されています [1]。重要なのは、最初のソートが可能
な限り長いランを生成することです。勝ち抜き戦はこれを達成するための良い
方法です。もし利用可能な全てのメモリを使って勝ち抜き戦を行い、要素を置
換および濾過処理して現在のランに収めれば、ランダムな入力に対してメモリ
の二倍のサイズのランを生成することになり、大体順序づけがなされている入
力に対してはもっと高い効率になります。

さらに、ディスク上の 0 番目の要素を出力して、現在の勝ち抜き戦に (最後
に出力した値に "勝って" しまうために) 収められない入力を得たなら、ヒー
プには収まらないため、ヒープのサイズは減少します。解放されたメモリは二
つ目のヒープを段階的に構築するために巧妙に再利用することができ、この二
つ目のヒープは最初のヒープが崩壊していくのと同じ速度で成長します。最初
のヒープが完全に消滅したら、ヒープを切り替えて新たなランを開始します。
なんと巧妙で効率的なのでしょう！

一言で言うと、ヒープは知って得するメモリ構造です。私はいくつかのアプリ
ケーションでヒープを使っていて、'ヒープ' モジュールを常備するのはいい
事だと考えています。:-)

-[ 注記 ]-

[1] 現在使われているディスクバランス化アルゴリズムは、最近はもはや
    巧妙 というよりも目障りであり、このためにディスクに対するシーク機
    能が重 要になっています。巨大な容量を持つテープのようにシーク不能
    なデバイ スでは、事情は全く異なり、個々のテープ上の移動が可能な限
    り効率的に 行われるように非常に巧妙な処理を (相当前もって) 行わな
    ければなりま せん (すなわち、もっとも統合処理の "進行" に関係があ
    ります)。テー プによっては逆方向に読むことさえでき、巻き戻しに時間
    を取られるのを 避けるために使うこともできます。正直、本当に良いテ
    ープソートは見て いて素晴らしく驚異的なものです！ソートというのは
    常に偉大な芸術なの です！:-)
