带权区间调度问题(Weighted Interval Scheduling)的进阶版:带资源限制的区间调度
题目描述
给定 \(n\) 个区间,每个区间有开始时间 \(s_i\)、结束时间 \(e_i\) 和价值 \(v_i\),以及一个资源限制 \(R\)。每个区间在调度时需要消耗 \(r_i\) 单位的资源,且任意时刻被调度的区间的资源消耗总和不能超过 \(R\)。目标是选择若干个互不重叠的区间(即任意两个被选中的区间在时间上不重叠,但允许紧邻),使得在任意时刻资源消耗总和不超过 \(R\) 的前提下,最大化所选区间的总价值。其中 \(1 \leq n \leq 1000, 1 \leq s_i < e_i \leq 10^4, 1 \leq v_i \leq 10^4, 1 \leq R, r_i \leq 10\)。
解题过程
1. 问题理解与转换
基础版的“带权区间调度”是:选择互不重叠的区间,最大化总价值,可以用动态规划在 \(O(n \log n)\) 内解决。
但现在引入了资源限制:每个区间有一个资源消耗 \(r_i\),在任意时刻,所有正在进行的区间(即它们的起止时间覆盖了该时刻)的资源消耗总和不能超过 \(R\)。
这意味着我们不仅需要考虑时间不重叠,还要考虑并发时的资源约束。
2. 思路拆解
我们可以将时间离散化,然后使用“时间 + 资源状态”作为 DP 的一个维度。
但直接按时间做 DP 会非常复杂,因为时间范围大(\(10^4\)),资源限制 \(R\) 很小(≤ 10),所以我们可以考虑用资源分配作为状态的一部分。
另一种更高效的方法是区间按结束时间排序,然后使用 DP 递推,但在状态转移时需要判断“如果选择当前区间,之前可选的区间必须与它不重叠,并且资源不冲突”。
资源冲突的判断需要知道之前选择的区间集合在时间上的资源占用情况,这难以直接记录。
关键观察:
由于 \(R \leq 10, r_i \leq 10\),我们可以考虑用“资源分配”作为状态的一个维度,但必须与时间结合。
一种可行方法:将时间轴离散化,用 DP[t][c1][c2]…[cR] 表示在时间 t 时资源 1 到 R 的占用情况,但这状态数太大,不现实。
改进思路:
将区间按结束时间排序,定义 dp[i] 表示以区间 i 结尾的合法选择的最大价值。
在计算 dp[i] 时,我们需要找到之前的某个区间 j,使得:
- 区间 j 的结束时间 ≤ 区间 i 的开始时间(不重叠);
- 并且将区间 i 加入后,在 i 的整个运行期间内,资源占用不会超过 R。
难点是第二个条件:在区间 i 的运行期间内,可能有多个与 i 不重叠但彼此重叠的区间在并行,我们需要知道这些区间的资源占用情况。
3. 更可行的动态规划定义
我们可以用“资源分配”作为状态,但将时间按区间的端点离散化,然后用“时间 + 当前资源占用”作为状态。
具体来说:
- 将时间离散化为所有区间的开始时间和结束时间,排序去重,得到时间点列表 \(T_1, T_2, ..., T_m\)。
- 定义 \(dp[t][r\_sum]\) 表示在时间点 \(T_t\) 时,当前正在运行的区间占用的资源总量为 \(r\_sum\),并且之前的选择是合法的,此时能获得的最大总价值。
- 但这样还需要知道哪些区间正在运行,状态难以转移。
更常见的方法:
将资源占用看作“资源槽”,因为 R ≤ 10,我们可以用一个整数 mask 的每一位表示一个资源单元是否被占用。但 r_i 可以大于 1,无法简单用 0/1 表示。
实际上,由于 R 很小,我们可以枚举“当前时间段占用的资源总量”,用区间的开始和结束事件来驱动 DP。
4. 事件驱动 DP
将每个区间拆成两个事件:(s_i, +v_i, r_i) 表示在 s_i 时刻尝试开始区间 i,需要占用 r_i 资源,价值 v_i;(e_i, 0, r_i) 表示在 e_i 时刻释放 r_i 资源。
我们按时间顺序处理事件,用 dp[used] 表示当前资源占用为 used 时能获得的最大价值。
在遇到开始事件时,如果 used + r_i ≤ R,则可以选择这个区间,进入新状态 dp[used + r_i] += v_i。
在遇到结束事件时,释放资源,更新对应状态。
但这里有一个问题:结束事件时,我们需要知道是哪个区间结束了,以及它当时占用了多少资源,这需要记录区间与资源的对应关系,状态复杂。
5. 经典解法:对资源进行状态压缩 DP + 区间排序
我们可以将 R 个资源看作 R 个相同的“机器”,每个区间占用 r_i 个机器,在区间运行期间这些机器不能被其他区间占用。
那么问题变成:选择若干区间,将它们分配到 R 个机器上,同一个机器上的区间不能重叠,且一个区间占用连续 r_i 个机器,目标是最大化总价值。
这等价于有多个相同机器,每个区间需要一定数量的机器,且运行期间这些机器必须连续占用的调度问题。
因为 R 很小,我们可以用 DP 状态 f[i][mask] 表示考虑前 i 个区间,当前时间线上机器的占用情况用 mask 表示时的最大价值。但 mask 需要表示每个机器是否被占用,而 R=10 时 mask 大小 2^10=1024 可接受,但还要结合时间,复杂度较高。
6. 最终可行解法(区间按结束时间排序 + DP 状态为最后结束时间 + 资源占用)
我们可以用更简单的思路:
先将区间按结束时间排序,设 dp[i][r] 表示考虑前 i 个区间,且当前最后一个被选择的区间结束时间之后,资源占用为 r 时的最大价值。
这里“资源占用”是指:在最后一个区间的结束时间之后,还有某些机器被占用直到更晚的时间,但那些占用会在后面区间的开始时间之前释放。这依然难以表示。
实际上,更常用的方法是将区间按开始时间排序,用资源占用作为状态,用 DP[t][used] 表示在时间 t 时资源占用为 used 的最大价值。但时间 t 是离散化的区间端点,转移时:
- 如果不选当前区间,则 t 前进到下一个事件点,used 不变。
- 如果选当前区间,则需要满足 used + r_i ≤ R,且当前时间 t 就是 s_i,然后进入新状态 dp[e_i][used + r_i] = max(..., dp[t][used] + v_i)。
这相当于“在开始时间决定是否选择,结束时间释放资源”。
7. 具体算法步骤
- 将所有区间按开始时间升序排序,如果开始时间相同按结束时间升序。
- 提取所有时间点(开始和结束),去重并排序,得到时间点数组 times。
- 设 dp[time_index][used] 表示在时间点 times[time_index] 时,资源占用为 used 的最大价值。初始化 dp[0][0] = 0,其他为 -inf。
- 我们按时间顺序遍历 times,同时维护一个事件队列:在时间 t 时,有哪些区间开始,有哪些区间结束。
- 对于每个时间点 t,先处理结束事件:释放资源,更新 dp[t][used] 为释放后的状态。
- 然后处理开始事件:对于每个在 t 开始的区间 (s, e, v, r),对于每个 used,如果 used + r ≤ R,则尝试选择它:
- 新状态在时间 e 时,资源占用为 used + r,价值增加 v。
- 同时,在每个时间点,可以不选任何开始的区间,直接继承状态到下一个时间点。
- 最终答案:所有 dp[time][used] 的最大值。
8. 复杂度分析
时间点最多 2n 个,R ≤ 10,状态数 O(nR)。每个区间在每个时间点尝试转移,总复杂度 O(n^2 R),在 n=1000 时可能较大,但可通过优化事件处理降低。
9. 示例
假设 R=3,区间:
1: s=1, e=4, v=5, r=2
2: s=2, e=5, v=6, r=2
3: s=3, e=6, v=4, r=1
按算法:
- 时间点:1,2,3,4,5,6
- 在 t=1,区间1开始,used=0→选,则 t=4 时 used=2,价值=5
- 在 t=2,区间2开始,但此时 used=2,如果选需 r=2,则 2+2>R=3,不能选
- 在 t=3,区间3开始,此时 used=2,选需 r=1,2+1=3≤R,可以选,则 t=6 时 used=3,价值=5+4=9
- 最终最大价值=9
10. 总结
此题是带权区间调度的进阶版,增加了资源约束。
核心解法是将时间离散化,用 DP[时间][资源占用] 进行状态转移,按时间顺序处理区间开始和结束事件。
由于 R 很小,状态数可接受,可以在 O(n^2 R) 时间内求解。