贪心算法详解:从理论到实践
一、贪心算法概述
1. 什么是贪心算法
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最好或最优的选择,从而期望导致结果是全局最优解的算法策略。它的核心是:
- 通过局部最优选择
- 期望达到全局最优解
- 一旦做出选择,不再回退
2. 算法核心特征
- 贪心选择性质:
- 每步选择都是当前状态下最优的
- 不考虑后续影响
- 不会回溯或修改之前的选择
- 最优子结构:
- 问题的最优解包含子问题的最优解
- 每个子问题的解决都是局部最优的
- 无后效性:
- 当前决策不会影响之前的状态
- 每个决策都是独立的
3. 与其他算法的本质区别
- 贪心 vs 动态规划:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// 贪心算法解决硬币问题 func greedyCoinChange(_ amount: Int, _ coins: [Int]) -> Int { var remaining = amount var count = 0 // 每次都选择最大面额 for coin in coins.sorted(by: >) { count += remaining / coin remaining %= coin } return count } // 动态规划解决硬币问题 func dpCoinChange(_ amount: Int, _ coins: [Int]) -> Int { var dp = Array(repeating: amount + 1, count: amount + 1) dp[0] = 0 // 考虑所有可能的组合 for i in 1...amount { for coin in coins { if coin <= i { dp[i] = min(dp[i], dp[i - coin] + 1) } } } return dp[amount] }
区别:
- 贪心:每步只选最优,不回头
- 动态规划:保存所有状态,找全局最优
- 贪心 vs 回溯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
// 贪心选择活动 func greedyActivity(_ activities: [(start: Int, end: Int)]) -> Int { let sorted = activities.sorted { $0.end < $1.end } var count = 0 var lastEnd = 0 for activity in sorted { if activity.start >= lastEnd { count += 1 lastEnd = activity.end } } return count } // 回溯尝试所有可能 func backtrackActivity(_ activities: [(start: Int, end: Int)]) -> Int { func backtrack(_ index: Int, _ current: [(start: Int, end: Int)]) -> Int { if index >= activities.count { return current.count } // 尝试选择或不选择当前活动 var max = backtrack(index + 1, current) if canAdd(activities[index], to: current) { max = Swift.max(max, backtrack(index + 1, current + [activities[index]])) } return max } return backtrack(0, []) }
区别:
- 贪心:一次选择,不能反悔
- 回溯:尝试所有可能,可以回退
4. 如何识别贪心问题
- 问题特征:
- 问题可以分解为子问题
- 局部最优可能导致全局最优
- 每步选择都是独立的
- 适用场景:
- 最优化问题
- 可以通过局部选择达到最优
- 子问题之间相对独立
- 验证方法:
- 尝试反证法
- 数学归纳法
- 举反例验证
二、核心概念详解
1. 贪心选择性质
1
2
3
4
5
6
7
8
9
10
11
12
13
// 示例:选择最大数字
func greedyMax(_ numbers: [Int]) -> Int {
// 贪心策略:直接选择最大值
return numbers.max() ?? 0
}
// 对比动态规划
func dpMax(_ numbers: [Int]) -> Int {
// 需要考虑各种组合
var dp = Array(repeating: 0, count: numbers.count)
// ... 复杂的状态转移
return dp.last ?? 0
}
2. 最优子结构
当问题的最优解包含其子问题的最优解时,称该问题具有最优子结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 示例:找零钱问题
func makeChange(amount: Int, coins: [Int]) -> Int {
var remaining = amount
var count = 0
// 贪心:每次选择最大面额
for coin in coins.sorted(by: >) {
count += remaining / coin // 直接使用最大面额
remaining %= coin // 更新剩余金额
}
return count
}
3. 无后效性
一旦做出选择,不会影响之前的选择。
三、经典问题详解
1. 会议室安排问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct Meeting {
let id: Int
let start: Int
let end: Int
}
class MeetingScheduler {
func schedule(_ meetings: [Meeting]) -> [Meeting] {
// 1. 按结束时间排序
let sortedMeetings = meetings.sorted { $0.end < $1.end }
var result: [Meeting] = []
var lastEndTime = 0
// 2. 贪心选择:每次选择结束最早的会议
for meeting in sortedMeetings {
if meeting.start >= lastEndTime {
result.append(meeting)
lastEndTime = meeting.end
}
}
return result
}
}
// 使用示例
let meetings = [
Meeting(id: 1, start: 9, end: 10),
Meeting(id: 2, start: 9, end: 12),
Meeting(id: 3, start: 10, end: 11),
Meeting(id: 4, start: 11, end: 12)
]
let scheduler = MeetingScheduler()
let scheduled = scheduler.schedule(meetings)
// 输出:[会议1, 会议3, 会议4]
2. 分糖果问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CandyDistributor {
// 每个孩子至少分到一颗糖果
// 评分高的孩子必须比相邻的获得更多的糖果
func distribute(_ ratings: [Int]) -> Int {
let n = ratings.count
var candies = Array(repeating: 1, count: n)
// 从左向右遍历
for i in 1..<n {
if ratings[i] > ratings[i-1] {
candies[i] = candies[i-1] + 1
}
}
// 从右向左遍历
for i in (0..<n-1).reversed() {
if ratings[i] > ratings[i+1] {
candies[i] = max(candies[i], candies[i+1] + 1)
}
}
return candies.reduce(0, +)
}
}
四、实际应用场景
1. 任务调度
1
2
3
4
5
6
7
8
9
10
11
12
struct Task {
let id: String
let priority: Int
let duration: Int
}
class TaskScheduler {
func schedule(_ tasks: [Task]) -> [Task] {
// 贪心策略:按优先级排序
return tasks.sorted { $0.priority > $1.priority }
}
}
2. 资源分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Resource {
let id: String
let efficiency: Double
let cost: Double
}
class ResourceAllocator {
func allocate(budget: Double, resources: [Resource]) -> [Resource] {
// 贪心策略:按性价比(效率/成本)排序
return resources
.sorted { $0.efficiency/$0.cost > $1.efficiency/$1.cost }
.filter { /* 预算限制 */ }
}
}
五、常见陷阱和解决方案
1. 局部最优导致全局次优
1
2
3
4
5
// 反例:硬币问题的特殊情况
let coins = [1, 3, 4]
let amount = 6
// 贪心:4 + 1 + 1 = 6 (需要3个硬币)
// 最优:3 + 3 = 6 (只需要2个硬币)
2. 处理边界情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GreedySolver {
func solve(_ input: [Int]) -> Int {
// 1. 空输入处理
guard !input.isEmpty else { return 0 }
// 2. 单元素处理
guard input.count > 1 else { return input[0] }
// 3. 特殊值处理
if input.contains(where: { $0 < 0 }) {
// 处理负数情况
}
// 正常逻辑
return result
}
}
六、性能优化技巧
1. 数据预处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OptimizedGreedy {
private func preprocess(_ data: [Int]) -> [Int] {
// 1. 排序
let sorted = data.sorted()
// 2. 去重
let unique = Array(Set(sorted))
// 3. 预计算
let preprocessed = unique.map { /* 预处理逻辑 */ }
return preprocessed
}
}
2. 使用高效的数据结构
1
2
3
4
5
6
7
8
9
10
11
12
class EfficientGreedy {
// 使用堆来维护优先级
private var priorityQueue: Heap<Int>
// 使用哈希表加速查找
private var lookup: [Int: Int]
init() {
self.priorityQueue = Heap<Int>(sort: >)
self.lookup = [:]
}
}
七、调试和测试策略
1. 验证贪心策略
1
2
3
4
5
6
7
8
9
class GreedyValidator {
func validate(_ solution: [Int], against bruteForce: [Int]) -> Bool {
// 1. 结果正确性
guard solution.count == bruteForce.count else { return false }
// 2. 最优性验证
return solution.reduce(0, +) <= bruteForce.reduce(0, +)
}
}
2. 测试用例设计
1
2
3
4
5
6
7
8
9
10
11
12
class GreedyTester {
func runTests() {
// 1. 基本测试
testBasicCases()
// 2. 边界测试
testEdgeCases()
// 3. 压力测试
testLargeInput()
}
}
八、总结与最佳实践
1. 使用贪心算法的检查清单:
- 问题是否具有贪心选择性质?
- 是否具有最优子结构?
- 是否需要证明贪心策略的正确性?
- 是否考虑了所有边界情况?
2. 实现建议:
- 先证明贪心策略的正确性
- 考虑边界情况
- 选择合适的数据结构
- 注意性能优化
- 完善测试用例
3. 常见错误:
- 没有证明贪心策略的正确性
- 忽略了特殊情况
- 性能考虑不足
参考资源
- 《算法导论》第16章 贪心算法
- LeetCode 贪心专题
- Swift 标准库文档
- 《编程珠玑》贪心算法案例
本文由作者按照 CC BY 4.0 进行授权