贪心算法(三)
引言
贪心算法(Greedy Algorithm)是一种常见的算法策略,常用于优化问题的求解。它的核心思想是:在每一步选择中都采取当前状态下最优(或最有利)的选择,从而希望通过局部的最优选择最终达到全局的最优。然而,贪心算法并不总能保证得到问题的最优解,它的有效性往往取决于问题的具体性质。
在本篇文章中,我们将深入探讨贪心算法的应用,介绍其思想、特点,并通过几个典型问题来展示贪心算法如何有效地解决实际问题。
贪心算法的基本思想
贪心算法的基本步骤可以归纳为以下几点:
- 选择最优策略:在当前的选择阶段,做出局部最优选择,即选择当前可行解中最优的一个。
- 更新状态:通过选择最优策略后,更新问题的状态,缩小问题的规模,去掉已处理部分。
- 重复选择:继续重复选择最优策略,直到达到终止条件,得到最终解。
贪心算法之所以能在某些问题中高效解决问题,是因为它每一步都选择局部最优解,尽管这种方式不能保证全局最优解,但在很多问题中,贪心选择往往能给出接近最优的解,并且计算效率较高。
贪心算法的特点
贪心算法的应用并非适用于所有问题,适用的情况通常满足以下特点:
- 贪心选择性质:问题的全局最优解可以通过局部最优解来推导。
- 无后效性:当前的选择不会影响之后的决策,也就是说,做出的选择不需要回溯。
- 最优子结构:问题的最优解包含了子问题的最优解。也就是说,问题可以被分解为若干个子问题,子问题的最优解可以合成成整个问题的最优解。
贪心算法常见应用场景
贪心算法适用于很多优化问题,特别是那些具有最优子结构和贪心选择性质的问题。以下是一些常见的贪心算法应用场景:
- 最短路径问题(如Dijkstra算法)
- 背包问题(分数背包问题)
- 区间调度问题
- 哈夫曼编码
- 活动选择问题
- 贪心图着色问题
接下来,我们将通过具体的案例来详细探讨这些应用。
案例一:活动选择问题
问题描述
假设有一系列活动,它们各自有开始时间和结束时间。每个活动都可以在某个时间段内进行,但是一个活动的开始时间必须在另一个活动的结束时间之后才能执行。如何选择尽可能多的活动,使得所有选定的活动都不会重叠?
贪心算法思路
- 按照活动的结束时间升序排序。
- 从最早结束的活动开始,选择该活动,并且删除所有与其重叠的活动。
- 继续选择下一个不重叠的活动,直到所有活动都被处理完。
解决方案
pythonCopy Codedef activity_selection(start, finish):
n = len(start)
selected_activities = []
# 按照结束时间进行排序
activities = sorted(zip(start, finish), key=lambda x: x[1])
# 选择第一个活动
selected_activities.append(activities[0])
last_finish_time = activities[0][1]
# 遍历剩下的活动,选择不重叠的活动
for i in range(1, n):
if activities[i][0] >= last_finish_time:
selected_activities.append(activities[i])
last_finish_time = activities[i][1]
return selected_activities
示例
假设活动的开始时间和结束时间分别为:
pythonCopy Codestart = [1, 3, 0, 5, 8, 5]
finish = [2, 4, 6, 7, 9, 9]
执行 activity_selection(start, finish)
会得到最大数量的活动选择。
贪心选择过程
- 按照结束时间排序:
[(1, 2), (3, 4), (0, 6), (5, 7), (8, 9), (5, 9)]
- 选择第一个活动
(1, 2)
,更新最后结束时间为 2。 - 选择下一个不重叠的活动
(3, 4)
,更新最后结束时间为 4。 - 选择下一个不重叠的活动
(5, 7)
,更新最后结束时间为 7。 - 选择下一个不重叠的活动
(8, 9)
,更新最后结束时间为 9。
最终选择的活动为 [(1, 2), (3, 4), (5, 7), (8, 9)]
,共选择了 4 个活动。
复杂度分析
- 排序时间复杂度为
O(n log n)
,其中n
是活动的数量。 - 遍历选择活动的时间复杂度为
O(n)
。
因此,总时间复杂度为 O(n log n)
。
案例二:背包问题(分数背包)
问题描述
给定一组物品,每个物品都有重量和价值,并且有一个背包,背包的最大容量为 W
。目标是选择物品装入背包,使得装入背包的物品总价值最大,并且背包中的物品总重量不能超过最大容量 W
。在分数背包问题中,每个物品可以选择部分装入(即可以分割物品)。
贪心算法思路
- 计算每个物品的单位重量价值,即
value/weight
。 - 按照单位重量价值从大到小排序物品。
- 从单位价值最大的物品开始装入背包,直到背包容量达到限制。
解决方案
pythonCopy Codedef fractional_knapsack(weights, values, capacity):
n = len(weights)
items = []
# 计算每个物品的单位价值
for i in range(n):
items.append((values[i] / weights[i], weights[i], values[i]))
# 按单位价值降序排序
items.sort(reverse=True, key=lambda x: x[0])
total_value = 0
for item in items:
if capacity == 0:
break
if item[1] <= capacity:
total_value += item[2]
capacity -= item[1]
else:
total_value += item[2] * (capacity / item[1])
capacity = 0
return total_value
示例
假设我们有以下物品和背包容量:
pythonCopy Codeweights = [10, 40, 20, 30]
values = [60, 40, 100, 120]
capacity = 50
执行 fractional_knapsack(weights, values, capacity)
会得到最大价值。
贪心选择过程
- 计算每个物品的单位价值:
[6.0, 1.0, 5.0, 4.0]
- 按单位价值排序:
[(6.0, 10, 60), (5.0, 20, 100), (4.0, 30, 120), (1.0, 40, 40)]
- 从第一个物品开始:
- 物品
(10, 60)
,装入背包,剩余容量 40,背包价值为 60。 - 物品
(20, 100)
,装入背包,剩余容量 20,背包价值为 160。 - 物品
(30, 120)
,只能装入部分,装入 20,背包价值为 160 + 80 = 240。
- 物品
最终的最大价值为 240。
复杂度分析
- 计算每个物品的单位价值时间复杂度为
O(n)
。 - 排序时间复杂度为
O(n log n)
。 - 遍历物品的时间复杂度为
O(n)
。
因此,总时间复杂度为 O(n log n)
。
案例三:最短路径问题(Dijkstra算法)
问题描述
给定一个有向图,每个边都有一个非负权重,要求从一个源点出发,找到到所有其他节点的最短路径。Dijkstra算法是一种贪心算法,它通过每次选择当前已知最短路径的节点来计算最短路径。
贪心算法思路
- 初始化源点到其他所有点的距离为无穷大,源点到自己的距离为 0。
- 每次选择当前未处理节点中距离源