当前位置: 首页 > news >正文

贪心算法应用:活动选择问题详解

在这里插入图片描述

Java中的贪心算法应用:活动选择问题详解

贪心算法是一种在每一步选择中都采取当前状态下最优的选择,从而希望导致结果是全局最优的算法策略。活动选择问题是贪心算法的一个经典应用场景。下面我将从多个维度全面详细地讲解这个问题及其Java实现。

一、问题定义

1.1 问题描述

活动选择问题(Activity Selection Problem)是指给定一组活动,每个活动都有一个开始时间和结束时间,要求从中选择出最大数量的互不冲突的活动(即这些活动的时间段不重叠)。

1.2 形式化定义

给定:

  • n个活动的集合S = {a₁, a₂, …, aₙ}
  • 每个活动aᵢ有一个开始时间sᵢ和结束时间fᵢ,其中0 ≤ sᵢ < fᵢ < ∞

求:

  • 最大兼容活动子集A ⊆ S,其中对于任意两个活动aᵢ, aⱼ ∈ A,有[sᵢ, fᵢ)和[sⱼ, fⱼ)不重叠

二、贪心算法解决思路

2.1 贪心选择性质

活动选择问题具有贪心选择性质,即局部最优选择能导致全局最优解。具体策略是:

  1. 按照结束时间对活动进行排序
  2. 总是选择结束时间最早的活动
  3. 然后排除与该活动时间冲突的所有活动
  4. 重复上述过程,直到没有活动可选

2.2 为什么贪心算法有效

贪心算法有效的关键在于:

  • 选择结束时间最早的活动可以为后续活动留下尽可能多的时间
  • 这种选择方式能确保最大数量的活动被安排
  • 数学上可以证明这种策略能得到最优解

三、Java实现详解

3.1 活动类定义

首先定义一个表示活动的类:

public class Activity {private String name;    // 活动名称private int startTime; // 开始时间private int endTime;   // 结束时间public Activity(String name, int startTime, int endTime) {this.name = name;this.startTime = startTime;this.endTime = endTime;}// Getterspublic String getName() { return name; }public int getStartTime() { return startTime; }public int getEndTime() { return endTime; }@Overridepublic String toString() {return name + "(" + startTime + "-" + endTime + ")";}
}

3.2 贪心算法实现

3.2.1 递归实现
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;public class ActivitySelection {// 递归实现public static List<Activity> selectActivitiesRecursive(List<Activity> activities) {// 先按照结束时间排序activities.sort(Comparator.comparingInt(Activity::getEndTime));List<Activity> selected = new ArrayList<>();// 第一个活动总是被选中(结束时间最早)selected.add(activities.get(0));// 递归选择剩余活动selectRecursive(activities, 0, selected);return selected;}private static void selectRecursive(List<Activity> activities, int lastSelectedIndex, List<Activity> selected) {// 找到下一个开始时间 >= 上一个选中活动结束时间的活动int nextIndex = lastSelectedIndex + 1;while (nextIndex < activities.size() && activities.get(nextIndex).getStartTime() < activities.get(lastSelectedIndex).getEndTime()) {nextIndex++;}// 如果找到符合条件的活动if (nextIndex < activities.size()) {selected.add(activities.get(nextIndex));selectRecursive(activities, nextIndex, selected);}// 否则递归终止}
}
3.2.2 迭代实现(更高效)
public static List<Activity> selectActivitiesIterative(List<Activity> activities) {// 先按照结束时间排序activities.sort(Comparator.comparingInt(Activity::getEndTime));List<Activity> selected = new ArrayList<>();// 第一个活动总是被选中selected.add(activities.get(0));int lastSelectedIndex = 0;// 遍历剩余活动for (int i = 1; i < activities.size(); i++) {// 如果当前活动的开始时间 >= 上一个选中活动的结束时间if (activities.get(i).getStartTime() >= activities.get(lastSelectedIndex).getEndTime()) {selected.add(activities.get(i));lastSelectedIndex = i;}}return selected;
}

3.3 测试代码

public static void main(String[] args) {List<Activity> activities = new ArrayList<>();activities.add(new Activity("A1", 1, 4));activities.add(new Activity("A2", 3, 5));activities.add(new Activity("A3", 0, 6));activities.add(new Activity("A4", 5, 7));activities.add(new Activity("A5", 3, 9));activities.add(new Activity("A6", 5, 9));activities.add(new Activity("A7", 6, 10));activities.add(new Activity("A8", 8, 11));activities.add(new Activity("A9", 8, 12));activities.add(new Activity("A10", 2, 14));activities.add(new Activity("A11", 12, 16));System.out.println("所有活动:");activities.forEach(System.out::println);System.out.println("\n递归选择结果:");List<Activity> selectedRecursive = selectActivitiesRecursive(activities);selectedRecursive.forEach(System.out::println);System.out.println("\n迭代选择结果:");List<Activity> selectedIterative = selectActivitiesIterative(activities);selectedIterative.forEach(System.out::println);
}

四、算法分析

4.1 时间复杂度

  1. 排序阶段:O(n log n) - 使用快速排序或归并排序
  2. 选择阶段:
    • 递归实现:最坏情况下O(n),因为每个活动最多被检查一次
    • 迭代实现:O(n),因为只需要一次遍历

总体时间复杂度为O(n log n),主要由排序阶段决定。

4.2 空间复杂度

  1. 递归实现:O(n) - 由于递归调用栈的深度在最坏情况下为n
  2. 迭代实现:O(1) - 除了结果存储外,只需要常数空间

五、算法正确性证明

贪心算法的正确性可以通过贪心选择性质和最优子结构性质来证明:

5.1 贪心选择性质

设活动按结束时间排序为a₁, a₂, …, aₙ,且f₁ ≤ f₂ ≤ … ≤ fₙ。

定理:存在一个最优解包含活动a₁。

证明
设A是一个最优解,且A中第一个活动是aₖ。

  • 如果k=1,定理成立
  • 如果k≠1,构造解A’ = (A - {aₖ}) ∪ {a₁}
  • 因为f₁ ≤ fₖ,A’中的活动都是兼容的
  • |A’| = |A|,所以A’也是最优解

5.2 最优子结构性质

设Sᵢⱼ = {aₖ ∈ S | fᵢ ≤ sₖ < fₖ ≤ sⱼ},即所有开始时间不早于aᵢ结束,结束时间不晚于aⱼ开始的活动。

定理:如果Aᵢⱼ是Sᵢⱼ的一个最优解,且aₖ ∈ Aᵢⱼ,则Aᵢⱼ = Aᵢₖ ∪ {aₖ} ∪ Aₖⱼ。

这表明问题具有最优子结构性质,即最优解包含子问题的最优解。

六、变种与扩展

6.1 加权活动选择

每个活动有一个权重,目标是选择权重和最大的兼容活动集。这时贪心算法不再适用,需要使用动态规划。

6.2 多资源活动选择

有多个资源(如多个会议室),需要安排尽可能多的活动。可以使用贪心算法结合优先队列。

6.3 最早开始时间优先

如果改为选择最早开始的活动,贪心算法可能得不到最优解。例如:

  • 活动A: 0-6
  • 活动B: 1-4
  • 活动C: 5-7

最早开始优先会选择A,而最优解是B和C。

七、实际应用场景

  1. 会议室安排:选择最多数量的会议安排在有限的会议室中
  2. 课程表安排:安排最多数量的课程不冲突
  3. 任务调度:在单处理器上调度最多数量的任务
  4. 电视节目安排:选择最多数量的电视节目观看而不冲突
  5. 交通调度:安排火车或飞机在跑道上的起降时间

八、完整Java实现示例

下面是一个更完整的实现,包含更多功能和测试用例:

import java.util.*;
import java.util.stream.Collectors;public class AdvancedActivitySelection {public static void main(String[] args) {// 测试用例1:标准情况List<Activity> activities1 = Arrays.asList(new Activity("A1", 1, 4),new Activity("A2", 3, 5),new Activity("A3", 0, 6),new Activity("A4", 5, 7),new Activity("A5", 3, 9),new Activity("A6", 5, 9),new Activity("A7", 6, 10),new Activity("A8", 8, 11),new Activity("A9", 8, 12),new Activity("A10", 2, 14),new Activity("A11", 12, 16));testSelection(activities1, "标准测试用例");// 测试用例2:所有活动都冲突List<Activity> activities2 = Arrays.asList(new Activity("B1", 1, 5),new Activity("B2", 2, 6),new Activity("B3", 3, 7),new Activity("B4", 4, 8));testSelection(activities2, "所有活动冲突的情况");// 测试用例3:没有活动冲突List<Activity> activities3 = Arrays.asList(new Activity("C1", 1, 2),new Activity("C2", 3, 4),new Activity("C3", 5, 6),new Activity("C4", 7, 8));testSelection(activities3, "没有活动冲突的情况");// 测试用例4:单个活动List<Activity> activities4 = Collections.singletonList(new Activity("D1", 1, 2));testSelection(activities4, "单个活动的情况");// 测试用例5:空列表testSelection(new ArrayList<>(), "空活动列表");}private static void testSelection(List<Activity> activities, String testCaseName) {System.out.println("\n=== " + testCaseName + " ===");System.out.println("所有活动 (" + activities.size() + "个):");activities.forEach(a -> System.out.println("  " + a));List<Activity> selected = selectActivities(activities);System.out.println("\n选择结果 (" + selected.size() + "个活动):");selected.forEach(a -> System.out.println("  " + a));}public static List<Activity> selectActivities(List<Activity> activities) {if (activities == null || activities.isEmpty()) {return new ArrayList<>();}// 防御性拷贝List<Activity> sortedActivities = new ArrayList<>(activities);// 按照结束时间排序sortedActivities.sort(Comparator.comparingInt(Activity::getEndTime));List<Activity> selected = new ArrayList<>();selected.add(sortedActivities.get(0));int lastSelectedIndex = 0;for (int i = 1; i < sortedActivities.size(); i++) {Activity current = sortedActivities.get(i);Activity lastSelected = sortedActivities.get(lastSelectedIndex);if (current.getStartTime() >= lastSelected.getEndTime()) {selected.add(current);lastSelectedIndex = i;}}return selected;}// 带权重的活动选择(动态规划解法)public static List<Activity> selectWeightedActivities(List<WeightedActivity> activities) {if (activities == null || activities.isEmpty()) {return new ArrayList<>();}// 按照结束时间排序List<WeightedActivity> sorted = new ArrayList<>(activities);sorted.sort(Comparator.comparingInt(WeightedActivity::getEndTime));int n = sorted.size();int[] dp = new int[n + 1];// dp[i]表示前i个活动的最大权重dp[0] = 0;dp[1] = sorted.get(0).getWeight();for (int i = 2; i <= n; i++) {WeightedActivity current = sorted.get(i - 1);int latestNonConflict = findLatestNonConflict(sorted, i - 1);int includeWeight = current.getWeight();if (latestNonConflict != -1) {includeWeight += dp[latestNonConflict + 1];}int excludeWeight = dp[i - 1];dp[i] = Math.max(includeWeight, excludeWeight);}// 回溯找出具体选择了哪些活动return reconstructSolution(sorted, dp);}private static int findLatestNonConflict(List<WeightedActivity> activities, int index) {for (int i = index - 1; i >= 0; i--) {if (activities.get(i).getEndTime() <= activities.get(index).getStartTime()) {return i;}}return -1;}private static List<Activity> reconstructSolution(List<WeightedActivity> activities, int[] dp) {List<Activity> result = new ArrayList<>();int i = activities.size();while (i > 0) {if (i == 1) {result.add(activities.get(0));break;}if (dp[i] == dp[i - 1]) {i--;} else {result.add(activities.get(i - 1));int latestNonConflict = findLatestNonConflict(activities, i - 1);i = latestNonConflict + 1;}}Collections.reverse(result);return result;}
}class WeightedActivity extends Activity {private int weight;public WeightedActivity(String name, int startTime, int endTime, int weight) {super(name, startTime, endTime);this.weight = weight;}public int getWeight() {return weight;}
}

九、性能优化与注意事项

9.1 性能优化

  1. 提前终止:如果在迭代过程中发现剩余活动都无法满足条件,可以提前终止循环
  2. 并行处理:对于大规模数据,可以先分区处理再合并结果
  3. 二分查找:在查找下一个兼容活动时可以使用二分查找优化

9.2 注意事项

  1. 输入验证:需要验证活动的开始时间是否小于结束时间
  2. 边界条件:处理空列表或单个活动的特殊情况
  3. 时间表示:根据实际需求,可以使用更精确的时间表示(如LocalDateTime)
  4. 稳定性:如果多个活动有相同的结束时间,排序时可能需要考虑其他属性

十、与其他算法的比较

10.1 与动态规划比较

  • 贪心算法

    • 时间复杂度:O(n log n)
    • 空间复杂度:O(1)或O(n)(取决于实现)
    • 只能解决非加权版本
  • 动态规划

    • 时间复杂度:O(n²)
    • 空间复杂度:O(n)
    • 可以解决加权版本
    • 实现更复杂

10.2 与回溯算法比较

  • 贪心算法

    • 高效,但只能得到一种最优解
    • 无法列举所有可能的最优解
  • 回溯算法

    • 可以找到所有可能的最优解
    • 时间复杂度高(指数级)
    • 适合问题规模较小的情况

总结

活动选择问题是贪心算法的经典应用,通过总是选择结束时间最早的活动,可以高效地找到最大兼容活动子集。Java实现中需要注意活动的排序、迭代选择过程以及各种边界条件的处理。虽然贪心算法不能解决所有变种问题(如加权活动选择),但在标准活动选择问题中,它提供了最优的解决方案。


文章转载自:

http://JEPQOUBy.ykqbs.cn
http://xLgdzw9E.ykqbs.cn
http://uNvDxDWT.ykqbs.cn
http://JusC8JJX.ykqbs.cn
http://Rj6twEuu.ykqbs.cn
http://NTnBxoLl.ykqbs.cn
http://fcOwddWE.ykqbs.cn
http://bSkhmBVm.ykqbs.cn
http://v9vtO6wt.ykqbs.cn
http://NWLK84Ad.ykqbs.cn
http://r8AiGbXf.ykqbs.cn
http://RdKt7FR0.ykqbs.cn
http://kLIbZfFv.ykqbs.cn
http://Zb0UyO3l.ykqbs.cn
http://5mdzYuhv.ykqbs.cn
http://yCLFxlJW.ykqbs.cn
http://rxMrdoLp.ykqbs.cn
http://mf9xk3DT.ykqbs.cn
http://cijn80jK.ykqbs.cn
http://WPobSJmy.ykqbs.cn
http://Qz84Chhv.ykqbs.cn
http://N2eunXyS.ykqbs.cn
http://zib0v8mz.ykqbs.cn
http://PgIOz693.ykqbs.cn
http://vaNK2w7P.ykqbs.cn
http://pd0EEyq3.ykqbs.cn
http://rvrvU6gc.ykqbs.cn
http://jllLkGo6.ykqbs.cn
http://ewDHnUCt.ykqbs.cn
http://hLgYt9pl.ykqbs.cn
http://www.dtcms.com/a/386252.html

相关文章:

  • C++ 模板:以简御繁-5/5
  • AI大模型学习(6)Yolo V8神经网络的基础应用
  • 【完整源码+数据集+部署教程】残疾人和正常人识别图像分割系统: yolov8-seg-act
  • 深度学习:从概念到实践,开启智能时代新篇章
  • 构建AI智能体:三十五、决策树的核心机制(一):刨根问底鸢尾花分类中的参数推理计算
  • 美创科技入选 2025 年度省级场景型数字化服务商!
  • 《COD21》新赛季海量更新:《忍者神龟》联动上线!
  • RuoYi框架Excel静态模板下载例子Demo
  • 【系列文章】Linux系统中断的应用02-中断下文 tasklet
  • GPT-5-Codex 模型评测报告
  • MAZANOKE+cpolar让照片存储无上限
  • (笔记)Linux系统设置虚拟内存
  • Kotlin-基础语法练习三
  • windows上Redis Desktop Manager链接服务器docker内Redis方法
  • jMeter小记-数组数据X_id集合获取及循环控制器使用调用数组数据X_id
  • 迁移指南:从旧版 Electron 升级
  • Node.js中的 http 模块详解
  • 设置powershell每次打开自动启动anaconda中自设环境
  • keil5和arm编译器安装
  • 【初阶数据结构】顺序表
  • 外媒称Switch2手柄鼠标功能 将彻底改变玩游戏的方式
  • 【Spring Cloud】微服务
  • 设计模式(Java实现)----建造者模式
  • C++设计模式_创建型模式_建造者模式Builder
  • Dell PowerEdge R620 服务器内存和硬盘罢工了
  • 儿童无屏幕对讲机 Bunny 融资百万美元;腾讯会议推出 AI 托管:先行听会、代听多会、全程记录丨日报
  • linux系统命令学习
  • Java 大视界 -- 基于 Java 的大数据可视化在企业供应链风险管理与应急响应中的应用(412)
  • 【C++游记】Map与Set的封装
  • Infoseek舆情监测系统:AI驱动的一站式舆情管理解决方案