P1712 [NOI2016] 区间(双指针/线段树)
题目描述
在数轴上有 n个闭区间从 1 至 n 编号,第 i 个闭区间为 [li,ri][l**i,r**i] 。
现在要从中选出 m 个区间,使得这 m 个区间共同包含至少一个位置。换句话说,就是使得存在一个 x ,使得对于每一个被选中的区间 [li,ri][l**i,r**i],都有 li≤x≤ri。
对于一个合法的选取方案,它的花费为被选中的最长区间长度减去被选中的最短区间长度。
区间 [li,ri][l**i,r**i] 的长度定义为 (ri−li) ,即等于它的右端点的值减去左端点的值。
求所有合法方案中最小的花费。如果不存在合法的方案,输出 −1。
输入格式
第一行包含两个整数,分别代表 n 和 m。
第 22 到第 (n+1) 行,每行两个整数表示一个区间,第 (i+1) 行的整数 li,ri 分别代表第 i 个区间的左右端点。
输出格式
输出一行一个整数表示答案。
输入输出样例
输入 #1复制
6 3
3 5
1 2
3 4
2 2
1 5
1 4
输出 #1复制
2
看到题目说花费为“最长区间长度减去被选中的最短区间长度”,由此可以想到先给区间按照长度排序(和别的按照左右端点排序的题不一样)。然后考虑双指针法,右端点按照长度从小到大遍历,并把当前线段覆盖的区间整体+1,然后内循环只要有一个点的覆盖次数大于等于m,左指针就不断朝右指针移动更新答案,同时把指向的区间整体-1。可以用线段树来维护覆盖次数,需要维护的就是区间点值的最大值(查询的时候直接查询树根的值即可),看到数据范围记得离散化。
证明:当右指针指向某一个区间时,左指针不断移动删除的区间必定不可能对答案有贡献(因为删掉以后仍然有点的被覆盖次数大于等于m,此时被删区间的下一个区间一定更优,这是由花费的计算方法决定的)。
可能有人会问,这样得到的答案区间一定是从小到大排序后一段连续的区间吗(此区间是排序后线段下标构成的区间)?其实不一定,比如m = 3,而线段为(1, 2), (1, 3), (5, 6), (1, 10)。但其实我们并不关心这个,排序只是为了方便计算,最终得到的就是一个最大减最小值而已。
#include <bits/stdc++.h>
using namespace std;
int n, m;
struct Seg {
int l, r, len;
} seg[500005];
bool cmp(Seg a, Seg b) {
if(a.len != b.len) return a.len < b.len;
else return a.l < b.l;
}
struct SegmentTree {
int l, r, dat, mx, add;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define add(x) tree[x].add
#define mx(x) tree[x].mx
} tree[8000005];
void build(int p, int l, int r) {
l(p) = l, r(p) = r;
if(l == r) {
mx(p) = 0;
return;
}
int mid = (l + r) >> 1;
build(2 * p, l, mid);
build(2 * p + 1, mid + 1, r);
mx(p) = 0;
}
void spread(int p) {
if(add(p)) {
add(2 * p) += add(p);
add(2 * p + 1) += add(p);
mx(2 * p) += add(p);
mx(2 * p + 1) += add(p);
add(p) = 0;
}
}
void change(int p, int l, int r, int d) {
//cout << p << endl;
if(l <= l(p) && r >= r(p)) {
add(p) += d;
mx(p) += d;
return;
}
spread(p);
int mid = (l(p) + r(p)) / 2;
if(l <= mid) change(2 * p, l, r, d);
if(r > mid) change(2 * p + 1, l, r, d);
mx(p) = max(mx(2 * p), mx(2 * p + 1));
}
int main() {
cin >> n >> m;
int v[1000005], cnt = 0;
for(int i = 1; i <= n; i++) {
cin >> seg[i].l >> seg[i].r;
seg[i].len = seg[i].r - seg[i].l;
v[++cnt] = seg[i].r;
v[++cnt] = seg[i].l;
}
sort(seg + 1, seg + n + 1, cmp);
sort(v + 1, v + cnt + 1);
int len = unique(v + 1, v + cnt + 1) - v - 1;
for(int i = 1; i <= n; i++) {
seg[i].l = lower_bound(v + 1, v + len + 1, seg[i].l) - v;
seg[i].r = lower_bound(v + 1, v + len + 1, seg[i].r) - v;
}
int pos = 1, ans = 0x3f3f3f3f;
build(1, 1, 1000005);//别忘记建树 由于500000个线段,而线段是两个端点 因此要开1000000
for(int i = 1; i <= n; i++) {
change(1, seg[i].l, seg[i].r, 1);
while(tree[1].mx >= m && pos <= i) {
ans = min(ans, seg[i].len - seg[pos].len);
change(1, seg[pos].l, seg[pos].r, -1);
pos++;
}
}
if(ans != 0x3f3f3f3f) cout << ans;
else cout << -1;
return 0;
}