文章

贪心算法详解:从理论到实践

一、贪心算法概述

1. 什么是贪心算法

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最好或最优的选择,从而期望导致结果是全局最优解的算法策略。它的核心是:

  • 通过局部最优选择
  • 期望达到全局最优解
  • 一旦做出选择,不再回退

2. 算法核心特征

  1. 贪心选择性质
    • 每步选择都是当前状态下最优的
    • 不考虑后续影响
    • 不会回溯或修改之前的选择
  2. 最优子结构
    • 问题的最优解包含子问题的最优解
    • 每个子问题的解决都是局部最优的
  3. 无后效性
    • 当前决策不会影响之前的状态
    • 每个决策都是独立的

3. 与其他算法的本质区别

  1. 贪心 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]
    }
    

    区别:

    • 贪心:每步只选最优,不回头
    • 动态规划:保存所有状态,找全局最优
  2. 贪心 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. 问题特征
    • 问题可以分解为子问题
    • 局部最优可能导致全局最优
    • 每步选择都是独立的
  2. 适用场景
    • 最优化问题
    • 可以通过局部选择达到最优
    • 子问题之间相对独立
  3. 验证方法
    • 尝试反证法
    • 数学归纳法
    • 举反例验证

二、核心概念详解

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. 实现建议:

  1. 先证明贪心策略的正确性
  2. 考虑边界情况
  3. 选择合适的数据结构
  4. 注意性能优化
  5. 完善测试用例

3. 常见错误:

  1. 没有证明贪心策略的正确性
  2. 忽略了特殊情况
  3. 性能考虑不足

参考资源

  1. 《算法导论》第16章 贪心算法
  2. LeetCode 贪心专题
  3. Swift 标准库文档
  4. 《编程珠玑》贪心算法案例
本文由作者按照 CC BY 4.0 进行授权