区间选点(贪心)
区间选点(贪心)
题目
给定 \(N\) 个闭区间 \([a_i,b_i]\),请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
输入格式
第一行包含整数 \(N\),表示区间数。
接下来 \(N\) 行,每行包含两个整数 \(a_i,b_i\),表示一个区间的两个端点。
输出格式
输出一个整数,表示所需的点的最小数量。
数据范围
\(1 \le N \le 10^5\),
\(-10^9 \le a_i \le b_i \le 10^9\)
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
题解
对于在数轴上离散分布的区间,我们需要建立尽可能少的节点使得每个区间至少都含有一个节点
我们先将区间利用右端点的坐标进行排序,从而可以避免左端点排序产生的小区间被大区间完全包含的问题
排好序后,我们利用第一个区间的右端点作为第一个新建的节点,使用右端点作为节点的原因是尽可能更接近使这个节点更接近后面的区间,从而可以实现后面尽可能多的区间含有该节点,这就是贪心的思想,贪心的想使该节点接触到更多的区间
遍历排好序后的区间,如果下一个区间左端点在当前建立的节点之后,则我们就需要新建节点,也建在当前区间的右端点,并统计节点数量,直到遍历完全部区间
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, res;
struct Range
{
int l, r;
bool operator < (Range &W)const //重载运算符 <
{
return r < W.r;
}
}range[N];
void solve()
{
cin >> n;
for (int i = 0; i < n; i ++ )
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
//先根据右端点 对所有区间进行排序 (这也是我们为什么要重载运算符 < )
sort(range, range + n);
int ed = -2e9; //初始化右端点
for (int i = 0; i < n; i ++ )
{
if (range[i].l > ed) //如果当前区间在上一段区间之后 即没有交集 我们就需要新建一个点
{
ed = range[i].r; //更新右端点
res ++ ;
}
}
cout << res << endl;
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
小变式:最大不相交区间数量
题目
给定 \(N\) 个闭区间 \([a_i,b_i]\),请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。
输出可选取区间的最大数量。
输入格式
第一行包含整数 \(N\),表示区间数。
接下来 \(N\) 行,每行包含两个整数 \(a_i,b_i\),表示一个区间的两个端点。
输出格式
输出一个整数,表示可选取区间的最大数量。
数据范围
\(1 \le N \le 10^5\),
\(-10^9 \le a_i \le b_i \le 10^9\)
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
题解
区间选点问题的变形,在给定的区间内选出最多互不相交的区间,先将区间按照右端点排序,我们也利用建立节点的方式去找有多少不相交的区间,当当前区间的左端点坐标大于所建节点的坐标,我们就利用当前区间的右端点新建节点,遍历所有区间最后记录的节点数量就是最多互不相交区间的数量
代码都没变
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, res;
struct Range
{
int l, r;
bool operator < (const Range &W)const
{
return r < W.r;
}
}range[N];
void solve()
{
cin >> n;
for (int i = 0; i < n; i ++ )
{
int a, b;
cin >> a >> b;
range[i] = {a, b};
}
sort(range, range + n);
int ed = -2e9;
for (int i = 0; i < n; i ++ )
{
if (range[i].l > ed)
{
ed = range[i].r;
res ++ ;
}
}
cout << res << endl;
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
大变式:区间分组
给定 \(N\) 个闭区间 \([a_i,b_i]\),请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
输出最小组数。
输入格式
第一行包含整数 \(N\),表示区间数。
接下来 \(N\) 行,每行包含两个整数 \(a_i,b_i\),表示一个区间的两个端点。
输出格式
输出一个整数,表示最小组数。
数据范围
\(1 \le N \le 10^5\),
\(-10^9 \le a_i \le b_i \le 10^9\)
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
题解
为什么不能按照右端点排序,而要按照左端点排序?
- 我觉得结合一个场景是不是能更好理解。比如,有n个人需要用教室,每个人占用教室的起始时间和终止时间是不一样的。
1、如果想知道只有一间教室,能安排下的最多不冲突人数(不是所有的人都有机会,有的会被舍掉)是多少(区间选点和最大不相交问题),那么当然是最先结束的人排在前面,这样后面的人才有更多机会。如果是按左端点排序,那么如果一个人0点开始用,那么肯定他排在最前面,但是如果他自己就占用了24小时,那么只能给他一个人用了,这样就达不到最大的效果。所以按左端点排序。
2、如果想知道这些人都必须安排,没有人被舍弃,至少需要多少个教室能安排下(区间分组问题)。那么肯定是按照开始时间排序,开始时间越早越优先。这样每间教室都能得到最充分的利用。
一个例子
[1, 3], [2, 5], [4, 100], [10, 13] ,利用这个例子给出的区间可以发现将区间利用左端点排序和右端点排序的区别
- 按照左端点排序:[1, 3], [2, 5], [4, 100], [10, 13] 将区间最少分为两组 1组:[1, 3], [4, 100] 2组:[2, 5],[10, 13]
- 而按照右端点排序:[1, 3], [2, 5], [10, 13], [4, 100] 将区间最少分为三组:1组:[1, 3], [10, 13] 2组:[2, 5] 3组:[4, 100]
- 由于排序后遍历的次序不同,进入小根堆的顺序不同,导致求出的最少组数不同
我们每次将当前区间的左端点 与 已经分好组的区间的右端点最小值进行比较,这样能快速判断当前区间能否加入之前的组,要是连右端点最小的组都与当前区间存在交集\((r_{min} >= range[i].l)\),那就只能新开一个组存当前区间
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n;
struct Range
{
int l, r;
bool operator < (const Range &W)const
{
return l < W.l; //依据区间左端点排序
}
}range[N];
void solve()
{
cin >> n;
for (int i = 0; i < n; i ++ )
{
int a, b;
cin >> a >> b;
range[i] = {a, b};
}
sort(range, range + n); //将区间根据左端点进行排序
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i ++ )
{
if (heap.empty() || heap.top() >= range[i].l) //如果堆为空 或者 当前所有组内的最小右端点仍比当前点的左端点大时
heap.push(range[i].r);
else //当前点与最小组没有交集时,将右端点最小组的右端点更新为当前点的右端点,也就是将小根堆的堆顶弹出,将当前右端点放入堆
{
heap.pop();
heap.push(range[i].r);
}
}
cout << heap.size() << endl; //组数就是小根堆内的数个数
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
给定 \(N\) 个闭区间 \([a_i,b_i]\) 以及一个线段区间 \([s,t]\),请你选择尽量少的区间,将指定线段区间完全覆盖。
输出最少区间数,如果无法完全覆盖则输出 \(-1\)。
变式:区间覆盖
输入格式
第一行包含两个整数 \(s\) 和 \(t\),表示给定线段区间的两个端点。
第二行包含整数 \(N\),表示给定区间数。
接下来 \(N\) 行,每行包含两个整数 \(a_i,b_i\),表示一个区间的两个端点。
输出格式
输出一个整数,表示所需最少区间数。
如果无解,则输出 \(-1\)。
数据范围
\(1 \le N \le 10^5\),
\(-10^9 \le a_i \le b_i \le 10^9\),
\(-10^9 \le s \le t \le 10^9\)
输入样例:
1 5
3
-1 3
2 4
3 5
输出样例:
2
题解
- 按照左端点排序
- 设置初始st为题目给的待覆盖区间的左端点,每一次遍历所有区间,找到其左端点能覆盖st的区间,在这些区间中找到右端点最大的区间,将其右端点记录,更新为st
- while循环结束后,此时的j指针指向最后一个满足
range[j].l <= st
的区间的下一个区间,我们让i指针也指向这个区间,因为st已经更新,不用再去重复查找满足之前的st的区间(while循环已经将这些区间内右端点最大值找到了并更新为新的st了),直接从j指针指向的区间开始查找,方便下次循环对更新后的st查找满足要求的区间,这样可以大大降低时间复杂度 - 注意一些特判条件
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, res;
struct Range
{
int l, r;
bool operator < (const Range &W)const //按照左端点排序
{
return l < W.l;
}
}range[N];
void solve()
{
int st, ed;
cin >> st >> ed;
cin >> n;
for (int i = 0; i < n; i ++ )
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
//利用双指针对每一次更新后的st,找到覆盖st的区间里最大的右端点,将其更新为新的st
bool flag = false;
for (int i = 0; i < n; i ++ )
{
int j = i; //快指针j只用遍历i之后的区间
int r = -2e9;
while (j < n && range[j].l <= st)
{
r = max(r, range[j].r);
j ++ ;
}
if (r < st) //如果查找到的满足条件区间的右端点最大值都在当前st的左边,表示覆盖不完区间
{
res = -1;
break;
}
res ++ ; //当前找到的r符合条件 选择的区间数++
if (r >= ed) //如果此时已经找到一个区间的右端点比所需要覆盖的区间右端点更右 标记查找成功直接break
{
flag = true;
break;
}
st = r; //利用找到的符合条件的区间的最右端点更新st
i = j - 1; //i更新为当前选择的这个最优区间,由于上面j指针最后会多加一次,所以要-1,此步之后下一层循环i++ 自动从当前选的区间的下一个区间开始查找了
}
if (flag == false) //查找失败
{
cout << -1 << endl;
return;
}
cout << res << endl;
return;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}