贪心算法经典应用:活动选择问题(C++实现)

问题描述

假设有 n 个活动,每个活动都有开始时间和结束时间。我们的目标是选择尽可能多的活动,使得它们在时间上不冲突(即任意两个活动的时间区间不重叠)。

这是一个经典的贪心算法应用场景,也被称为"区间调度问题"。

解题思路

核心思想

在每一步都做出当前看起来最好的选择,最终得到全局最优解。

贪心策略选择

关键问题:按照什么标准来贪心选择?

可能的策略:

  1. 选择持续时间最短的活动
  2. 选择开始时间最早的活动
  3. 选择结束时间最早的活动 ✅

为什么选择结束时间最早的活动是最优的?

  • 直观理解:越早结束,留给后续活动的时间就越多

  • 形式化证明:假设存在一个最优解不包含最早结束的活动,我们可以用最早结束的活动替换其中某个活动,得到同样优或更优的解

    时间轴示例:

    a₁: |-----| (结束时间最早)
    aᵢ₁: |-------| (原最优解的第一个活动)
    aᵢ₂: |-------| (原最优解的第二个活动)

    关键点:

    • aᵢ₁ 与 aᵢ₂ 不冲突 → aᵢ₂.start ≥ aᵢ₁.finish
    • a₁.finish ≤ aᵢ₁.finish → aᵢ₂.start ≥ a₁.finish
    • 所以 a₁ 与 aᵢ₂ 也不冲突!

算法步骤

  1. 将所有活动按照结束时间升序排序
  2. 选择第一个活动(结束时间最早的)
  3. 从剩余活动中,选择开始时间不早于上一个已选活动结束时间的活动
  4. 重复步骤3,直到没有可选活动

时间复杂度

  • 排序:O(n log n)
  • 选择过程:O(n)
  • 总体:O(n log n)

C++ 实现

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>

// 活动结构体
struct Activity {
    int start;  // 开始时间
    int finish; // 结束时间
    int id;     // 活动编号(用于输出)
    
    // 构造函数
    Activity(int s, int f, int i) : start(s), finish(f), id(i) {}
};

// 比较函数:按结束时间升序排序
bool compareActivities(const Activity& a, const Activity& b) {
    return a.finish < b.finish;
}

// 贪心活动选择算法
std::vector<Activity> activitySelection(std::vector<Activity>& activities) {
    // 1. 按结束时间排序
    std::sort(activities.begin(), activities.end(), compareActivities);
    
    std::vector<Activity> selected;
    
    // 2. 选择第一个活动(结束时间最早的)
    selected.push_back(activities[0]);
    int lastSelectedIndex = 0;
    
    // 3. 贪心选择后续活动
    for (int i = 1; i < activities.size(); i++) {
        // 如果当前活动的开始时间 >= 上一个选中活动的结束时间
        if (activities[i].start >= activities[lastSelectedIndex].finish) {
            selected.push_back(activities[i]);
            lastSelectedIndex = i;
        }
    }
    
    return selected;
}

// 打印活动信息
void printActivities(const std::vector<Activity>& activities) {
    std::cout << "选中的活动:\n";
    std::cout << "活动ID\t开始时间\t结束时间\n";
    for (const auto& activity : activities) {
        std::cout << activity.id << "\t" << activity.start << "\t\t" << activity.finish << "\n";
    }
}

int main() {
    // 测试数据:每个活动的 {开始时间, 结束时间, ID}
    std::vector<Activity> activities = {
        Activity(1, 4, 1),
        Activity(3, 5, 2),
        Activity(0, 6, 3),
        Activity(5, 7, 4),
        Activity(3, 9, 5),
        Activity(5, 9, 6),
        Activity(6, 10, 7),
        Activity(8, 11, 8),
        Activity(8, 12, 9),
        Activity(2, 14, 10),
        Activity(12, 16, 11)
    };
    
    std::cout << "原始活动列表:\n";
    std::cout << "活动ID\t开始时间\t结束时间\n";
    for (const auto& act : activities) {
        std::cout << act.id << "\t" << act.start << "\t\t" << act.finish << "\n";
    }
    
    std::cout << "\n" << std::string(40, '-') << "\n";
    
    // 执行活动选择算法
    std::vector<Activity> selected = activitySelection(activities);
    
    // 输出结果
    printActivities(selected);
    std::cout << "\n总共选择了 " << selected.size() << " 个活动\n";
    
    return 0;
}

算法正确性证明(简要)

贪心选择性质:存在一个最优解包含最早结束的活动。

证明思路

  • 设 S 是一个最优解,其中第一个活动是 a_k(不是最早结束的 a_1)
  • 由于 a_1 结束时间 ≤ a_k 结束时间,所以用 a_1 替换 a_k 后,不会与 S 中其他活动冲突
  • 因此 S' = (S - {a_k}) ∪ {a_1} 也是一个最优解,且包含贪心选择

最优子结构:在做出贪心选择后,剩余子问题的最优解与贪心选择组合构成原问题的最优解。

运行结果示例

复制代码
原始活动列表:
活动ID	开始时间	结束时间
1	1		4
2	3		5
3	0		6
4	5		7
5	3		9
6	5		9
7	6		10
8	8		11
9	8		12
10	2		14
11	12		16

----------------------------------------
选中的活动:
活动ID	开始时间	结束时间
1	1		4
4	5		7
8	8		11
11	12		16

总共选择了 4 个活动

扩展思考

  1. 变种问题:如果活动有不同权重,目标是最大化总权重而不是活动数量,这时贪心算法不再适用,需要使用动态规划。

  2. 实际应用

    • 会议室调度
    • 任务调度
    • 广告插播安排
    • 课程安排
  3. 为什么贪心有效 :这个问题具有贪心选择性质最优子结构,这是贪心算法适用的关键条件。

相关推荐
暗然而日章2 小时前
C++基础:Stanford CS106L学习笔记 15 RAII&智能指针&构建C++工程
c++·笔记·学习
光羽隹衡2 小时前
决策树项目——电信客户流失预测
算法·决策树·机器学习
TL滕2 小时前
从0开始学算法——第二十一天(高级链表操作)
笔记·学习·算法
CoovallyAIHub2 小时前
无人机低空视觉数据集全景解读:从单机感知到具身智能的跨越
深度学习·算法·计算机视觉
学编程就要猛2 小时前
算法:1.移动零
java·算法
杜子不疼.2 小时前
【LeetCode 35 & 69_二分查找】搜索插入位置 & x的平方根
算法·leetcode·职场和发展
YYDS3142 小时前
次小生成树
c++·算法·深度优先·图论·lca最近公共祖先·次小生成树
xu_yule2 小时前
算法基础(区间DP)
数据结构·c++·算法·动态规划·区间dp
天骄t2 小时前
信号VS共享内存:进程通信谁更强?
算法