学习 › TypeScript
算法 · 31
贪心与优先队列
每一步都选眼下看起来最好的,再用一个能马上交出最小值的队列。
贪心就是每一步都选当前看起来最好的。
优先队列不管放入的先后,总是先交出最小(或最大)的值。
下面的例子把 5 叠卡片两两合并成一叠。
每次合并花的力气等于两叠卡片的张数之和;它总是合并最小的两叠,输出每次花的力气,最后输出总和。
import heapq n = int(input())piles = list(map(int, input().split()))heapq.heapify(piles) # 가장 작은 값이 언제나 맨 앞(piles[0])에 오게 한다 total = 0while len(piles) > 1: a = heapq.heappop(piles) # 가장 작은 두 묶음을 꺼낸다 b = heapq.heappop(piles) cost = a + b print(cost) total += cost heapq.heappush(piles, cost) # 합친 묶음을 다시 넣는다print(total)// 내장 우선순위 큐가 없다. 이만한 크기에서는 가장 작은 값을 직접 찾아 꺼내도 된다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);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);import java.util.PriorityQueue;import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); PriorityQueue<Integer> piles = new PriorityQueue<>(); // 꺼내면 언제나 가장 작은 값이 나온다 for (int i = 0; i < n; i++) piles.add(in.nextInt()); int total = 0; while (piles.size() > 1) { int a = piles.poll(); // 가장 작은 두 묶음을 꺼낸다 int b = piles.poll(); int cost = a + b; System.out.println(cost); total += cost; piles.add(cost); // 합친 묶음을 다시 넣는다 } System.out.println(total); }}using System;using System.Collections.Generic; class Program{ static void Main() { int n = int.Parse(Console.ReadLine()); // (값, 우선순위) 짝으로 넣고, 우선순위가 가장 작은 것부터 나온다 PriorityQueue<int, int> piles = new PriorityQueue<int, int>(); foreach (string w in Console.ReadLine().Split()) piles.Enqueue(int.Parse(w), int.Parse(w)); int total = 0; while (piles.Count > 1) { int a = piles.Dequeue(); // 가장 작은 두 묶음을 꺼낸다 int b = piles.Dequeue(); int cost = a + b; Console.WriteLine(cost); total += cost; piles.Enqueue(cost, cost); // 합친 묶음을 다시 넣는다 } Console.WriteLine(total); }}#include <functional>#include <iostream>#include <queue>#include <vector> int main() { int n; std::cin >> n; // std::greater를 줘야 가장 작은 값이 먼저 나온다(기본은 가장 큰 값) std::priority_queue<int, std::vector<int>, std::greater<int>> piles; for (int i = 0; i < n; i++) { int x; std::cin >> x; piles.push(x); } int total = 0; while (piles.size() > 1) { int a = piles.top(); // 가장 작은 두 묶음을 꺼낸다 piles.pop(); int b = piles.top(); piles.pop(); int cost = a + b; std::cout << cost << '\n'; total += cost; piles.push(cost); // 합친 묶음을 다시 넣는다 } std::cout << total << '\n';}#include <stdio.h> int piles[1000];int count; // 내장 우선순위 큐가 없다. 이만한 크기에서는 가장 작은 값을 직접 찾아 꺼내도 된다int popMin(void) { int at = 0; for (int i = 1; i < count; i++) if (piles[i] < piles[at]) at = i; int v = piles[at]; piles[at] = piles[--count]; // 빈자리는 맨 끝 값으로 메운다 return v;} int main(void) { int n; scanf("%d", &n); for (int i = 0; i < n; i++) scanf("%d", &piles[i]); count = n; int total = 0; while (count > 1) { int a = popMin(); // 가장 작은 두 묶음을 꺼낸다 int b = popMin(); int cost = a + b; printf("%d\n", cost); total += cost; piles[count++] = cost; // 합친 묶음을 다시 넣는다 } printf("%d\n", total); return 0;}// 내장 우선순위 큐가 없다. 이만한 크기에서는 가장 작은 값을 직접 찾아 꺼내도 된다popMin(list){ at = 0; 循环 (i = 1; i < list.数量; i++) { 如果 (list[i] < list[at]) { at = i; } } v = list[at]; list.按位置移除(at); 返回 v;} n = 控制台.读行().转数字();piles = 控制台.读数字(); total = 0;当 (piles.数量 > 1){ a = popMin(piles); // 가장 작은 두 묶음을 꺼낸다 b = popMin(piles); cost = a + b; 控制台.输出(cost); total += cost; piles.添加(cost); // 합친 묶음을 다시 넣는다}控制台.输出(total);5 4 3 2 6 1
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 没有内置的优先队列。
例子每次取出时都扫一遍列表找最小值。
这么小的规模这样做没问题。
各语言的写法
| Python | import heapq — 用 heapq.heapify(piles) 把列表变成堆,然后 heappush(piles, x)、heappop(piles)。只能先取出小的,要先取出大的就放入 -x,取出后再把符号变回来 |
|---|---|
| JavaScript | 没有内置的优先队列。 像例子那样找到最小值的位置,用 splice(at, 1) 取出;值很多时自己实现堆 |
| TypeScript | 和 JavaScript 一样 |
| Java | PriorityQueue<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 的
在没有优先队列的语言里,值很多时就像练习题的解答那样自己实现堆。
扫一遍列表再取出的做法只适合值很少的时候。
在这台电脑上实测,JavaScript 处理 1 万叠不到 1 秒,10 万叠却要大约 10 秒,而 Python 的
heapq 处理 10 万叠也不到 1 秒。在没有优先队列的语言里,值很多时就像练习题的解答那样自己实现堆。