文章目录

带权区间调度问题

带权区间调度问题

一、问题定义

给定 n 个区间,每个区间包含三个属性:起点 \(s_i\)、终点 \(e_i\)、权值 \(w_i\)。要求选出若干互不相交的区间,使得所选区间的权值总和最大。

与无权区间调度区分:无权问题求最多区间数,可用贪心算法;带权问题求最大权值和,必须用动态规划求解。

二、核心算法思想

标准最优解法为 排序 + 动态规划 + 二分查找优化,整体时间复杂度 \(O(n\log n)\),核心分为三步:

    1. 将所有区间按终点升序排序,保证终点单调有序;
    1. 对每个区间,通过二分查找找到最后一个与它不相交的前驱区间
    1. 用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 比较(允许不选任何区间);常规题目权值均为正,无需额外处理。


七、解题步骤

  1. 按终点升序排区间;
  2. 二分找每个区间的最后一个不相交前驱;
  3. DP 决策:选则加前驱最优解,不选则继承前一个;
  4. dp[n] 即为最终答案。

注:部分由 AI 润色

posted @ 2026-09-03 07:22  lvwangshu  阅读(11)  评论(0)    收藏  举报