{ }codelper

学ぶ › TypeScript

アルゴリズム · 31

貪欲法と優先度付きキュー

その場でいちばん良さそうなものを選び、いちばん小さい値をすぐに渡してくれるキューを使います。

貪欲法は、各段階でいまいちばん良さそうなものを選びます。
優先度付きキューは、入れた順番に関係なく、いちばん小さい(またはいちばん大きい)値から渡してくれます。
下の例は、カードの束5つを2つずつ合わせて1つにします。
合わせるたびに2つの束の枚数を足した分だけ手間がかかり、いつもいちばん小さい2つの束を合わせながらその手間を表示し、最後に全部の合計を表示します。

import { readFileSync } from "node:fs"; // 내장 우선순위 큐가 없다. 이만한 크기에서는 가장 작은 값을 직접 찾아 꺼내도 된다function popMin(list: number[]): number {  let at = 0;  for (let i = 1; i < list.length; i++) if (list[i] < list[at]) at = i;  return list.splice(at, 1)[0];} const t: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const n: number = t[0];const piles: number[] = t.slice(1, 1 + n); let total = 0;while (piles.length > 1) {  const a = popMin(piles); // 가장 작은 두 묶음을 꺼낸다  const b = popMin(piles);  const cost = a + b;  console.log(cost);  total += cost;  piles.push(cost); // 합친 묶음을 다시 넣는다}console.log(total);
stdin
5
4 3 2 6 1
stdout
3
6
10
16
35

ポイント

  • 貪欲法は先を見ずに、いまいちばん良いものを選びます。
    例ではいちばん小さい2つの束です。
    先に合わせた束は、あとで合わせるときにもう一度足されるので、小さいものから合わせると、何度も足される数が小さくなります。
  • 貪欲法が合わない問題もあります。
    4円・3円・1円の硬貨で 6円を作るとき、大きい硬貨から使うと 4 + 1 + 1 で 3枚ですが、3 + 3 なら 2枚です。
    500・100・50・10円の硬貨なら、大きいものから使っても正しくなります。
  • だから貪欲法で解く前に、小さな入力ですべての場合を試した答えと比べます。
    一度でも違えば、その規則は間違いです。
    例の規則は、小さな入力 3,000個ですべて一致しました。
  • 例は、いちばん小さい2つの値を取り出し、合わせた値を入れ直すことを繰り返します。
    新しい値が次々に入ってくるので、最初に一度並べ替えるだけでは足りません。
    こういうときに優先度付きキューを使います。
  • 優先度付きキューは、中で値をヒープという形で持っています。
    先頭の値がいちばん小さいことだけを守り、残りは並べ替えておかないので、入れるときも取り出すときも全体をたどりません。
  • JavaScript・TypeScript・C・Craykon には組み込みの優先度付きキューがありません。
    例は取り出すたびにリストをたどっていちばん小さい値を探します。
    この大きさならそれで十分です。

言語ごとの書き方

Pythonimport heapq — リストを heapq.heapify(piles) でヒープにして、heappush(piles, x)、heappop(piles)。
小さい値からしか出てこないので、大きい値から取り出すには -x を入れ、取り出したあとで符号を戻します
JavaScript組み込みの優先度付きキューはありません。
例のようにいちばん小さい値の位置を探して splice(at, 1) で取り出し、値が多いときはヒープを自分で作ります
TypeScriptJavaScript と同じです
JavaPriorityQueue<Integer> — add(x)、poll()(取り出す)、peek()(見るだけ)。
小さい値から出てきます。
大きい値からは new PriorityQueue<>(Comparator.reverseOrder())。
空のときの poll() は null を返します
C#PriorityQueue<int, int> — Enqueue(値, 優先度)、Dequeue()、Peek()。
優先度が小さいものから出てくるので、大きい値からにするには優先度に -x を渡します。
空のときの Dequeue() は InvalidOperationException
C++<queue> の std::priority_queue<int> は大きい値から出てきます。
小さい値からは std::priority_queue<int, std::vector<int>, std::greater<int>>。
top() で見て pop() で取り除きます(pop() は値を返しません)
C組み込みの優先度付きキューはありません。
例のように配列からいちばん小さい値を探して取り出し、値が多いときはヒープを自分で作ります
Craykon組み込みの優先度付きキューはありません。
例のようにいちばん小さい値の位置を探して 位置で取り除く(at) で取り出します。
この方法は束 5,000個までは動きましたが、1万個では時間がかかりすぎるとして止められました。
そのときはヒープを自分で作ります

よくある間違い

貪欲法が通用しない問題に使った

const coins = [4, 3, 1];
let left = 6, count = 0;
for (const c of coins) {
  count += Math.floor(left / c);
  left %= c;
}
console.log(count);

→ 3 — 4 + 1 + 1 です。3 + 3 なら 2 枚で済みます

大きいものから選ぶ規則が正しいか、小さな例で先に確かめます。
このような硬貨の問題は、金額ごとの答えを表に埋めていく動的計画法で解きます。

heapify なしで heappop を呼んだPython

import heapq
piles = [4, 3, 2, 6, 1]
print(heapq.heappop(piles))

→ 4 — いちばん小さい 1 ではなく、先頭にあった値が出てきました。エラーにもなりません

先に heapq.heapify(piles) を呼ぶか、空のリストに heappush で一つずつ入れます。

std::priority_queue は小さい値から出てくると思ったC++

std::priority_queue<int> piles;
for (int x : {4, 3, 2, 6, 1}) piles.push(x);
std::cout << piles.top() << '\n';

→ 6 — 既定ではいちばん大きい値が先です

小さい値からにするには std::priority_queue<int, std::vector<int>, std::greater<int>> にします。
Python の heapq と Java の PriorityQueue は逆で、小さい値が先です。

言語ごとの違い
リストをたどって取り出す方法は、値が少ないときだけ使います。
このコンピューターで計ったところ、JavaScript では束 1万個は 1秒もかかりませんでしたが、10万個は約 10秒かかり、Python の heapq は 10万個でも 1秒かかりませんでした。
優先度付きキューがない言語で値が多いときは、練習問題の解答のようにヒープを自分で作ります。

練習問題