CC++ & Algorithm

贪心典型应用:活动选择与背包

较难2
语言版本:C++Python
概述:活动选择、部分背包问题等经典场景中,贪心策略能高效找到最优或近似最优解。

贪心算法实战:如何安排活动最多、背包最值钱?

你有没有遇到过这样的烦恼?一天有好多活动想去参加,可时间冲突,只能选一部分;或者你有个书包,想装进最值钱的东西,但每种东西可以只拿一部分。贪心算法就是帮你做出“当前最好选择”的方法,它每一步都挑眼前最有利的,最终得到很好的结果。今天我们就用两个经典问题来学会它。


活动选择:怎样参加最多活动?

问题:一天有多个活动,每个活动有开始时间和结束时间。你只能参加时间不重叠的活动,怎么选才能参加的数量最多?

贪心策略:每次选结束时间最早的活动。因为结束早,后面的时间更充裕,就有机会参加更多活动。

为什么选结束最早就对了?

想象你从早上8点到晚上10点,有一堆活动:

  • 活动A:8:00 - 9:00
  • 活动B:8:30 - 10:00
  • 活动C:9:30 - 10:30

如果先选A(最早结束8:00-9:00),剩下时间可以选C(9:30-10:30),一共2个。
如果先选B(8:30-10:00),整天只能参加这一个。
所以选最早结束的活动,给后面留出最多时间。

换个例子更好懂

比如你和朋友约好做几件事:

  1. 看电影(1点开始,3点结束)
  2. 打篮球(2点开始,4点结束)
  3. 写作业(3点开始,5点结束)
  4. 吃火锅(0点开始,6点结束)

按结束时间排序:看电影(结束3点)→ 打篮球(结束4点)→ 写作业(结束5点)→ 吃火锅(结束6点)。
选结束最早的“看电影”,然后下一个只能在3点之后开始——剩下“写作业”(3-5点)可选。再下一个只能在5点之后——没活动了。一共2个。
如果先选“吃火锅”(0-6点),只能参加这一个。贪心帮我们找到最多2个活动。

Python代码实现

def max_activities(activities):
    # activities: 列表,每个元素为 (开始时间, 结束时间)
    activities.sort(key=lambda x: x[1])  # 按结束时间升序排序
    count = 0
    last_end = 0  # 上一个选中的活动的结束时间
    for start, end in activities:
        if start >= last_end:  # 如果当前活动开始时间不早于上一个结束
            count += 1
            last_end = end
    return count

# 测试
acts = [(1,3), (2,4), (3,5), (0,6)]
print("最多能参加的活动数:", max_activities(acts))  # 输出2

部分背包问题:怎样装最值钱?

问题:你有一个容量固定的背包,有若干种物品(每种有重量和价值),而且物品可以只拿一部分(比如半块黄金、半袋米)。怎样装能让总价值最大?

贪心策略:每次选单位重量价值最高的物品,优先拿它,直到背包装满。

单位价值是什么?

单位价值 = 价值 ÷ 重量。就是“每斤值多少钱”。
比如黄金:重量10斤,价值600元,单位价值 = 600/10 = 60元/斤
白银:20斤,价值800元,单位价值 = 800/20 = 40元/斤
钻石:30斤,价值900元,单位价值 = 900/30 = 30元/斤

显然先拿黄金最划算。

生活例子:食堂打饭

你有一个饭盒容量500毫升。三种菜:

  • 红烧肉:200毫升,热量800卡(单位热量4卡/毫升)
  • 青菜:100毫升,热量200卡(单位热量2卡/毫升)
  • 米饭:300毫升,热量450卡(单位热量1.5卡/毫升)

贪心:先装红烧肉(全部200毫升,得800卡),剩余容量300毫升;再装青菜(全部100毫升,得200卡),剩余200毫升;最后装米饭(只能装200/300 = 2/3份,得450×2/3 = 300卡)。总热量 = 800+200+300 = 1300卡。
如果先装米饭(全部300毫升,得450卡),剩余200毫升装红烧肉(200毫升,得800卡),总热量1250卡,不如贪心方案。

Python代码实现

def fractional_knapsack(items, capacity):
    # items: 列表,每个元素为 (重量, 价值)
    # 按单位价值(价值/重量)降序排序
    items.sort(key=lambda x: x[1] / x[0], reverse=True)
    total_value = 0
    for weight, value in items:
        if capacity >= weight:
            total_value += value
            capacity -= weight
        else:
            fraction = capacity / weight
            total_value += value * fraction
            break
    return total_value

# 测试
items = [(10,60), (20,100), (30,120)]  # (重量, 价值)
cap = 50
print("最大价值:", fractional_knapsack(items, cap))  # 输出240.0

新手常犯的错误

  1. 活动选择的排序搞错了
    有的人按开始时间排序,或者按活动时长排序,这样可能得不到最多活动。必须按结束时间最早的顺序选。

  2. 部分背包问题中搞混“单位价值”和“总价值”
    不能只看总价值最高的物品,要看“每斤值多少钱”。比如钻石总价值120很高,但单位价值只有4,不如黄金的6。所以先拿黄金。

  3. 忘记部分背包和0-1背包的区别

    • 部分背包:物品可以切分,贪心有效。
    • 0-1背包:物品只能整个拿或不拿,贪心不一定最优(比如容量50,物品A: (30,60)单位2,B: (20,100)单位5,C: (20,90)单位4.5,贪心先拿B和C得190,但拿A+C得150,而最优是B+A得160?需验证——其实贪心在这情况不一定对)。所以0-1背包要用动态规划。
      很多同学把0-1背包也当部分背包去贪心,导致答案错误。
  4. 排序后没考虑容量可能装不下整个物品
    在部分背包中,最后一种物品可能只装一部分,代码中要用 fraction 计算,不能直接 total_value += value


完整可运行的代码示例

下面把活动选择和部分背包合在一起展示,并加入更多测试数据。

# ---------- 活动选择 ----------
def max_activities(activities):
    # activities: 元组列表 (开始, 结束)
    activities.sort(key=lambda x: x[1])  # 按结束时间升序
    count = 0
    last_end = 0
    for start, end in activities:
        if start >= last_end:
            count += 1
            last_end = end
    return count

# 测试活动
acts = [(1,3), (2,4), (3,5), (0,6), (4,7), (5,8)]
print("最多活动数:", max_activities(acts))  # 输出3 (选(1,3),(3,5),(5,8))

# ---------- 部分背包 ----------
def fractional_knapsack(items, capacity):
    # items: 元组列表 (重量, 价值)
    items.sort(key=lambda x: x[1] / x[0], reverse=True)  # 按单位价值降序
    total_value = 0
    for weight, value in items:
        if capacity >= weight:
            total_value += value
            capacity -= weight
        else:
            fraction = capacity / weight
            total_value += value * fraction
            break
    return total_value

# 测试物品
stuff = [(10,60), (20,100), (30,120)]  # (重量,价值)
cap = 50
print("最大价值:", fractional_knapsack(stuff, cap))  # 240.0

# 再加一个测试:水果装袋
fruits = [(1,5), (2,8), (3,10)]  # 重量kg, 价值元
bag = 4
print("水果最大价值:", fractional_knapsack(fruits, bag))  # 单位价值:5,4,3.33 → 先拿1kg水果(5),再拿2kg(8),最后拿1/3的3kg(3.33) → 5+8+3.33=16.33

运行结果:

最多活动数: 3
最大价值: 240.0
水果最大价值: 16.333333333333332

还想学更多?

贪心算法还有很多经典应用:

  • 哈夫曼编码:用最短的二进制码表示字符,节省存储空间。
  • 最小生成树(Prim、Kruskal算法):修路怎样最省钱。
  • 加油站问题:开车跑长途,怎样加油最少。

但贪心不是万能的,遇到0-1背包、**找零钱(某些币值)**等题目,就需要动态规划或回溯法。建议你学完贪心后,再接触动态规划,对比两者的区别。

试一试:如果活动可以中断(参加一半就走),还能用贪心吗?想一想,欢迎自己写代码验证!

例题精讲

1单选题

在活动选择问题中,贪心算法通常依据哪个原则来选择活动?

A选择开始时间最早的活动
B选择结束时间最早的活动
C选择持续时间最短的活动
D选择参与人数最多的活动
2单选题

对于部分背包问题(物品可分),贪心算法应按照哪个指标进行排序?

A物品重量从小到大
B物品价值从大到小
C物品单位价值(价值/重量)从大到小
D物品价值与重量的乘积从大到小
3判断题

在活动选择问题中,如果按照开始时间最早的原则进行贪心选择,同样可以得到活动数量最多的最优解。

4填空题
以下代码是活动选择问题的贪心实现,请补全循环中判断活动是否可选的空缺部分。

def activity_selection(start, finish):
    n = len(start)
    activities = sorted(zip(start, finish), key=lambda x: x[1])  # 按结束时间排序
    selected = [activities[0]]
    last_finish = activities[0][1]
    for i in range(1, n):
        if activities[i][0] ___ last_finish:  # 填空处
            selected.append(activities[i])
            last_finish = activities[i][1]
    return selected
5填空题
以下代码是实现部分背包问题的贪心算法,请补全排序的 key 以及循环中装入物品的部分。

def fractional_knapsack(weights, values, capacity):
    n = len(weights)
    items = [(values[i], weights[i], values[i]/weights[i]) for i in range(n)]
    items.sort(key=lambda x: ___, reverse=True)  # 填空1
    total_value = 0.0
    for value, weight, ratio in items:
        if capacity >= weight:
            total_value += value
            capacity -= weight
        else:
            total_value += ___  # 填空2
            break
    return total_value