区间动态规划例题:带权重的区间调度问题(允许区间重叠的加权版本)
题目描述
给定一系列任务,每个任务表示为区间 [start_i, end_i),以及该任务对应的权重(或价值)weight_i。你需要从中选择一个任务子集,允许任务区间重叠,但选择的子集必须满足一个约束条件:任何两个被选中的任务之间,其区间不能有超过 k 个重叠(这里我们定义“重叠”为两个任务区间有交集)。更形式化地,对于被选中的任意两个任务 i 和 j,如果 start_i < end_j 且 start_j < end_i,则认为它们重叠。
目标:最大化所选任务的总权重之和。
举例:
假设有任务区间:
任务1: [1, 4), weight=5
任务2: [2, 5), weight=6
任务3: [3, 6), weight=4
任务4: [7, 10), weight=8
约束 k=2(即最多允许任意两个任务之间有重叠,但如果有三个任务两两重叠,则不允许)。
一种可能的选择是 {任务1, 任务2, 任务4},总权重=5+6+8=19(因为任务1和任务2重叠,但任务4不与其他重叠,满足 k=2,任意两个任务之间的重叠数不超过2)。但如果选择 {任务1, 任务2, 任务3},则这三个任务两两重叠,违反了 k=2(因为存在三个任务互相重叠,即重叠数超过2)。
解题思路
这是一个区间动态规划问题,我们需要考虑任务的选择顺序和重叠限制。
关键点:
- 将任务按照结束时间
end排序,这样可以方便地判断任务之间的先后关系。 - 定义状态
dp[i]表示考虑前i个任务(按结束时间排序后),并且必须选择第i个任务时,能达到的最大权重和。 - 状态转移时,我们需要找到前一个可以选择的最近任务
j,使得任务j与任务i不冲突(即end_j <= start_i),但由于允许重叠,我们还需要考虑重叠限制。-
重叠限制意味着我们不能选择与当前任务
i重叠的任务数量超过k。但直接处理“任意两个任务之间的重叠”比较复杂,我们可以换一种思路:
实际上,题目等价于:在任意时刻,被选中的任务中互相重叠的任务个数不能超过k。
但更简单的转化是:我们可以将问题视为选择若干互不相交的任务组,每组内的任务两两重叠,且每组的大小不超过k。
但这样转化后仍然难以直接DP。 -
另一种更直接的DP方法:定义状态
dp[i][c]表示考虑前i个任务,且以第i个任务作为一组重叠任务的最后一个,这一组重叠任务的数量为c(1 ≤ c ≤ k)时的最大权重和。
转移时,我们考虑前一个任务j:- 如果任务
j与任务i不重叠(即end_j <= start_i),那么任务i可以新开一组,即c=1,从dp[j][*]转移过来。 - 如果任务
j与任务i重叠(即end_j > start_i),那么任务i可以加入任务j所在的重叠组,但要求重叠组的大小不超过k,即从dp[j][c-1]转移过来(c ≥ 2)。
但这样的状态定义会导致复杂度较高(O(n² * k)),且需要考虑重叠组的连续性。
- 如果任务
-
更简洁的解法(本题的标准解法):
我们先预处理出每个任务i的“前一个不重叠任务”的索引prev[i],即最大的j满足end_j <= start_i(二分查找即可)。
然后,我们定义dp[i]表示考虑前i个任务(排序后)时,能获得的最大权重和,此时不要求必须选择第 i 个任务。
转移有两种选择:
(1) 不选任务 i:dp[i] = dp[i-1]
(2) 选任务 i:那么我们需要确保选择任务 i 后,与它重叠的任务数不超过 k。
如何判断?我们可以枚举一个位置p,使得从p到i的所有任务都与任务 i 重叠(即start_p < end_i),并且选择任务 i 时,这些重叠的任务中最多只能选 k 个(包括 i 本身)。
但这样枚举太耗时。实际上,我们可以将问题转化为:
选择若干个任务,使得在任意时刻,被选中的任务中,结束时间晚于当前时刻、开始时间早于当前时刻的任务数不超过 k。
这个条件等价于:在时间轴上滑动,同时维护一个“选中任务集合”,这个集合在任意时刻的大小不超过 k。但区间DP通常按区间端点进行划分。更巧妙的解法是:
将任务按结束时间排序后,用dp[i][j]表示考虑前 i 个任务,且选中了 j 个任务时的最大权重和,其中 j ≤ k?不对,因为 k 是重叠限制,不是选中任务数的限制。经过思考,我发现这个问题实际上是一个经典的带权区间调度问题,但允许有限重叠(k-overlap)。
标准解法是:- 将任务按结束时间排序。
- 定义
dp[i]为前 i 个任务的最大权重和。 - 对于每个任务 i,我们考虑两种情况:
- 不选 i:
dp[i] = dp[i-1] - 选 i:那么我们需要找到前一个与 i 不冲突的任务集合。但由于允许重叠,我们可以选择 i,并且同时选择所有与 i 重叠的任务,只要这些重叠的任务不超过 k 个。
但这样会变成组合问题。
- 不选 i:
实际上,这个问题有一个更简单的定义:
选择一组任务,使得没有超过 k 个任务在任意时间点同时重叠。
这等价于:将时间轴上的每个点,被覆盖的次数不超过 k。对于此类问题,常用的方法是转化为最大权 k-区间包问题。
一个高效的DP解法:- 将所有区间按右端点排序。
- 定义
dp[i][j]表示考虑前 i 个区间,且选择了 j 个区间时的最大权重和(j ≤ 总任务数)。 - 转移:
- 不选第 i 个区间:
dp[i][j] = dp[i-1][j] - 选第 i 个区间:那么我们需要找到最后一个与第 i 个区间不重叠的区间 p(即
end_p ≤ start_i)。
如果选择第 i 个区间,那么我们可以从dp[p][j-1]转移过来,但前提是我们必须保证加入第 i 个区间后,不会造成在某些时间点上重叠的区间数超过 k。
如何保证?实际上,如果我们按结束时间排序后选择区间,并且保证选择的区间集合中,任意两个重叠的区间之间满足某种性质,就可以用“最大独立集”的扩展来解。
- 不选第 i 个区间:
鉴于这个问题的复杂性,我决定采用一种更直观且易于实现的DP思路:
将任务按开始时间排序,然后定义状态dp[i]表示以第 i 个任务为最后一个被选中的任务时的最大权重和。
转移时,我们枚举前一个被选中的任务 j,要求任务 j 与任务 i 不重叠(即end_j ≤ start_i),并且从 j 到 i 之间被跳过的任务中,与 i 重叠的任务数不超过 k-1(因为加上 i 本身,重叠数不超过 k)。
但这样仍然需要判断重叠数。最终,我采用以下方法(标准解法):
- 将任务按结束时间排序。
- 对于每个任务 i,预处理出
prev[i]表示最大的 j 满足end_j ≤ start_i(即不重叠的前一个任务)。 - 定义
dp[i][t]表示考虑前 i 个任务,且在最后一个被选中的任务结束之后,又额外选择了 t 个与当前任务重叠的任务(t ≤ k-1)时的最大权重和。
这个状态定义比较巧妙,它记录了重叠任务的数量。
转移时:- 不选任务 i:
dp[i][t] = dp[i-1][t] - 选任务 i:那么我们需要找到前一个不重叠的任务
p = prev[i],然后从dp[p][*]转移过来,同时,如果 t > 0,则说明有重叠任务被选择。
- 不选任务 i:
然而,这种状态定义仍较复杂。
为了清晰起见,我决定采用一种简化但能说明区间DP思想的版本:
假设 k=1,即不允许任何重叠。这就是经典的加权区间调度问题。
状态定义:dp[i]表示考虑前 i 个任务(按结束时间排序)时的最大权重和。
转移:
dp[i] = max(dp[i-1], dp[prev[i]] + weight_i)
其中prev[i]是最大的 j 满足end_j ≤ start_i。对于 k>1 的情况,我们可以扩展状态:
dp[i][c]表示考虑前 i 个任务,且以第 i 个任务作为一组重叠任务的最后一个,这组重叠任务包含 c 个任务(1 ≤ c ≤ k)时的最大权重和。
转移:
dp[i][c] = max_{j < i} { if (end_j ≤ start_i) then dp[j][*] + weight_i # 新开一组,c=1 else if (c > 1) then dp[j][c-1] + weight_i # 加入重叠组 }
同时,我们还需要一个全局最优解数组best[i]表示前 i 个任务的最大权重和(不一定以 i 结尾)。实现时,我们按结束时间排序后,对于每个 i,枚举所有 j < i,根据是否重叠决定转移。时间复杂度 O(n² * k)。
-
逐步讲解
我们以 k=2 为例,任务区间如下(已按结束时间排序):
任务1: [1, 4), w=5
任务2: [2, 5), w=6
任务3: [3, 6), w=4
任务4: [7, 10), w=8
步骤1:预处理
对于每个任务 i,计算 prev[i] 表示最后一个不与 i 重叠的任务索引(即最大的 j 满足 end_j ≤ start_i)。
- 任务1: start=1, prev[1]=0(虚拟任务0,表示空)
- 任务2: start=2, 检查任务1的end=4,4>2,所以重叠,prev[2]=0
- 任务3: start=3, 检查任务2的end=5>3,任务1的end=4>3,所以重叠,prev[3]=0
- 任务4: start=7, 检查任务3的end=6≤7,所以prev[4]=3
步骤2:状态定义
dp[i][c]:考虑前 i 个任务,且以任务 i 作为一组重叠任务的最后一个,这组重叠任务包含 c 个任务(1≤c≤k)时的最大权重和。
best[i]:考虑前 i 个任务时的全局最大权重和(不一定以 i 结尾)。
初始化:dp[*][*] = -∞,best[0]=0。
步骤3:状态转移
按 i=1 到 n 顺序计算:
-
i=1(任务1):
- c=1:新开一组,可以从虚拟任务0转移:
dp[1][1] = weight1 = 5 - c=2:不可能,因为c=2需要前一个任务与它重叠,但前面没有任务。
best[1] = max(best[0], dp[1][1]) = max(0,5)=5
- c=1:新开一组,可以从虚拟任务0转移:
-
i=2(任务2):
- c=1:新开一组,可以从
best[prev[2]]转移,prev[2]=0,所以dp[2][1] = best[0] + weight2 = 0+6=6
也可以从任务1转移,但任务1与任务2重叠(end1=4 > start2=2),所以不能作为不重叠的前驱。 - c=2:加入重叠组,需要前一个任务与它重叠(即任务1),且该组已有c-1=1个任务。
所以从dp[1][1]转移:dp[2][2] = dp[1][1] + weight2 = 5+6=11 best[2] = max(best[1], dp[2][1], dp[2][2]) = max(5,6,11)=11
- c=1:新开一组,可以从
-
i=3(任务3):
- c=1:新开一组,从
best[prev[3]]转移,prev[3]=0,所以dp[3][1] = best[0] + weight3 = 0+4=4 - c=2:加入重叠组,需要前一个重叠任务(任务1或任务2)。
从任务1:dp[1][1] + weight3 = 5+4=9
从任务2:dp[2][1] + weight3 = 6+4=10
取最大值:dp[3][2] = 10 - c=3?但k=2,所以c不能超过2。
best[3] = max(best[2], dp[3][1], dp[3][2]) = max(11,4,10)=11
- c=1:新开一组,从
-
i=4(任务4):
- c=1:新开一组,从
best[prev[4]]转移,prev[4]=3,所以dp[4][1] = best[3] + weight4 = 11+8=19 - c=2:加入重叠组,但任务4与任务3不重叠(end3=6 ≤ start4=7),所以不能从任务3转移。实际上,任务4与前面所有任务都不重叠,所以c=2不可能。
best[4] = max(best[3], dp[4][1]) = max(11,19)=19
- c=1:新开一组,从
最终答案:best[4]=19。
验证:选择任务1、2、4,权重和=5+6+8=19,且任务1和任务2重叠(重叠任务数为2,满足k=2),任务4不与其他重叠,符合条件。
复杂度分析
- 预处理
prev:排序 O(n log n),二分查找每个任务的 prev 需 O(log n),总 O(n log n)。 - DP状态数:O(n * k)。
- 转移:对于每个
dp[i][c],c=1时从best[prev[i]]转移(O(1)),c>1时需要枚举所有与 i 重叠的前驱任务 j(最坏 O(n)),因此总复杂度 O(n² * k)。
可以通过进一步优化(如对于每个 i,只考虑与它重叠的前驱)来减少枚举,但最坏仍为 O(n² * k)。
总结
这个题目是带权区间调度问题的扩展,允许有限重叠(k-overlap)。核心思路是按结束时间排序,定义状态 dp[i][c] 表示以任务 i 为结尾且所在重叠组大小为 c 时的最大权重,同时维护全局最优 best[i]。转移时分别考虑新开一组(c=1)和加入前驱的重叠组(c>1)两种情况。最终答案为 best[n]。
通过这个例子,你可以看到区间DP如何通过增加状态维度(重叠组大小 c)来应对更复杂的约束条件(有限重叠)。