贪心算法

贪心算法

策略速查

贪心在每一步选择当前最优的选项,通常与排序配合使用(先排序,再贪心)。核心是价值函数——决定哪个选择是最优的:

  • 要最大化某个值 → 每次选择最大的
  • 要最小化某个值 → 每次选择最小的
问题类型 贪心策略
最多选 k 个(耗时/体积受限) 按耗时/体积升序选
活动安排 / 区间选点 结束时间排序,选最早结束的
最少段覆盖不匹配 逐位扫描,段计数
间隔合并 vs 分开 比较合并代价与新建代价(如 $gap$ vs $K+1$)
只能左移的重排 从左到右匹配,数需要移动的元素
硬币找零 仅特定面额系统有效
背包(价值/重量) 无通用贪心,需 DP

解释

贪心算法的特点

  1. 贪心选择性质:每一步都做出当前最优的选择
  2. 最优子结构:问题的最优解包含子问题的最优解
  3. 不可回溯:一旦做出选择,就不再考虑其他可能

正确性通常通过交换参数证法(exchange argument)证明:假设最优解中选了 B 而不是更优的 A,把 B 换成 A 不会变差,矛盾。

贪心 vs 动态规划

特征 贪心算法 动态规划
选择策略 只考虑当前最优 考虑所有可能
时间复杂度 通常 O(n) 或 O(nlogn) 通常 O(n²) 或更高
最优性 不保证全局最优 保证全局最优
适用问题 具有贪心选择性质 最优子结构

何时使用贪心:具有贪心选择性质 + 最优子结构 + 问题规模较大需要高效算法。

贪心失效的例子

硬币找零:面额 {1, 3, 4}、目标 6——贪心解 4+1+1 用 3 枚,最优解 3+3 只要 2 枚。贪心只在特定面额系统(如美元、欧元)下有效。

背包问题

物品 重量 价值 价值/重量
A 3 18 6
B 2 10 5
C 2 10 5

容量 4:贪心按价值/重量选 A(18),最优是 B+C(20)。背包需要动态规划解决。

例题

例题 1:学习算法(排序贪心入门)

来源:Codeforces - Very Easy

Steph 有 X 分钟时间学习算法,有 N 个算法,第 i 个需要 a_i 分钟。求最多能学多少个算法。(1 ≤ X ≤ 10^41 ≤ N ≤ 100

思路:先学用时最短的——a_i 升序排列后依次累加,直到时间用完。

正确性(交换论证):若最优解先选了用时更长的 B,把它换成用时更短的 A,总时间不增、可选数量不减,故先选最短总是最优。

algorithms.sort()
count = 0  # 已用时间
i = 0
while i < N and count + algorithms[i] <= X:
    count += algorithms[i]
    i += 1
print(i)

复杂度:O(N log N)

例题 2:活动安排 Movie Festival(按结束时间排序)

来源:CSES - Easy

有 N 个活动,每个活动有开始和结束时间,每次只能全程参加一个。求最多能参加多少个活动。

错误策略:选最早开始的。反例:[(0,6), (5,10), (6,7)]——按最早开始只能选 1 个,最优是 [(0,6), (6,7)] 两个。

正确策略:选最早结束的,为后续留出更多时间。

正确性:设 E1 比 E2 结束早。能接在 E2 之后的活动 X 必然也能接在 E1 之后,所以选 E1 不会更差、后续可选集合更大。

events.sort(key=lambda x: x[1])  # 按结束时间排序
current_end = -1
ans = 0

for start, end in events:
    if start >= current_end:  # 可以参加
        current_end = end
        ans += 1

print(ans)

复杂度:O(N log N)

例题 3:Mad Scientist(不匹配段计数)

来源:USACO Bronze - Mad Scientist(文件名 breedflip)

给定两个长度为 n 的字符串 a 和 b,每次操作可以将 a 的任意连续子串反转。求最少多少次操作将 a 变成 b。

思路:每次反转恰好消除一个连续的不匹配段,所以答案 = 不匹配段的个数。用 flag 扫描:不在段中遇到 a[i] != b[i] 就开新段计数,段中遇到相等就结束段。

import sys

sys.stdin = open("breedflip.in", "r")
sys.stdout = open("breedflip.out", "w")

n = int(input())
a = input()
b = input()

flag = True  # True 表示当前不在不匹配段中
ans = 0

for i in range(n):
    if flag:
        if a[i] != b[i]:  # 开始新的不匹配段
            ans += 1
            flag = False
    else:
        if a[i] == b[i]:  # 不匹配段结束
            flag = True

print(ans)

复杂度:时间 O(n),空间 O(1)

例题 4:Watching Mooloo(间隔合并 or 分开)

来源:USACO Bronze - Watching Mooloo

农民 John 要观看 N 个电影,第 $d_i$ 天上映($1 \leq d_i \leq 10^{14}$,递增)。订阅连续 $d$ 天费用为 $d + K$ 美元($1 \leq K \leq 10^9$),可多次订阅。求看完全部电影的最小总费用。

思路:比较相邻两部电影的间隔 $gap$——合并进当前订阅花费 $gap$,新开订阅花费 $K+1$(订 1 天付 $1+K$)。当 $gap > K + 1$ 时分开更划算,否则合并。

正确性(交换论证):$gap \le K+1$ 时合并费用 $gap+K \le 2(K+1)$ 不劣于两次独立订阅;$gap > K+1$ 时分开严格更优。每个间隔的决策互不影响,局部最优叠加即全局最优。

n, k = map(int, input().split())
arr = [int(x) for x in input().split()]
ans = k + 1

for i in range(1, n):
    if arr[i] - arr[i - 1] <= k + 1:
        ans += arr[i] - arr[i-1]
    else:
        ans += k + 1

print(ans)

复杂度:时间 O(n),空间 O(1)

关键点:每个订阅必须从某个观影日期开始;第一部电影也需要新订阅(费用 $K+1$)。

例题 5:Cow Tipping(从右下角逆序处理)

来源:USACO Bronze - Cow Tipping

$N \times N$ 格子($N \leq 10$),1 表示躺下。每次操作翻转左上角 (0,0)、右下角 (r,c) 的矩形。求最少操作次数使全变 0。

思路:从右下角 (N-1, N-1) 开始逐格处理——若当前格是 1,必须操作以它为右下角的矩形(这是唯一能改变它且不影响已处理格子的方式),计数 +1。

正确性:操作只影响左上方向,从右下开始保证已处理的格子不被破坏;每个 1 至少需要一次包含它的操作,贪心恰好达到该下界。

TIPPED = "0"

def flip(r: int, c: int, cows):
    if cows[r][c]:
        for ri in range(r + 1):
            for ci in range(c + 1):
                cows[ri][ci] = not cows[ri][ci]
        return True
    return False

with open("cowtip.in") as read:
    width = int(read.readline())
    cows = []
    for _ in range(width):
        row = read.readline()
        to_add = []
        for c in range(width):
            to_add.append(row[c] != TIPPED)
        cows.append(to_add)

min_flips = 0
x = width - 1
y = width - 1

while x >= 0 and y >= 0:
    min_flips += flip(x, y, cows)
    if x > 0:
        x -= 1
    else:
        y -= 1
        x = y

print(min_flips, file=open("cowtip.out", "w"))

复杂度:时间 O(N³)(最多 $N^2$ 次操作、每次影响 $O(N^2)$ 格),空间 O(N²)

例题 6:Photo Shoot 2(只能左移的重排)

来源:USACO Bronze - Photo Shoot 2

给定两个长度为 N 的排列 a 和 b。每次操作可将一头牛向左移动任意多位置(不能向右)。求最少操作次数把 a 变成 b。

思路:双指针从左到右匹配目标排列——j 扫过当前排列中未被移动的牛;若第一个未移动的牛恰好是目标牛则指针后移,否则这头目标牛必须从后面移动过来,计数 +1 并标记 moved

正确性:只能左移,所以从前向后匹配是唯一策略;每头需要移动的牛至少操作一次,贪心恰好达到该下界。

n = int(input())
arr = [int(x) for x in input().split()]
target = [int(x) for x in input().split()]
moved = [False] * (n + 1)
ans = 0
j = 0
for i in range(n):
    while j < n and moved[arr[j]]:
        j += 1
    if j >= n:
        break
    if target[i] == arr[j]:
        j += 1
    else:
        ans += 1
        moved[target[i]] = True

print(ans)

复杂度:时间 O(n),空间 O(n)

关键点:moved 数组避免重复计已移动的牛;指针 j 单调右移保证整体线性。

总结

  • 适用条件:最优子结构 + 贪心选择性质
  • 先排序,再贪心;按结束时间排序是活动安排类问题的万能起点
  • 用交换参数证法证明正确性
  • 遇到硬币找零、背包等经典反例场景,果断换 DP

贪心算法
https://mingsm17518.github.io/2026/09/20/刷题笔记/贪心/贪心算法/
作者
Ming
发布于
2026年9月20日
更新于
2026年9月20日
许可协议