贪心算法进阶:区间调度与最少资源整合解析

信奥新赛季备考 · 贪心策略高频考点。本文用一道原创「校园开放日摊位排期」题,带你吃透区间调度与最少资源整合两类经典贪心,附 C++/Python 双版解法、复杂度、易错点与进阶。

一、引言:为什么贪心是考场"性价比之王"

在信息学竞赛里,贪心算法以"每步只看当前最优、代码量极少、运行飞快"著称。统计上看,约 30% 的普及组题、15% 的提高组题会单独或结合使用贪心思想,常和排序、优先队列、扫描线搭配出现。

贪心能用的前提只有两个:局部最优能推出全局最优(贪心选择性质) + 子问题最优解可拼接(最优子结构)。一旦证明成立,原本需要 O(2ⁿ) 暴搜或 O(n²) 动态规划的问题,往往能被压缩到 O(n log n)。

今天我们就用区间类问题------贪心最经典的战场------来实战。

二、题目与目标(原创题:校园开放日摊位排期系统)

学校开放日来了 N 个社团申请摊位,每个社团给出自己的活动时间 [开始时刻, 结束时刻)(左闭右开,端点相接不算冲突)。你需要回答两个调度问题:

  • 任务 A(区间调度) :同一时刻一个摊位只能办一场活动,求最多能安排多少场不冲突的活动
  • 任务 B(最少资源整合) :若所有活动都必须办成,同一时刻并行活动最多有几场?这等价于最少需要开放几个摊位(资源)才能不冲突地容纳全部活动。

输入格式 :第一行整数 N;接下来 N 行,每行两个整数 s e,表示活动的起止时刻。

样例输入

复制代码
6
1 3
2 5
3 6
5 7
6 9
8 10

样例输出

复制代码
任务A 最多可安排活动数:3
任务B 最少需要摊位数:2

三、核心考点拆解

  1. 排序预处理是贪心的命门:几乎所有区间贪心都先排序,排错关键字全盘皆输。
  2. 任务 A 按"结束时间"升序:每次选"结束最早且不与已选冲突"的活动,能给后面留出最多空档。
  3. 任务 B 用扫描线/事件法:把每个区间拆成"开始 +1、结束 −1"两个事件,扫描一遍即可求出任意时刻的最大并发数。
  4. 端点开闭处理 :区间按 [s, e) 理解,两个区间如 [1,3)[3,6) 首尾相接不算重叠------事件排序时同坐标必须先处理结束 (−1) 再处理开始 (+1)
  5. 多关键字排序:同结束时间时顺序任意;同坐标事件必须靠第二关键字区分 ±1。

四、解法拆解

任务 A:最多不重叠活动(区间调度)

思路 :按结束时间升序排列,维护上一个被选活动的结束时刻 lastEnd。从左到右遍历,只要当前活动开始 s ≥ lastEnd(端点相接允许),就选它并更新 lastEnd = e

正确性直觉:局部选"结束最早"的,留给后续的可选空间最大,可证这是全局最优(反证法:若存在更优解,把其中最早结束的活动替换成我们的选择,不减少数量且结束不更晚)。

C++ 实现

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 任务A:最多不重叠区间(区间调度)
// intervals: 每个为 [start, end),返回可安排的最大活动数
int maxNonOverlap(vector<pair<int, int>>& intervals) {
    // 关键:按结束时间升序排序
    sort(intervals.begin(), intervals.end(),
         [](const pair<int, int>& a, const pair<int, int>& b) {
             return a.second < b.second;   // 仅按结束时间排,结束相同则任意
         });
    int cnt = 0;
    int lastEnd = -1;          // 已选最后一个活动的结束时间(初值视为 -∞)
    for (auto& it : intervals) {
        int s = it.first, e = it.second;
        if (s >= lastEnd) {    // 开始不早于上一个结束(端点相接不冲突)
            cnt++;
            lastEnd = e;
        }
    }
    return cnt;
}

Python 实现

python 复制代码
def max_non_overlap(intervals):
    """intervals: list of (start, end) 左闭右开,返回最多不重叠区间数"""
    ivs = sorted(intervals, key=lambda x: x[1])   # 按结束时间升序
    cnt = 0
    last_end = float('-inf')
    for s, e in ivs:
        if s >= last_end:        # 端点相接不算冲突
            cnt += 1
            last_end = e
    return cnt

样例手推 :排序后顺序为 (1,3)(2,5)(3,6)(5,7)(6,9)(8,10)。依次选 (1,3)(3,6)(6,9),共 3 场,其余都因冲突被跳过。


任务 B:最少资源(最大并发区间数)

思路 :把每个区间 [s, e) 拆成事件 (s, +1)(e, −1)。按坐标升序扫描,同坐标时结束事件 (−1) 排在开始事件 (+1) 之前 。用 cur 维护当前并发数,mx 记录峰值------峰值就是"同一时刻最多有几场并行",也就是最少需要的摊位数。

C++ 实现

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 任务B:最少所需资源数 = 任意时刻的最大重叠区间数
int minResources(vector<pair<int, int>>& intervals) {
    vector<pair<int, int>> events;   // (坐标, +1 或 -1)
    for (auto& it : intervals) {
        events.push_back({it.first, 1});    // 区间开始:占用 +1
        events.push_back({it.second, -1});  // 区间结束:释放 -1
    }
    // 同坐标时,结束(-1)必须先于开始(+1)处理,
    // 这样"首尾相接"的两个区间不会被误算成重叠
    sort(events.begin(), events.end(),
         [](const pair<int, int>& a, const pair<int, int>& b) {
             if (a.first != b.first) return a.first < b.first;
             return a.second < b.second;    // -1 < +1,先处理结束
         });
    int cur = 0, mx = 0;
    for (auto& ev : events) {
        cur += ev.second;
        mx = max(mx, cur);
    }
    return mx;
}

Python 实现

python 复制代码
def min_resources(intervals):
    """最少资源数 = 任意时刻最大重叠区间数"""
    events = []
    for s, e in intervals:
        events.append((s, 1))
        events.append((e, -1))
    # 同坐标:结束(-1)先于开始(+1)
    events.sort(key=lambda x: (x[0], x[1]))
    cur = mx = 0
    for _, d in events:
        cur += d
        mx = max(mx, cur)
    return mx

样例手推 :事件流为 1:+1(1) → 2:+1(2) → 3:-1(1) → 3:+1(2) → 5:-1(1) → 5:+1(2) → 6:-1(1) → 6:+1(2) → 7:-1(1) → 8:+1(2) → 9:-1(1) → 10:-1(0)cur 的峰值始终为 2 ,故最少需要 2 个摊位。注意 (3,-1)(3,+1) 之前处理,避免了 [1,3)[3,6) 被误算成重叠。

五、时间与空间复杂度

任务 时间复杂度 空间复杂度 说明
A 区间调度 O(n log n) O(1)(原地排序) 瓶颈在排序,扫描为 O(n)
B 最少资源 O(n log n) O(n) 事件数组 2n,排序 + 扫描

两者都远优于暴搜(2ⁿ)和普通 DP(O(n²)),在 n 达到 10⁵ 时仍轻松跑过。

六、易错点(考前必看)

  1. 任务 A 排错关键字 :必须按结束时间升序;若误按开始时间排序,会得到错误答案(贪心选择性质不成立)。
  2. 端点开闭理解错 :本题约定 [s, e)。若把区间当成闭区间,会导致"首尾相接"被误判冲突或误判重叠。
  3. 任务 B 同坐标事件顺序(e, −1) 必须在 (s', +1)(当 e == s')之前处理,否则把相邻区间算成重叠,资源数虚高。
  4. lastEnd 初值 :应初始化为负无穷(如 −1 或 float('-inf')),否则第一个区间可能因 s < 0 类初值被漏选(时刻非负时 −1 也行,但负无穷更稳妥)。
  5. 坐标跨度大时别用差分数组 :若时刻范围到 10⁹,开数组会爆内存------务必用事件扫描线法而非 diff[] 数组。
  6. 多组测试别忘重置cnt / cur / mx 等计数器每组需重新初始化,全局变量残留是常见 WA 来源。

七、进阶延伸

掌握上面两题后,可顺势拓展以下变体:

(1) 区间合并:把所有重叠/相接区间合并成不相交的大区间。

python 复制代码
def merge_intervals(intervals):
    ivs = sorted(intervals)
    res = []
    for s, e in ivs:
        if res and s <= res[-1][1]:      # 与上一区间重叠或相接
            res[-1][1] = max(res[-1][1], e)
        else:
            res.append([s, e])
    return res

(2) 区间覆盖 :用最少区间覆盖住整个 [1, T],每次贪心选"能盖住当前起点且延伸最远"的区间;若出现盖不住的空隙则返回 −1。

python 复制代码
def cover_segment(intervals, T):
    ivs = sorted(intervals)              # 按左端点升序
    pos, used, i, n = 1, 0, 0, len(ivs)
    while pos <= T:
        best_r = -1
        while i < n and ivs[i][0] <= pos:
            best_r = max(best_r, ivs[i][1])
            i += 1
        if best_r < pos:
            return -1                    # 出现无法覆盖的空隙
        used += 1
        pos = best_r + 1
    return used

(3) 带权活动选择(加权区间调度):每个活动有权重,求最大权重不重叠子集。按结束时间排序后 DP + 二分查找前驱,复杂度 O(n log n)。

python 复制代码
def weighted_activity(intervals, vals):
    n = len(intervals)
    items = sorted(zip(intervals, vals), key=lambda x: x[0][1])  # 按结束时间
    ends = [it[0][1] for it in items]
    dp = [0] * (n + 1)
    for k in range(1, n + 1):
        s, e = items[k - 1][0]
        v = items[k - 1][1]
        lo, hi, p = 0, k - 1, -1
        while lo < hi:                   # 找结束时间 <= s 的最后一个
            mid = (lo + hi) // 2
            if ends[mid] <= s:
                p = mid; lo = mid + 1
            else:
                hi = mid
        take = v + (dp[p + 1] if p >= 0 else 0)
        dp[k] = max(dp[k - 1], take)
    return dp[n]

(4) 堆优化扫描线:任务 B 求最大并发也可改用优先队列实时维护"结束时间最小"的活动,堆的峰值即答案,适合需要动态增删的场景。

(5) 区间选点:求最少需要几个点,使每个区间至少包含一个点------按右端点升序,每次在末尾放点即可。

八、小结与互动

今天我们拿下了两道区间贪心经典题:区间调度(按结束时间贪心)最少资源整合(扫描线求最大并发)。记住三句话------"先排序、再贪心、端点顺序别搞反",区间类题基本就稳了。

你在新赛季刷题时还遇到过哪些"看着像贪心、其实是 DP/二分"的坑?欢迎在评论区聊聊你的踩坑经历,我们一起对拍验证 🚀

如果觉得有用,记得点赞 + 收藏 + 关注,下一篇我们继续拆解信奥高频算法。


📚 免费少儿编程资料(夸克网盘领取)

以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
Yanjun2i1 小时前
Agent学习记录六:Tool 类 + Tool Registry
开发语言·python·学习
(Charon)1 小时前
【C++】 定时器入门:从定时任务到 epoll 驱动
开发语言·c++
2601_962381861 小时前
Python异步编程async/await核心用法
python·async/await·事件循环·异步编程·并发调度
量化吞吐机1 小时前
会写代码学量化,先把数据、规则和执行连清楚
人工智能·python
青山木1 小时前
Hot 100 --- 划分字母区间
java·数据结构·算法·leetcode·贪心算法
泡干脆面就番茄2 小时前
02_摄像头实时人脸检测与微笑识别
python·opencv
励志不掉头发的内向程序员2 小时前
【LibreCAD 2D架构】鼠标点下的坐标为什么会被“吸”走?LibreCAD 对象捕捉系统解析
开发语言·c++·qt·学习·系统架构
Sunsets_Red2 小时前
浅谈扫描线
c++·算法·编程·题解·洛谷·扫描线·信息学竞赛
a187927218312 小时前
【算法】双指针与滑动窗口(一):框架总纲——三类问题、一个原理与判决书
算法·leetcode·区间·双指针·滑动窗口·原理·算法讲解