带权区间调度问题
带权区间调度问题
一、问题定义
给定 n 个区间,每个区间包含三个属性:起点 \(s_i\)、终点 \(e_i\)、权值 \(w_i\)。要求选出若干互不相交的区间,使得所选区间的权值总和最大。
与无权区间调度区分:无权问题求最多区间数,可用贪心算法;带权问题求最大权值和,必须用动态规划求解。
二、核心算法思想
标准最优解法为 排序 + 动态规划 + 二分查找优化,整体时间复杂度 \(O(n\log n)\),核心分为三步:
-
- 将所有区间按终点升序排序,保证终点单调有序;
-
- 对每个区间,通过二分查找找到最后一个与它不相交的前驱区间;
-
- 用DP做「选/不选」决策,递推得到最优解。
三、解法
1. 排序预处理
将所有区间按终点 \(e\) 从小到大排序。
- 排序目的:让区间终点形成单调序列,为后续二分查找前驱区间提供前提;
- 终点相同时,起点顺序不影响最终结果。
2. 状态定义
设排序后的区间编号为 \(1 \sim n\)(下标从1开始,0作为空集边界):
dp[i]:考虑前 \(i\) 个区间,能选出的最大权值总和;prev[i]:满足 \(e_j \le s_i\) 的最大下标 \(j\),即最后一个与第 \(i\) 个区间不相交的区间编号。
3. 转移方程
对第 \(i\) 个区间,有两种互斥决策,取最优值:
- 不选第 \(i\) 个区间:最优解等于前 \(i-1\) 个区间的最优解\[dp[i] = dp[i-1] \]
- 选第 \(i\) 个区间:所有与 \(i\) 相交的区间都不能选,最优解为前驱区间的最优解加上当前权值\[dp[i] = dp[\text{prev}[i]] + w_i \]
最终转移公式:
\[\boldsymbol{dp[i] = \max(dp[i-1],\ dp[\text{prev}[i]] + w_i)}
\]
4. 边界条件
dp[0] = 0:0个区间时,权值和为0;- 若不存在满足条件的前驱区间,则
prev[i] = 0,对应「只选第 \(i\) 个区间」的情况。
5. 二分查找优化求前驱
由于区间按终点升序排列,\(e_1 \le e_2 \le \dots \le e_n\) 是单调序列,因此对每个 \(i\),可在 \([1,i-1]\) 范围内二分查找最大的 \(j\) 满足 \(e_j \le s_i\),单次查找时间从 \(O(n)\) 降至 \(O(\log n)\)。
四、复杂度分析
- 排序:\(O(n\log n)\)
- 预处理前驱数组:\(n\) 次二分查找,总 \(O(n\log n)\)
- DP 递推:\(O(n)\)
- 总时间复杂度:\(O(n\log n)\)
- 空间复杂度:\(O(n)\)
五、完整代码(C++)
#include <bits/stdc++.h>
using namespace std;
struct Interval {
int s, e, w; // 起点、终点、权值
};
// 按终点升序排序
bool cmp(const Interval& a, const Interval& b) { return a.e < b.e; }
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n; cin >> n;
vector<Interval> seg(n + 1); // 1~n 编号
vector<int> end_time(n + 1); // 存储各区间终点,用于二分
for (int i = 1; i <= n; i ++) cin >> seg[i].s >> seg[i].e >> seg[i].w;
// 1. 按终点升序排序
sort(seg.begin() + 1, seg.end(), cmp);
for (int i = 1; i <= n; i ++) end_time[i] = seg[i].e;
vector<int> dp(n + 1, 0);
for (int i = 1; i <= n; i ++) {
// 2. 二分查找前驱:最大的 j 满足 end_time[j] <= seg[i].s
int left = 1, right = i - 1;
int prev = 0;
while (left <= right) {
int mid = (left + right) / 2;
if (end_time[mid] <= seg[i].s) {
prev = mid;
left = mid + 1;
}
else right = mid - 1;
}
// 3. DP 转移
dp[i] = max(dp[i - 1], dp[prev] + seg[i].w);
}
cout << dp[n] << '\n';
return 0;
}
STL 简化写法(用
upper_bound替代手写二分):int prev = upper_bound(end_time.begin() + 1, end_time.begin() + i, seg[i].s) - end_time.begin() - 1;
六、关键细节与易错点
1. 端点相交判定
根据题意调整二分条件:
- 端点重合不算相交(如 [1,3] 和 [3,5] 合法):查找 \(e_j \le s_i\),即上述代码写法;
- 端点重合算相交:查找 \(e_j < s_i\),将二分条件中的
<=改为<即可。
2. 下标处理
建议区间和 dp 数组均从 1 开始编号,用 0 作为空集边界,可避免大量越界判断,简化代码。
3. 负权值情况
若存在权值为负的区间,转移时需要额外和 0 比较(允许不选任何区间);常规题目权值均为正,无需额外处理。
七、解题步骤
- 按终点升序排区间;
- 二分找每个区间的最后一个不相交前驱;
- DP 决策:选则加前驱最优解,不选则继承前一个;
- dp[n] 即为最终答案。
注:部分由 AI 润色
晨雾纸间
山中何事?松花酿酒,春水煎茶
文/lvwangshu·书于云深处
晨雾纸间
✦
原创·转载请注明出处
✦
浙公网安备 33010602011771号