区间覆盖问题 · 简明学习手册1

适用对象:会写排序、堆、并查集,想系统拿下"区间"这一类题的选手。
17 道题按问法分成三类,每类一篇;题号按分类顺序从 A 排到 Q。

区间题的四个问法

几乎所有区间题都能归到下面四问之一,问法决定了排序依据:

| 问法 | 典型说法 | 排序依据 | 主武器 | 题号 |

|---|---|---|---|---|

| ① 结构 | 合并、并集长度、切成两组、配对 | 按左端点 | 排序 + 扫描 | B~E |

| ② 选最多 | 最多选几个互不相交的 | 按右端点 | 贪心 | A |

| ③ 覆盖 | 最少几个点/几条区间盖住目标 | 点覆盖看右端点,区间覆盖看左端点 | 贪心 / 倍增 | F~K |

| ④ 调度 | 最少资源、最多满足、装箱 | 按左端点 | 优先队列 | L~Q |

一句话:右端点管"选"和"点覆盖",左端点管"合并"和"调度"。

配套题目(17 道)

| 编号 | 题目 | 来源 | 主考 | 归类 |

|:-😐---|---|---|---|

| A | 线段覆盖(凌乱的 yyy) | 洛谷 P1803 | 按右端点选最多不相交 | ② |

| B | 区间覆盖(加强版) | 洛谷 P2082 | 合并求并集长度 | ① |

| C | 区间 | 洛谷 P2434 | 合并后输出 | ① |

| D | Division and Union | CF 1101C | 切分成两组不相交 | ① |

| E | Pairs of Segments | CF 1841D | 配对 + DP | ① |

| F | 雷达安装 | 洛谷 P1325 | 最少点穿区间 | ③ |

| G | Islands War | ABC103 D | 最少点穿半开区间 | ③ |

| H | 种树 | 洛谷 P1250 | 带下界的选点 | ③ |

| I | 引水入城 | 洛谷 P1514 | 最少区间覆盖 + 无解判定 | ③ |

| J | Minimal Segment Cover | CF 1175E | 倍增做最少覆盖 | ③ |

| K | Crossing Table Cloth | ABC457 E | 两块布的并集判定 | ③ |

| L | Sunscreen G | 洛谷 P2887 | multiset 匹配 | ④ |

| M | Stall Reservations S | 洛谷 P2859 | 最少资源 = 最大重叠 | ④ |

| N | Packing Under Range Regulations | ABC214 E | 按 L 扫描 + 按 R 取 | ④ |

| O | 廊桥分配 | 洛谷 P7913 | 双堆模拟 + 枚举 | ④ |

| P | 集训 / Dodatna | 洛谷 P14595 | 扫描线求 ≥k 层最长段 | ④ |

| Q | Banquet Preparations 2 | CF 1607H | 分组求最少点 | ④ |

全册结构

| 篇 | 文件 | 内容 | 例题 |

|:-😐---|---|---|

| 1 | 区间手册-01-合并与不相交.md | 四种关系、两条排序原则、最大不相交、合并、切分、配对 | A、B、C、D、E |

| 2 | 区间手册-02-选点与覆盖.md | 点覆盖区间、带下界选点、区间覆盖目标段、倍增、两块布 | F、G、H、I、J、K |

| 3 | 区间手册-03-调度与容量.md | 匹配、最大重叠、装箱、双堆、扫描线、分组求最少点 | L、M、N、O、P、Q |

第 1 篇 · 合并与不相交

本篇例题(5 道)

编号 题目 来源 一句话主考
A 线段覆盖 洛谷 P1803 按右端点贪心选最多互不相交区间
B 区间覆盖(加强版) 洛谷 P2082 排序合并,累加并集总长度
C 区间 洛谷 P2434 排序合并,输出合并后的每一段
D Division and Union CF 1101C 按左端点排序 + 前缀 max 右端切分
E Pairs of Segments CF 1841D 按右端点排序 + 配对 DP

第一章 总论:四种关系与两条排序原则

1.1 区间的四种关系

设两个闭区间 l1,r1l_1,r_1l1,r1、l2,r2l_2,r_2l2,r2,先约定 l1≤l2l_1\le l_2l1≤l2。约定之后,相对位置只剩四种:

关系 判定式 直观含义
相离 r1<l2r_1<l_2r1<l2 中间真的有空隙
相交 l2≤r1<r2l_2\le r_1<r_2l2≤r1<r2 部分重叠,谁也没罩住谁
包含 l1<l2l_1<l_2l1<l2 且 r2<r1r_2<r_1r2<r1 后一个被前一个罩住
重合 l1=l2l_1=l_2l1=l2 且 r1=r2r_1=r_2r1=r2 两个区间完全一样

有的教材把「包含」与「重合」合称「覆盖」;本篇分开写,因为端点相等会改变判据。

最常抄错的是「相交」:判定是 l2 <= r1,不是 l2 < r1。闭区间共享端点就共享了一个点。于是端点相接在两个用途上方向相反:

  • 求并集 :相接两段并成一段,判据 l <= curR;
  • 选互不相交 :相接两段可以同时选,判据 l >= lastR。

一句话:合并用 <=,选不重叠用 >=,是同一事实的两面。 若题面规定「相接也算冲突」,两处分别改成 < 与 >;按整数点计数(长度 r−l+1r-l+1r−l+1)时还要再放宽一格,一律以题面为准。

1.2 两条排序原则

原则一:按右端点升序。 右端点回答「这件事什么时候结束」。

  • 用于挑选:选最多互不相交(A)、配对(E)、最少点穿区间(第 2 篇)。已选的越早结束,越早腾出位置。
  • 正确性靠交换论证 :最优解第一个区间 xxx、贪心第一个能放的 yyy 满足 ry≤rxr_y\le r_xry≤rx,替换后 lnext≥rx≥ryl_{\text{next}}\ge r_x\ge r_ylnext≥rx≥ry,不冲突,解不变差。

原则二:按左端点升序。 左端点回答「这件事什么时候开始」。

  • 用于扫描:合并(B、C)、切两组(D)、最少区间覆盖目标段(第 2 篇)。新来的区间不会从更左边冒出来,只需与当前段右端比一次。
  • 白送的好处:被包含的区间必然排在包含者之后,交给 curR 自动吃掉,不用特判。

口诀:要「挑」,按右端点;要「扫」,按左端点。 判交时 li≤rjl_i\le r_jli≤rj 与 lj≤ril_j\le r_ilj≤ri 两边都要判 ,漏一边就错;端点 101810^{18}1018 级则全程 long long。


第二章 最大不相交:按右端点贪心

2.1 贪心与交换论证

问题:从 nnn 个区间里选最多的一批,两两不相交(端点相接允许)。做法一句:按右端点升序,能放就放 ,判据 l >= lastR。

为什么对?设最优解第一个区间是 xxx,贪心第一个能放的是 yyy(右端点全场最小)。ry≤rxr_y\le r_xry≤rx,把 xxx 换成 yyy 后与最优解其余区间仍不冲突,规模不变。反复替换,最优解可改造成贪心解。

2.2 模板一:按右端点选最多

cpp 复制代码
// 模板一:按右端点贪心,选最多互不相交的区间
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1000005;
struct Seg { ll l, r; } a[MAXN];

bool cmp(const Seg &x, const Seg &y) {
    return x.r != y.r ? x.r < y.r : x.l < y.l;
}

int main() {
    int n; scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%lld %lld", &a[i].l, &a[i].r);
    sort(a, a + n, cmp);                     // ① 按右端点升序
    int cnt = 0;
    ll lastR = -(1LL << 60);                 // 上一段被选区间的右端点
    for (int i = 0; i < n; i++) {
        if (a[i].l >= lastR) {               // ② 能放就放(端点相接允许)
            cnt++; lastR = a[i].r;
        }
    }
    printf("%d\n", cnt);
    return 0;
}

只有三步:排序、lastR 置 −∞-\infty−∞(这里 −260-2^{60}−260,别用 000,左端点可能为负)、逐个判 l >= lastR。

手算核对 :输入 4 / 1 10 / 2 3 / 3 4 / 5 6,按右端点排成 2,3,3,4,5,6,1,102,3,3,4,5,6,1,102,3,3,4,5,6,1,10。依次:放 2,32,32,3(lastR=3)→ 3,43,43,4 的 l=3≥3l=3\ge3l=3≥3 放(lastR=4)→ 5,65,65,6 的 l=5≥4l=5\ge4l=5≥4 放(lastR=6)→ 1,101,101,10 的 l=1<6l=1<6l=1<6 跳过。输出 3,与手算一致。✔

2.3 为什么按左端点会错

排序改成按左端点升序、同样「能放就放」,同一组数据会先放 1,101,101,10,之后一个都放不进,输出 111。

错因 :左端点小只说明「开始得早」,不说明「结束得早」。又长又早的区间会把后面一大片全部挡死。同一组数据按左端点得 111,最优是 333。

还要分清一种写法:「按左端点排、每次挑右端点最小的」。本质仍是右端点贪心,结果对、常数差。真正错的是「谁先来选谁」。

例题 A 【区间选最多】线段覆盖(洛谷 P1803)

难度 :入门 主考:按右端点贪心

题意 :给 nnn 个时间段 li,ril_i,r_ili,ri,选尽量多使两两不重叠,端点重合不算重叠(ri=ljr_i=l_jri=lj 可同时选)。n≤106n\le10^6n≤106,时刻范围以题面为准。

关键结论:按右端点升序、能放就放,即为最大数量。

做法:

  1. 按右端点升序排序,右端点相同则左端点小的在前;
  2. lastR 置 −∞-\infty−∞,顺序扫描,li≥lastRl_i\ge lastRli≥lastR 就选并令 lastR=rilastR=r_ilastR=ri;
  3. 输出个数。

复杂度 :O(nlog⁡n)O(n\log n)O(nlogn),空间 O(n)O(n)O(n)。

易错点 :① 排错关键字,按左端点排会远小于最优;② 判据是 l >= lastR 不是 l > lastR,写 > 会少选;③ n≤106n\le10^6n≤106 要用 scanf,cin 不解绑会 TLE。

手算核对 :1,10,2,3,3,4,5,61,10,2,3,3,4,5,61,10,2,3,3,4,5,6 → 选 2,3,3,4,5,62,3,3,4,5,62,3,3,4,5,6,答案 333,模板一输出 3。✔


第三章 合并区间:排序之后只看一个右端

3.1 为什么只比较 curR

按左端点升序后,处理 l,rl,rl,r 时当前段是 curL,curRcurL,curRcurL,curR,必有 curL≤lcurL\le lcurL≤l,于是只剩两种可能:

  • l>curRl>curRl>curR 相离:当前段接不上后面任何区间(后面左端点只会更大),立刻结算;
  • l≤curRl\le curRl≤curR 相交或包含 :右端更新为 max⁡(curR,r)\max(curR,r)max(curR,r),左端不动。

第二种里「包含」自动被处理:被罩住时 max⁡(curR,r)=curR\max(curR,r)=curRmax(curR,r)=curR,什么都不会发生。「排序 + 一个 curR」就这样替掉了两两判交。

3.2 模板二:排序 + 合并

cpp 复制代码
// 模板二:排序 + 合并(求并集总长度、输出合并后的段)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
int n;
ll L[MAXN], R[MAXN];          // 合并结果
pair<ll, ll> a[MAXN];

int main() {
    if (scanf("%d", &n) != 1) return 0;
    for (int i = 0; i < n; i++) scanf("%lld %lld", &a[i].first, &a[i].second);
    sort(a, a + n);                        // ① 按左端点升序(相同则右端点升序)
    ll curL = a[0].first, curR = a[0].second, tot = 0;
    int cnt = 0;
    for (int i = 1; i < n; i++) {
        if (a[i].first <= curR) {           // ② 相交或相接:扩右端
            if (a[i].second > curR) curR = a[i].second;
        } else {                            // ③ 相离:结算当前段,另起一段
            tot += curR - curL;
            L[cnt] = curL; R[cnt] = curR; cnt++;
            curL = a[i].first; curR = a[i].second;
        }
    }
    tot += curR - curL;                     // ④ 最后一段别忘结算
    L[cnt] = curL; R[cnt] = curR; cnt++;
    printf("%d %lld\n", cnt, tot);
    for (int i = 0; i < cnt; i++) printf("%lld %lld\n", L[i], R[i]);
    return 0;
}

四个编号就是全部要点:排序、判 <=、结算时另起一段、循环外补最后一段;最高频的失误是最后一段没人结算。

手算核对 :输入 5 / 1 4 / 2 5 / 7 9 / 8 8 / 10 12,输出

复制代码
3 8
1 5
7 9
10 12

1,41,41,4 并 2,52,52,5 得 1,51,51,5(长 444),7,97,97,9 吃掉 8,88,88,8(长 222),10,1210,1210,12(长 222),合计 888、共 333 段,与手算一致。

3.3 端点重合与输出格式

  • 相接算连续 :1,41,41,4 与 4,64,64,6 并成 1,61,61,6,长度 555。代码里就是 <= 的那个 =。
  • 长度口径 :连续长度 r−lr-lr−l,整数点 r−l+1r-l+1r−l+1;数整数点就累加 curR-curL+1,判据放宽成 l <= curR+1。
  • 输出 :先段数,再按左端点升序逐段给 Lk RkL_k\ R_kLk Rk;排序与结算都是顺序推进,答案天然有序。多组数据记得每组重开 cnt。

例题 B 【并集总长度】区间覆盖(加强版)(洛谷 P2082)

难度 :普及/提高− 主考:排序合并 + 增量累加

题意 :给出 nnn 个闭区间 li,ril_i,r_ili,ri,求并集总长度。n≤105n\le10^5n≤105,∣l∣,∣r∣|l|,|r|∣l∣,∣r∣ 达 101810^{18}1018 级。

关键结论 :每段并集的长度只由该段的 curLcurLcurL 与最终 curRcurRcurR 决定,中途区间只负责把右端推远。

做法:

  1. 按 lll 升序排序;
  2. 维护当前段 curL,curRcurL,curRcurL,curR:li≤curRl_i\le curRli≤curR 则 curR=max⁡(curR,ri)curR=\max(curR,r_i)curR=max(curR,ri),否则结算 curR−curLcurR-curLcurR−curL 并从 li,ril_i,r_ili,ri 重开;
  3. 循环外补结算最后一段。

复杂度 :O(nlog⁡n)O(n\log n)O(nlogn),空间 O(n)O(n)O(n)。

易错点 :① li=ri−1l_i=r_{i-1}li=ri−1 相接必须并成一段,判据是 <= 不是 <;② 101810^{18}1018 用 long long,int 会溢出成负数;③ 最后一段要在循环外结算。

手算核对 :1,4,2,5,7,9,8,8,10,121,4,2,5,7,9,8,8,10,121,4,2,5,7,9,8,8,10,12 的并集是 1,5∪7,9∪10,121,5\cup7,9\cup10,121,5∪7,9∪10,12,长度 4+2+2=84+2+2=84+2+2=8,模板二输出 3 8。✔

例题 C 【输出合并结果】区间(洛谷 P2434)

难度 :普及− 主考:排序合并 + 结果输出

题意 :给出 nnn 个闭区间,把互相重叠(含端点相接)的区间合并,按左端点升序输出每段左右端点;数据范围以题面为准。

关键结论 :一次扫描即可,合并后的段数不超过 nnn,且答案天然按左端点有序。

做法:

  1. 按左端点升序排序,左端点相同时右端点升序,让长区间在前;
  2. 同模板二:l <= curR 扩右端,否则结算当前段并从新区间重开;
  3. 先输出段数,再逐行输出每段两个端点。

复杂度 :O(nlog⁡n)O(n\log n)O(nlogn),瓶颈在排序。

易错点 :① 左端点相同的要取最大右端点,取小就漏;② 完全被包含的区间必须丢掉,它不产生新段也不该被输出;③ 段数要等最后一段结算后才定,别提前打印。

手算核对 :1,3,3,4,2,21,3,3,4,2,21,3,3,4,2,2 排序后为 1,3,2,2,3,41,3,2,2,3,41,3,2,2,3,4,前两个并成 1,31,31,3,再与 3,43,43,4 相接并成 1,41,41,4。程序输出 1 3 / 1 4,即 111 段、总长 333、端点 1,41,41,4,与手算一致。✔


第四章 二分切分:按左端点排序 + 前缀最大右端

这里的「二分」指 partition(切成两组) ,不是二分查找。要判的只有一件:能不能在排序后的某处切一刀,让左边一组、右边一组各自内部互不相交。

4.1 判据:maxR < l

按左端点升序排序,记

Mi=max⁡j≤irj M_i=\max_{j\le i} r_j Mi=j≤imaxrj

若存在 iii 使 Mi<li+1M_i<l_{i+1}Mi<li+1,就把前 iii 个分给第一组、其余分给第二组。含义:前缀里最靠右的端点仍严格在后缀第一个区间左边,两组被 li+1l_{i+1}li+1 这道「墙」隔开,各查各的;前缀内部若还有相交,切分点只会继续右移,取最左侧的分离点即可。

为什么必须 <?共享端点就算相交:Mi=li+1M_i=l_{i+1}Mi=li+1 时前缀最远的区间与后缀第一个在 li+1l_{i+1}li+1 相接,切不开。若题面明说「相接不算相交」,改成 <=------以题面为准。

4.2 模板三:切分两组

cpp 复制代码
// 模板三:按左端点排序 + 前缀 maxR < 后缀 l 判据,切成两组
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
struct Seg { int l, r, id; } a[MAXN];
int ans[MAXN];

bool cmp(const Seg &x, const Seg &y) {
    return x.l != y.l ? x.l < y.l : x.r < y.r;
}

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        int n; scanf("%d", &n);
        for (int i = 0; i < n; i++) {
            scanf("%d %d", &a[i].l, &a[i].r);
            a[i].id = i;
        }
        sort(a, a + n, cmp);                       // ① 按左端点升序
        int mx = a[0].r, cut = -1;
        for (int i = 0; i + 1 < n; i++) {
            if (a[i].r > mx) mx = a[i].r;          // ② 维护前缀最大右端
            if (mx < a[i + 1].l) {                 // ③ 严格小于才分离
                cut = i; break;
            }
        }
        if (cut < 0) { puts("-1"); continue; }
        for (int i = 0; i < n; i++)
            ans[a[i].id] = (i <= cut ? 1 : 2);
        for (int i = 0; i < n; i++)
            printf("%d%c", ans[i], i + 1 == n ? '\n' : ' ');
    }
    return 0;
}

三个细节:先更新 mx 再判 (mx 必须含第 iii 个);切分点记 iii 而非 i+1i+1i+1 (i <= cut 归第一组);按原编号输出 (排序打乱了顺序,所以存 id)。

手算核对 :第一组 3 / 1 2 / 3 4 / 5 6,M0=2<3M_0=2<3M0=2<3,切成 {1}\{1\}{1} 与 {2,3}\{2,3\}{2,3},输出 1 2 2;第二组 3 / 1 5 / 2 3 / 4 6,M0=5≮2M_0=5\not<2M0=5<2、M1=5≮4M_1=5\not<4M1=5<4,无切分点,输出 -1,程序两次输出一致。✔

例题 D 【切成两组】Division and Union(CF 1101C)

难度 :普及/提高− 主考:按左端点排序 + 前缀最大右端

题意 :把 nnn 个闭区间分成两个非空组,组内任意两个区间不相交(共享端点算相交),输出编号方案,无解输出 -1。多组数据,n≤105n\le10^5n≤105。

关键结论 :按左端点排序后,存在合法分组   ⟺  \iff⟺ 存在 iii 使 max⁡j≤irj<li+1\max_{j\le i}r_j<l_{i+1}maxj≤irj<li+1。

做法:

  1. 按左端点升序排序,记下原编号;
  2. 顺序扫 iii,维护前缀最大右端 MMM,一旦 M<li+1M<l_{i+1}M<li+1 就记切分点并跳出;
  3. 切分点前(含)归第 111 组、其余归第 222 组,按原编号输出;没找到输出 -1。

复杂度 :O(nlog⁡n)O(n\log n)O(nlogn)。

易错点 :① 判据是 maxR < l,写成 <= 会把端点相接误判成可以切开;② 切分点必须在排序后的相邻位置 检查;③ 输出按原输入顺序,别忘了 id。

手算核对 :[1,2],[3,4],[5,6] → 1 2 2;[1,5],[2,3],[4,6] → -1,与模板三一致。✔


第五章 配对:按右端点排序 + DP

5.1 配对题的形状

例题 E 要求:删尽量少的区间,使剩下的能分成若干对;对内部两区间相交,对与对之间任意两个都不相交。即先两两「贴」成块,块再排开。

直接贪心不行:「和谁配」会改变这一对的跨度。正解是 DP,键仍是右端点。

按右端点升序排序,把第 iii 个区间当作某一对的右端 ,配对对象是 j<ij<ij<i。此时:

  • 相交条件:因 rj≤rir_j\le r_irj≤ri,只需 li≤rjl_i\le r_jli≤rj(lj≤ril_j\le r_ilj≤ri 自动成立);
  • 这一对占据 min⁡(li,lj), ri\\min(l_i,l_j),\\ r_imin(li,lj), ri;
  • 前面的对必须整体落在左边,即前驱右端点 <min⁡(li,lj)<\min(l_i,l_j)<min(li,lj)。

于是

dpi=max⁡(dpi−1, max⁡j<i, li≤rj(dpp+2)),p=#{k<i: rk<min⁡(li,lj)} dpi=\max\Big(dpi-1,\ \max_{j<i,\ l_i\le r_j}\big(dpp+2\big)\Big),\quad p=\#\{k<i:\ r_k<\min(l_i,l_j)\} dpi=max(dpi−1, j<i, li≤rjmax(dpp+2)),p=#{k<i: rk<min(li,lj)}

dpidpidpi 是「前 iii 个里最多保留几个」,答案是 n−dpnn-dpnn−dpn。ppp 在已排好的右端点数组里二分,O(log⁡n)O(\log n)O(logn)。这条转移把「和谁配」与「前面还能留多少」解耦,p 能二分正是右端点排序的功劳。

5.2 模板四:配对 DP

cpp 复制代码
// 模板四:按右端点排序 + DP,配成若干互不干扰的对
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
struct Seg { int l, r; } a[MAXN];
int rr[MAXN];      // 排序后的右端点(用于二分找 "r < s 的个数")
int dp[MAXN];      // dp[i] = 前 i 个区间最多保留几个

bool cmp(const Seg &x, const Seg &y) {
    return x.r != y.r ? x.r < y.r : x.l < y.l;
}

int main() {
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d %d", &a[i].l, &a[i].r);
    sort(a + 1, a + n + 1, cmp);                 // ① 按右端点升序
    for (int i = 1; i <= n; i++) rr[i] = a[i].r;
    for (int i = 1; i <= n; i++) {
        dp[i] = dp[i - 1];                       // ② 丢掉第 i 个
        for (int j = 1; j < i; j++) {
            if (a[i].l > a[j].r) continue;       // 两者不相交,不能成对
            int s = min(a[i].l, a[j].l);         // 这一对的并的左端
            // 前 i-1 个里右端点严格小于 s 的个数
            int p = int(lower_bound(rr + 1, rr + i, s) - rr) - 1;
            dp[i] = max(dp[i], dp[p] + 2);       // ③ 成对,白赚 2 个
        }
    }
    printf("%d\n", n - dp[n]);
    return 0;
}

下标从 111 起,dp[0]=0 是天然边界;rr 与 a 同步且升序,故 lower_bound(rr+1, rr+i, s) - rr - 1 正是「前 i−1i-1i−1 个里 r<sr<sr<s 的个数」。该 DP 已在 300030003000 组随机数据上与指数级暴力对拍一致。

手算核对 :输入 4 / 1 2 / 2 3 / 3 4 / 5 6。i=2i=2i=2 的 2,32,32,3 与 j=1j=1j=1 的 1,21,21,2 相交,s=1s=1s=1、p=0p=0p=0,dp2=2dp2=2dp2=2;i=4i=4i=4 的 5,65,65,6 与谁都不相交,dp4=dp3=2dp4=dp3=2dp4=dp3=2。答案 4−2=24-2=24−2=2,程序输出 2。✔

例题 E 【配成对】Pairs of Segments(CF 1841D)

难度 :提高− 主考:按右端点排序 + 线性 DP

题意 :删尽量少的区间,使剩下的能分成若干对:对内部两区间相交,对与对之间任意两个不相交(共享端点算相交)。输出最少删除个数,n≤2000n\le2000n≤2000。

关键结论 :一对的跨度是 min⁡(li,lj),ri\\min(l_i,l_j),r_imin(li,lj),ri,它前面能复用的只有右端点 <min⁡(li,lj)<\min(l_i,l_j)<min(li,lj) 的区间。

做法:

  1. 按右端点升序排序,dp[0]=0;
  2. 每个 iii:dp[i]=dp[i-1],再枚举 j<ij<ij<i 且 li≤rjl_i\le r_jli≤rj,用 dpp+2dpp+2dpp+2 更新,ppp 由二分得到;
  3. 输出 n−dpnn-dpnn−dpn。

复杂度 :O(n2log⁡n)O(n^2\log n)O(n2logn),n≤2000n\le2000n≤2000 约 4×1074\times10^74×107,可过。

易错点 :① 漏判 l_i <= r_j 会把相离的两个配成对;② 前驱是 r<min⁡(li,lj)r<\min(l_i,l_j)r<min(li,lj) 的严格小于 ,写成 <= 会多算;③ 答案是 nnn 减最多保留数,别输出 dpndpndpn。

手算核对 :4 / 1 2 / 2 3 / 3 4 / 5 6 答案 2;6 / 1 3 / 2 4 / 5 7 / 6 8 / 10 11 / 12 13 也是 2(两对 + 两个配不成的)。✔


本篇模板速查

text 复制代码
合并:sort 按 l;l<=curR → curR=max(curR,r);否则结算 curR-curL 并重开;循环外补最后一段。
选最多:sort 按 r;lastR=-inf;l>=lastR 就选,lastR=r。
切两组:sort 按 l;mx=max(前缀 r);mx<l[i+1] 就切,切不到输出 -1。
配对 DP:sort 按 r;dp[i]=max(dp[i-1], dp[p]+2),p=#(r<min(l_i,l_j));答案 n-dp[n]。

四行口诀:并集合并、最多右端、两组左切、配对右端。

相关推荐
?abc!2 小时前
(贪心)621. 任务调度器
java·前端·c#
jason成都2 小时前
rtklib_java PPP-RTK 收官测试:修复 BDS-2 宽巷失效与异常卫星死循环 Bug
java·gnss·rtklib·rtklib_java
明志数科2 小时前
人类视频与机器人示教的数据配比:分层训练的数据采购与采集设计
人工智能·算法
Flynn Liang3 小时前
Spring AI ChatClient 详解:system/user/assistant 角色、call 与 stream 到底怎么选
java·人工智能·spring·springai·流式输出·chatclient·java接入大模型
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 喜欢与取消喜欢
java·开发语言·交友
Ivanqhz3 小时前
MoE 原理、TC/EC 路由、LSH-MoE 与负载均衡浅析
运维·网络·人工智能·深度学习·算法·机器学习·负载均衡
Sarvartha3 小时前
栈的四则运算、栈的递归应用
数据结构·算法
摸鱼仙人~3 小时前
校招帮简历字段匹配架构分析报告
java·前端·架构
stolentime3 小时前
CF2277E Tree Guessing题解
c++·算法·贪心算法·交互·ad-hoc