深入理解区间调度问题:从贪心算法到动态规划的加权优化
引言
区间调度问题是一个经典的算法问题,它广泛应用于任务调度、资源分配等领域。本文将从基础的区间调度问题出发,介绍如何通过贪心算法高效解决这个问题。接着,我们会探讨问题的扩展——加权区间调度问题,并展示如何使用动态规划来解决该问题,以实现权重的最大化。通过对这两种算法的实现和对比,您将理解不同算法的应用场景、优劣以及最优性。
一、问题描述
1.1 经典的区间调度问题
经典的区间调度问题定义如下:
- 我们有 (n) 个请求,每个请求 (i) 具有开始时间 (s(i)) 和结束时间 (f(i)),且 (s(i) < f(i))。
- 两个请求 (i) 和 (j) 是兼容的,若它们的时间区间不重叠,即 (f(i) \leq s(j)) 或 (f(j) \leq s(i))。
- 目标是从这些请求中选择一个最大兼容子集,确保所选请求的时间不重叠,并且尽可能多地选择请求。
例如,给定一组请求:
- 请求1:(s(1) = 1, f(1) = 4)
- 请求2:(s(2) = 3, f(2) = 5)
- 请求3:(s(3) = 0, f(3) = 6)
- 请求4:(s(4) = 5, f(4) = 7)
- 请求5:(s(5) = 8, f(5) = 9)
- 请求6:(s(6) = 5, f(6) = 9)
我们需要找到一个不重叠的最大请求子集。
1.2 加权区间调度问题
加权区间调度问题是经典区间调度问题的扩展:
- 除了每个请求 (i) 具有开始时间 (s(i)) 和结束时间 (f(i)) 之外,每个请求还附带一个权重 (w(i))。
- 目标从这些请求中选择一个最大权重兼容子集,即所有请求不重叠,并且所选请求的总权重最大。
二、贪心算法解决经典区间调度问题
在经典的区间调度问题中,贪心算法表现良好。它的核心思想是:每次选择结束时间最早的请求,这样可以为后续的请求留出更多的时间空间,保证能够选择尽可能多的不重叠请求。
2.1 贪心算法步骤
- 排序:按照请求的结束时间 (f(i)) 对所有请求进行升序排序。
- 选择:每次选择当前结束时间最早的请求,并排除与之重叠的其他请求。
- 重复:继续选择剩余请求中结束时间最早的请求,直到所有请求都处理完。
2.2 Python实现
class Request:
def __init__(self, start, finish):
self.start = start
self.finish = finish
def greedy_interval_scheduling(requests):
# 按结束时间排序
sorted_requests = sorted(requests, key=lambda x: x.finish)
selected_requests = []
last_finish_time = 0
# 选择结束时间最早且不重叠的请求
for request in sorted_requests:
if request.start >= last_finish_time:
selected_requests.append(request)
last_finish_time = request.finish
return selected_requests
# 测试数据
requests = [
Request(1, 4),
Request(3, 5),
Request(0, 6),
Request(5, 7),
Request(8, 9),
Request(5, 9)
]
selected_requests = greedy_interval_scheduling(requests)
# 输出结果
print("贪心算法选择的请求:")
for r in selected_requests:
print(f"请求({r.start}, {r.finish})")
2.3 运行结果
贪心算法选择的请求:
请求(1, 4)
请求(5, 7)
请求(8, 9)
贪心算法的时间复杂度为 (O(n \log n)),其中 (n) 是请求的数量,排序操作占据了主要的时间复杂度。
三、贪心算法的数学证明
贪心算法的正确性并非仅仅依赖直觉。事实上,我们可以通过数学归纳法严格证明,在经典的区间调度问题中,贪心算法始终能够找到最优解。
3.1 贪心选择策略
贪心算法的选择规则是:每次选择结束时间最早的请求,然后排除与其重叠的其他请求。这种选择保证了剩余的区间有最多的空间选择更多的请求。
3.2 贪心算法输出的区间序列
贪心算法输出的区间序列为:
[
(s(i_1), f(i_1)), (s(i_2), f(i_2)), \dots, (s(i_k), f(i_k))
]
满足:
[
s(i_1) < f(i_1) \leq s(i_2) < f(i_2) \leq \dots \leq s(i_k) < f(i_k)
]
即,所有区间按照结束时间排序,并且这些区间互不重叠。
3.3 数学归纳法证明
基础情况:
当最优解只包含一个区间时(即 (k^* = 1)),任意选择该区间都是最优的。
归纳假设:
假设对于最多选择 (k^*) 个不重叠区间的情况,贪心算法能够找到最优解。
归纳步骤:
假设最优解包含 (k^* + 1) 个不重叠区间,设其为:
[
S^* = {<s(j_1), f(j_1)>, <s(j_2), f(j_2)>, \dots, <s(j_{k^+1}), f(j_{k^+1})>}
]
贪心算法首先选择了结束时间最早的区间 (<s(i_1), f(i_1)>),此时必有 (f(i_1) \leq f(j_1))。我们可以用 (<s(i_1), f(i_1)>) 替换最优解中的第一个区间 (<s(j_1), f(j_1)>),得到新的区间集合 (S^{**}):
[
S^** = {<s(i_1), f(i_1)>, <s(j_2), f(j_2)>, \dots, <s(j_{k^+1}), f(j_{k^+1})>}
]
该集合与 (S^*) 有相同数量的区间,并且仍然是合法的选择。因此,贪心算法在第一步的选择是正确的。
接下来,我们递归在剩余的区间上运行贪心算法。定义集合 (L’) 为所有开始时间不早于 (f(i_1)) 的区间集合。根据归纳假设,贪心算法能够在 (L’) 上找到最优解。将这些不重叠区间与 (<s(i_1), f(i_1)>) 结合,即得到了原问题的最优解。
因此,通过归纳法,我们证明了贪心算法在经典区间调度问题中总能找到最优解。
四、动态规划解决加权区间调度问题
对于加权区间调度问题,贪心算法并不能直接适用,因为它只考虑了时间约束,而忽略了请求的权重。为了解决该问题,我们使用动态规划。
4.1 动态规划思想
我们将定义一个数组 dp[i]
,表示前 (i) 个请求中可以选择的不重叠请求的最大权重。对每个请求 (i),我们有两个选择:
- 不选择请求 (i):此时最大权重等于 (dp[i-1])。
- 选择请求 (i):此时我们需要找到最后一个与请求 (i) 不重叠的请求 (j),则最大权重为 (w(i) + dp[j])。
4.2 递归关系
[
dp(i) = max(dp(i-1), w(i) + dp(p(i)))
]
其中 (p(i)) 表示最后一个与 (i) 不重叠的请求。
4.3 Python实现
class Request:
def __init__(self, start, finish, weight):
self.start = start
self.finish = finish
self.weight = weight
def find_last_non_conflicting(requests, i):
low, high = 0, i - 1
while low <= high:
mid = (low + high) // 2
if requests[mid].finish <= requests[i].start:
if requests[mid + 1].finish <= requests[i].start:
low = mid + 1
else:
return mid
else:
high = mid - 1
return -1
def weighted_interval_scheduling(requests):
# 按结束时间排序
requests = sorted(requests, key=lambda x: x.finish)
n = len(requests)
dp = [0] * n
dp[0] = requests[0].weight
selected_requests = [[] for _ in range(n)]
selected_requests[0] = [requests[0]]
for i in range(1, n):
last_non_conflicting = find_last_non_conflicting(requests, i)
include_weight = requests[i].weight
if last_non_conflicting != -1:
include_weight += dp[last_non_conflicting]
exclude_weight = dp[i - 1]
dp[i] = max(include_weight, exclude_weight)
if include_weight > exclude_weight:
selected_requests[i] = selected_requests[last_non_conflicting] + [requests[i]] if last_non_conflicting != -1 else [requests[i]]
else:
selected_requests[i] = selected_requests[i - 1]
return dp[-1], selected_requests[-1]
# 测试数据
requests = [
Request(1, 4, 5),
Request(3, 5, 1),
Request(0, 6, 8),
Request(5, 7, 4),
Request(8, 9, 6),
Request(5, 9, 3)
]
max_weight, selected_requests = weighted_interval_scheduling(requests)
# 输出结果
print("\n动态规划算法选择的请求及其总权重:")
for r in selected_requests:
print(f"请求({r.start}, {r.finish}) - 权重: {r.weight}")
print(f"总权重: {max_weight}")
五、贪心算法与动态规划的对比
特性 | 贪心算法 | 动态规划 |
---|---|---|
适用问题 | 经典区间调度问题 | 加权区间调度问题 |
考虑权重 | 否 | 是 |
时间复杂度 | (O(n \log n)) | (O(n \log n)) |
是否最优解 | 是(不含权重) | 是(含权重) |
算法思路 | 选择结束时间最早的请求 | 通过递归关系最大化总权重 |
六、结论
在经典的区间调度问题中,贪心算法通过每次选择结束时间最早的请求,可以高效地找到不重叠请求的最大集合。然而,当引入请求的权重后,贪心算法无法确保最优解。这时,动态规划提供了更适合的解决方案,能够在保证请求不重叠的前提下,找到总权重最大的请求子集。
通过对贪心算法的数学归纳法证明,我们进一步验证了其在经典区间调度问题中的最优性。而动态规划则为加权区间调度问题提供了有效的解决方案,确保总权重最大化。