{ }codelper

学习 › JavaScript

算法 · 31

贪心与优先队列

每一步都选眼下看起来最好的,再用一个能马上交出最小值的队列。

贪心就是每一步都选当前看起来最好的。
优先队列不管放入的先后,总是先交出最小(或最大)的值。
下面的例子把 5 叠卡片两两合并成一叠。
每次合并花的力气等于两叠卡片的张数之和;它总是合并最小的两叠,输出每次花的力气,最后输出总和。

// 내장 우선순위 큐가 없다. 이만한 크기에서는 가장 작은 값을 직접 찾아 꺼내도 된다function popMin(list) {  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 = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const n = t[0];const piles = 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

要点

  • 贪心不往后看,只选眼下最好的。
    在例子里就是最小的两叠。
    先合并的那一叠在后面的合并中还会再被加一次,所以先合并小的,被多次相加的数就小。
  • 贪心并不适合所有题目。
    用 4 元、3 元、1 元的硬币凑 6 元,先用大的是 4 + 1 + 1,共 3 枚,而 3 + 3 只要 2 枚。
    500、100、50、10 元的硬币则可以放心先用大的。
  • 所以用贪心之前,先在小输入上和把所有情况都试一遍的答案对比。
    只要有一次不同,这条规则就是错的。
    例子里的规则在 3,000 个小输入上全部一致。
  • 例子不断取出最小的两个值,再把合并后的值放回去。
    新的值不断进来,只在开头排一次序是不够的。
    这时就用优先队列。
  • 优先队列内部把值摆成一种叫堆的形状。
    它只保证最前面的值最小,其余的并不排好序,所以放入和取出都不用把全部扫一遍。
  • JavaScript、TypeScript、C 和 Craykon 没有内置的优先队列。
    例子每次取出时都扫一遍列表找最小值。
    这么小的规模这样做没问题。

各语言的写法

Pythonimport heapq — 用 heapq.heapify(piles) 把列表变成堆,然后 heappush(piles, x)、heappop(piles)。
只能先取出小的,要先取出大的就放入 -x,取出后再把符号变回来
JavaScript没有内置的优先队列。
像例子那样找到最小值的位置,用 splice(at, 1) 取出;值很多时自己实现堆
TypeScript和 JavaScript 一样
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 就调用 heappopPython

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 秒。
在没有优先队列的语言里,值很多时就像练习题的解答那样自己实现堆。

练习题