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

贪心算法应用:带权任务间隔调度问题详解

在这里插入图片描述

贪心算法应用:带权任务间隔调度问题详解

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。带权任务间隔调度问题是贪心算法的一个经典应用场景。

问题定义

**带权任务间隔调度问题(Weighted Interval Scheduling Problem)**描述如下:

  • 给定一组任务,每个任务有一个开始时间、结束时间和权重(价值)
  • 目标:选择一组互不重叠的任务,使得这些任务的总权重最大

与普通间隔调度问题不同之处在于,这里每个任务有不同的权重,我们需要考虑权重而不仅仅是任务数量。

问题分析

输入表示

每个任务可以表示为三元组 (start, end, weight)

class Task {int start;int end;int weight;public Task(int start, int end, int weight) {this.start = start;this.end = end;this.weight = weight;}
}

关键概念

  1. 兼容任务:两个任务如果它们的执行时间不重叠,则称为兼容任务
  2. p(j):对于任务j,p(j)是最大的i < j且任务i与任务j兼容的任务索引

贪心算法解决方案

1. 动态规划解法(非贪心)

首先我们看一下动态规划解法,以便与贪心算法对比:

public int weightedIntervalScheduling(Task[] tasks) {// 按结束时间排序Arrays.sort(tasks, (a, b) -> a.end - b.end);int n = tasks.length;int[] dp = new int[n + 1];for (int j = 1; j <= n; j++) {int i = latestNonOverlapping(tasks, j - 1);int weight = tasks[j - 1].weight;dp[j] = Math.max(weight + (i != -1 ? dp[i + 1] : 0), dp[j - 1]);}return dp[n];
}private int latestNonOverlapping(Task[] tasks, int j) {for (int i = j - 1; i >= 0; i--) {if (tasks[i].end <= tasks[j].start) {return i;}}return -1;
}

这种方法时间复杂度为O(n²),我们可以用贪心算法优化。

2. 贪心算法优化

贪心算法的关键在于如何选择任务。我们可以通过以下步骤实现:

  1. 将所有任务按照结束时间排序
  2. 使用二分查找快速找到p(j)
  3. 使用记忆化或自底向上的动态规划

优化后的Java实现:

import java.util.Arrays;
import java.util.Comparator;public class WeightedIntervalScheduling {static class Task {int start;int end;int weight;public Task(int start, int end, int weight) {this.start = start;this.end = end;this.weight = weight;}}public static int schedule(Task[] tasks) {// 1. 按结束时间排序Arrays.sort(tasks, Comparator.comparingInt(a -> a.end));int n = tasks.length;int[] dp = new int[n + 1];// 预处理p数组,p[j]表示在任务j之前最后一个不与j冲突的任务索引int[] p = new int[n];for (int j = 0; j < n; j++) {p[j] = binarySearch(tasks, j);}// 动态规划填表dp[0] = 0;for (int j = 1; j <= n; j++) {int include = tasks[j - 1].weight;if (p[j - 1] != -1) {include += dp[p[j - 1] + 1];}dp[j] = Math.max(include, dp[j - 1]);}return dp[n];}// 使用二分查找找到最后一个不与tasks[j]冲突的任务private static int binarySearch(Task[] tasks, int j) {int low = 0;int high = j - 1;int key = tasks[j].start;int result = -1;while (low <= high) {int mid = (low + high) / 2;if (tasks[mid].end <= key) {result = mid;low = mid + 1;} else {high = mid - 1;}}return result;}public static void main(String[] args) {Task[] tasks = {new Task(1, 3, 5),new Task(2, 5, 6),new Task(4, 6, 5),new Task(6, 7, 4),new Task(5, 8, 11),new Task(7, 9, 2)};System.out.println("Maximum weight: " + schedule(tasks));}
}

3. 算法复杂度分析

  1. 排序:O(n log n)
  2. 预处理p数组:n次二分查找,每次O(log n),总共O(n log n)
  3. 动态规划填表:O(n)

总时间复杂度:O(n log n),比纯动态规划的O(n²)更高效。

贪心选择性质证明

贪心算法的正确性需要证明其具有贪心选择性质和最优子结构性质。

贪心选择性质:全局最优解可以通过局部最优(贪心)选择达到。

对于带权任务间隔调度问题:

  1. 设任务集按结束时间排序为1, 2, …, n
  2. 设O是最优解,且任务j是O中结束时间最早的任务
  3. 如果任务j不在贪心解中,可以用j替换O中第一个任务,不会降低总权重
  4. 因此存在包含任务j的最优解

最优子结构性质:问题的最优解包含子问题的最优解。

在选择任务j后,剩余问题是求解在j结束后开始的任务集合的最大权重调度,这正是原问题的子问题。

变种与扩展

1. 输出具体任务序列

除了计算最大权重,我们还可以记录具体选择了哪些任务:

public static List<Task> scheduleWithTasks(Task[] tasks) {Arrays.sort(tasks, Comparator.comparingInt(a -> a.end));int n = tasks.length;int[] dp = new int[n + 1];int[] choice = new int[n + 1]; // 记录选择int[] p = new int[n];for (int j = 0; j < n; j++) {p[j] = binarySearch(tasks, j);}dp[0] = 0;for (int j = 1; j <= n; j++) {int include = tasks[j - 1].weight;if (p[j - 1] != -1) {include += dp[p[j - 1] + 1];}if (include > dp[j - 1]) {dp[j] = include;choice[j] = 1; // 选择任务j} else {dp[j] = dp[j - 1];choice[j] = 0; // 不选择任务j}}// 回溯找出选择的任务List<Task> result = new ArrayList<>();int j = n;while (j > 0) {if (choice[j] == 1) {result.add(tasks[j - 1]);j = p[j - 1] + 1; // 跳到前一个兼容任务} else {j--;}}Collections.reverse(result);return result;
}

2. 资源分配扩展

当有多个资源(如多个会议室)时,问题变为区间图着色问题,可以使用贪心算法的最早结束时间策略,配合资源计数来解决。

实际应用场景

带权任务间隔调度算法在实际中有广泛应用:

  1. 会议室安排:选择最有价值的会议组合
  2. 作业调度:CPU任务调度,选择权重最高的任务组合
  3. 广告投放:在有限时间段选择最有价值的广告组合
  4. 课程安排:安排最有价值且不冲突的课程组合

性能优化技巧

  1. 预处理优化:提前计算并存储p(j)数组
  2. 记忆化搜索:使用递归+记忆化代替纯递归
  3. 迭代实现:使用迭代而非递归避免栈溢出
  4. 空间优化:只存储必要的DP状态,减少空间复杂度

与其他算法对比

  1. 与回溯算法对比

    • 回溯:时间复杂度O(2ⁿ),可以找到所有解
    • 贪心:时间复杂度O(n log n),只能找到一种最优解
  2. 与动态规划对比

    • 动态规划:通常需要填表,可能时间复杂度较高
    • 贪心:利用问题特性优化,通常更高效
  3. 与线性规划对比

    • 线性规划:更通用但实现复杂
    • 贪心:针对特定问题,实现简单高效

常见错误与陷阱

  1. 排序标准错误:必须按结束时间排序,而不是开始时间或权重
  2. 权重处理不当:不能简单地选择权重最大的任务,需要考虑兼容性
  3. 边界条件处理:注意空任务集和全冲突任务集的特殊情况
  4. 索引处理错误:在实现p(j)时容易出现的off-by-one错误

测试用例设计

好的测试用例应包括:

public class WeightedIntervalSchedulingTest {@Testpublic void testEmptyTasks() {Task[] tasks = {};assertEquals(0, WeightedIntervalScheduling.schedule(tasks));}@Testpublic void testSingleTask() {Task[] tasks = {new Task(1, 3, 5)};assertEquals(5, WeightedIntervalScheduling.schedule(tasks));}@Testpublic void testAllTasksConflict() {Task[] tasks = {new Task(1, 5, 3),new Task(2, 6, 4),new Task(3, 7, 2)};assertEquals(4, WeightedIntervalScheduling.schedule(tasks));}@Testpublic void testNoTasksConflict() {Task[] tasks = {new Task(1, 3, 5),new Task(4, 6, 3),new Task(7, 9, 2)};assertEquals(10, WeightedIntervalScheduling.schedule(tasks));}@Testpublic void testMixedCase() {Task[] tasks = {new Task(1, 3, 5),new Task(2, 5, 6),new Task(4, 6, 5),new Task(6, 7, 4),new Task(5, 8, 11),new Task(7, 9, 2)};assertEquals(16, WeightedIntervalScheduling.schedule(tasks));}
}

总结

带权任务间隔调度问题展示了贪心算法在解决特定类型优化问题时的强大能力。通过合理的问题分析和算法设计,我们可以将看似复杂的O(2ⁿ)问题转化为O(n log n)的高效解决方案。关键在于:

  1. 识别问题的贪心选择性质
  2. 设计合适的排序和选择策略
  3. 结合动态规划避免重复计算
  4. 正确处理边界条件和特殊情况

Java实现时需要注意任务表示、排序、二分查找优化等细节,以确保算法的高效性和正确性。

更多资源:

https://www.kdocs.cn/l/cvk0eoGYucWA

本文发表于【纪元A梦】!

http://www.lryc.cn/news/2401432.html

相关文章:

  • 用电脑控制keysight示波器
  • LLaMA-Factory - 批量推理(inference)的脚本
  • React从基础入门到高级实战:React 高级主题 - 测试进阶:从单元测试到端到端测试的全面指南
  • Ansible 剧本精粹 - 编写你的第一个 Playbook
  • 【Elasticsearch】Elasticsearch 核心技术(二):映射
  • 【计算机网络】网络层协议
  • .NET Core接口IServiceProvider
  • 结构型设计模式之Proxy(代理)
  • 案例分享--汽车制动卡钳DIC测量
  • Redis Set集合命令、内部编码及应用场景(详细)
  • C++算法动态规划1
  • 【快速预览经典深度学习模型:CNN、RNN、LSTM、Transformer、ViT全解析!】
  • KaiwuDB在边缘计算领域的应用与优势
  • 如何避免二极管过载?
  • Vue.js组件开发系统性指南
  • React---day9
  • 设计模式 - 模板方法模式
  • 鸿蒙开发List滑动每项标题切换悬停
  • ubuntu开机自动挂载windows下的硬盘
  • C# 实现软件开机自启动(不需要管理员权限)
  • 使用 Golang `testing/quick` 包进行高效随机测试的实战指南
  • 32 C 语言字符处理函数详解:isalnum、isalpha、iscntrl、isprint、isgraph、ispunct、isspace
  • Qt实现一个悬浮工具箱源码分享
  • 线夹金具测温在线监测装置:电力设备安全运行的“隐形卫士”
  • 《TCP/IP 详解 卷1:协议》第4章:地址解析协议
  • Dify 离线升级操作手册(适用于无外网企业内网环境)
  • Windows下运行Redis并设置为开机自启的服务
  • 网络编程之网络基础
  • Spring AI(11)——SSE传输的MCP服务端
  • 计算机网络备忘录