把区间按长度排序后做双指针,在线段树上维护当前长度窗口内的最大覆盖次数,找到最小可行长度差。
OJ: luogu
题目 ID: P1712
难度:提高+/省选-
标签:双指针线段树离散化区间覆盖建模
日期: 2026-06-21 02:53
题意
给出 n 个闭区间,要恰好选出 m 个。
如果这 m 个区间存在公共点,那么这个方案合法。
方案代价是被选区间中的最长长度减最短长度。
求所有合法方案中的最小代价;如果不存在合法方案,输出 -1。
思路
先看一个可以直接验证想法的朴素解:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 30;
const int INF = 0x3f3f3f3f;
struct Interval {
int l, r;
int len;
};
int n, m;
Interval a[MAXN];
int ans = INF;
void dfs(int idx, int chosen, int cur_l, int cur_r, int min_len, int max_len) {
if (chosen == m) {
ans = min(ans, max_len - min_len);
return;
}
if (idx > n) {
return;
}
if (chosen + (n - idx + 1) < m) {
return;
}
if (chosen > 0 && max_len - min_len >= ans) {
return;
}
// 选择当前区间。
int next_l = a[idx].l;
int next_r = a[idx].r;
int next_min_len = a[idx].len;
int next_max_len = a[idx].len;
if (chosen > 0) {
next_l = max(cur_l, a[idx].l);
next_r = min(cur_r, a[idx].r);
next_min_len = min(min_len, a[idx].len);
next_max_len = max(max_len, a[idx].len);
}
// 已选区间的公共交集只会越来越小,一旦为空就不可能再变回合法。
if (next_l <= next_r) {
dfs(idx + 1, chosen + 1, next_l, next_r, next_min_len, next_max_len);
}
// 不选当前区间。
dfs(idx + 1, chosen, cur_l, cur_r, min_len, max_len);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 这是一个小数据精确暴力:
// 枚举恰好选哪 m 个区间,再检查它们是否有公共点。
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i].l >> a[i].r;
a[i].len = a[i].r - a[i].l;
}
dfs(1, 0, -INF, INF, INF, -INF);
if (ans == INF) {
cout << -1 << '\n';
} else {
cout << ans << '\n';
}
return 0;
}brute.cpp 直接枚举选哪 m 个区间,维护这些区间交集的左右边界,再计算最长长度减最短长度。
这个思路完全正确,但只能做小数据验证。
真正的关键是不要直接枚举“选哪 m 个”,而要枚举“允许选的长度范围”。
把所有区间按长度从小到大排序。
如果某个合法方案中,最短长度是 L,最长长度是 R,那么它选中的所有区间都一定落在长度窗口 [L, R] 内。
反过来,只要某个长度窗口内存在一个点,被至少 m 个区间同时覆盖,那么就可以从这些区间里挑出恰好 m 个,得到一个合法方案。
因此答案就等价于:
- 在长度排序后的区间序列中
- 找一个最短窗口
- 使得窗口内某个点的覆盖次数至少是
m
这就可以用双指针来做:
- 右指针不断加入新区间
- 左指针在窗口仍然合法时尽量右移
窗口是否合法,需要支持:
- 加入一个区间
- 删除一个区间
- 查询当前是否存在某个点,被至少
m个区间覆盖
把所有端点离散化后,用线段树维护覆盖次数最大值即可。
加入一个区间 [l, r],就在离散化后的 [l, r] 上整体加 1;
删除时整体减 1。
只要线段树根节点维护的最大值 >= m,就说明当前窗口合法。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500000 + 5;
const int MAXP = MAXN << 1;
const int INF = 0x3f3f3f3f;
struct Interval {
int l, r;
int len;
};
int n, m;
Interval a[MAXN];
int xs[MAXP];
int xs_cnt;
int seg_max[MAXP << 2];
int lazy_add[MAXP << 2];
bool cmp_len(const Interval &x, const Interval &y) {
if (x.len != y.len) {
return x.len < y.len;
}
if (x.l != y.l) {
return x.l < y.l;
}
return x.r < y.r;
}
int get_id(int x) {
return lower_bound(xs + 1, xs + xs_cnt + 1, x) - xs;
}
void push_up(int u) {
seg_max[u] = max(seg_max[u << 1], seg_max[u << 1 | 1]);
}
void apply_add(int u, int val) {
seg_max[u] += val;
lazy_add[u] += val;
}
void push_down(int u) {
if (lazy_add[u] == 0) {
return;
}
apply_add(u << 1, lazy_add[u]);
apply_add(u << 1 | 1, lazy_add[u]);
lazy_add[u] = 0;
}
void range_add(int u, int l, int r, int ql, int qr, int val) {
if (ql <= l && r <= qr) {
apply_add(u, val);
return;
}
push_down(u);
int mid = (l + r) >> 1;
if (ql <= mid) {
range_add(u << 1, l, mid, ql, qr, val);
}
if (qr > mid) {
range_add(u << 1 | 1, mid + 1, r, ql, qr, val);
}
push_up(u);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i].l >> a[i].r;
a[i].len = a[i].r - a[i].l;
xs[++xs_cnt] = a[i].l;
xs[++xs_cnt] = a[i].r;
}
sort(xs + 1, xs + xs_cnt + 1);
xs_cnt = unique(xs + 1, xs + xs_cnt + 1) - (xs + 1);
for (int i = 1; i <= n; i++) {
a[i].l = get_id(a[i].l);
a[i].r = get_id(a[i].r);
}
sort(a + 1, a + n + 1, cmp_len);
int ans = INF;
int left = 1;
for (int right = 1; right <= n; right++) {
// 当前区间加入窗口后,它覆盖的所有坐标点都要 +1。
range_add(1, 1, xs_cnt, a[right].l, a[right].r, 1);
// 只要窗口内已经存在某个点被至少 m 个区间覆盖,
// 就可以尝试收缩左端,更新更小的长度差。
while (left <= right && seg_max[1] >= m) {
ans = min(ans, a[right].len - a[left].len);
range_add(1, 1, xs_cnt, a[left].l, a[left].r, -1);
left++;
}
}
if (ans == INF) {
cout << -1 << '\n';
} else {
cout << ans << '\n';
}
return 0;
}复杂度
排序和离散化都是
双指针过程中,每个区间最多进入窗口一次、离开窗口一次,每次在线段树上的修改是
所以总时间复杂度是
总结
这题最重要的转化是:
- 不枚举“选哪
m个区间” - 改为枚举“这些区间的长度落在哪个窗口里”
一旦做完这个转化,整题就变成:
- 双指针维护最短长度窗口
- 线段树维护窗口内的最大覆盖次数
这是一个很典型的“排序后枚举答案区间”的思路。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

