1. 区间选点
给定 𝑁 个闭区间 [𝑎𝑖,𝑏𝑖],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
输入格式
第一行包含整数 𝑁,表示区间数。
接下来 𝑁 行,每行包含两个整数𝑎𝑖,𝑏𝑖,表示一个区间的两个端点。
输出格式
输出一个整数,表示所需的点的最小数量。
数据范围
1≤𝑁≤105,
−109≤𝑎𝑖≤𝑏𝑖≤109
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
题意解读
数轴上有一些区间,在数轴上选取几个点,要求每个区间上最少有一个点。
题解
可以使用贪心解决。
-
将区间按右端点排序
-
遍历区间,如果该区间中不包含最后选的那个点,则选取区间右端点。如果包含最后选的那个点,则跳过。
-
输出所选点的个数。
证明
假设最优解为 ans 个点,贪心算法求出的为 cnt 个点。 只需要证明 ans == cnt 即可。
因为 ans 是最优解,所以 ans <= cnt
。
贪心算法求出的结果为 cnt
,每次让选取点数+1的区间一定不相交。共计cnt个这样的区间。,为了覆盖这cnt
个区间, 至少需要cnt
个点。所以ans >= cnt
。
综上: cnt == ans
代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
//保存区间
vector<vector<int>> a(N,vector<int>(2,0));
int n;
int main()
{
cin >> n;
//读入区间
for(int i = 0; i< n; i++)
{
int l, r;
cin >> l >> r;
a[i][0] = l;
a[i][1] = r;
}
// 按右端点排序
sort(a.begin(), a.begin() + n, [](vector<int> &a, vector<int> &b){return a[1] < b[1];});
// res 保存答案,end 是当前选的点
int res = 0, end = -1e9 - 10;
// 遍历区间
for(int i = 0; i < n; i++)
{
// 如果当前选的点覆盖了该区间,则跳过
if(end >= a[i][0] && end <= a[i][1])
continue;
else
{
// 选的点+1, 选的点更新为区间右端点
res++;
end = a[i][1];
}
}
cout << res;
return 0;
}
2. 最大不相交区间数量
给定 𝑁 个闭区间[𝑎𝑖,𝑏𝑖],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。
输出可选取区间的最大数量。
输入格式
第一行包含整数 𝑁,表示区间数。
接下来 𝑁 行,每行包含两个整数 𝑎𝑖,𝑏𝑖,表示一个区间的两个端点。
输出格式
输出一个整数,表示可选取区间的最大数量。
数据范围
1≤𝑁≤105,
−109≤𝑎𝑖≤𝑏𝑖≤109
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
题意解读
数轴上有一些区间,选取几个区间,要求所选的区间没有重合部分,求最多能选多个区间。
题解
可以使用贪心解决。
-
将区间按右端点排序
-
遍历区间,如果该区间和上一个选的区间有重合,则跳过。如果和上一个选的区间没有重合,则选取该区间。
-
输出所选区间的个数。
证明
假设最优解为 ans 个区间,贪心算法求出的为 cnt
个区间。 只需要证明 ans == cnt
即可。
该贪心算法求出的结果一定是符合区间两两不相交要求的,因此 ans >= cnt
。
没有被选中的区间,一定是被某个选中区间pass掉的,把选中的某个区间和被该区间pass掉的所有区间看做一个集合,则共有cnt
个集合。
每个集合中的区间,被pass掉的区间的右端点一定大于该集合中选中区间的右端点,因为是按照右端点排序后遍历的,所有选中区间只能pass掉右端点比他大的区间。
每个集合中的区间,被pass掉的区间的左端点一定小于该集合中选中区间的右端点,因为如果某个区间的左端点大于选中区间的右端点,则给区间一定不会被pass掉。(pass掉的条件是该区间左端点小于选中区间的右端点)
综合上面两个条件:每个集合中的区间一定两两相交。
假设ans > cnt
,根据抽屉原理,一定有某个集合中被选中了1个以上的区间。又因为同一集合中的区间两两相交,因此,如果ans > cnt
,则必定有区间是相交的,和题意矛盾,因此 ans 不能大于 cnt
。
综上:ans >= cnt
且 ans 不能大于 cnt
,所以ans == cnt
。
综上: cnt == ans
代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
//保存区间
vector<vector<int>> a(N,vector<int>(2,0));
int n;
int main()
{
cin >> n;
//读入区间
for(int i = 0; i< n; i++)
{
int l, r;
cin >> l >> r;
a[i][0] = l;
a[i][1] = r;
}
// 按右端点排序
sort(a.begin(), a.begin() + n, [](vector<int> &a, vector<int> &b){return a[1] < b[1];});
// res 保存答案,end 最后一个选中区间的右端点
int res = 0, end = -1e9 - 10;
// 遍历区间
for(int i = 0; i < n; i++)
{
// 如果当前当前区间和最后一个选中区间有重合,则跳过
if(end >= a[i][0] && end <= a[i][1])
continue;
else
{
// 选中区间数量+1, 更右端点
res++;
end = a[i][1];
}
}
cout << res;
return 0;
}
3. 区间分组
给定 𝑁 个闭区间[𝑎𝑖,𝑏𝑖],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
输出最小组数。
输入格式
第一行包含整数 𝑁,表示区间数。
接下来 𝑁 行,每行包含两个整数 𝑎𝑖,𝑏𝑖,表示一个区间的两个端点。
输出格式
输出一个整数,表示最小组数。
数据范围
1≤𝑁≤105,
−109≤𝑎𝑖≤𝑏𝑖≤109
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
题意解读
数轴上有一些区间,要求将区间分成若干集合,每个集合中的区间两两不重叠。问:最少需要多少个这样的集合。
解题思路
可以使用贪心算法来解决。
-
将区间按左端点排序。
-
依次遍历区间,如果当前区间能放到之前的某个集合中,则把该区间放到该集合,如果当前不能放到任意一个之前的集合中,则新开一个集合,把当前区间放到新开的集合中。
-
集合的数量就是答案。
关键步骤是第二步,如何判断当前区间能否放到之前的集合中。解决方法如下:
-
记录每个集合中保存的区间的最右侧端点,如果当前区间的左端点不和某个集合中保存的区间的最右侧端点相交,则当前区间不和该集合相交,能放到该集合中。
-
也就是,我们只需判断当前区间的左端点 是否和 右侧端点最小的那个集合是否相交即可。
-
为了快速找出右侧端点最小的那个集合,可以使用小根堆保存每个集合的右端点。
证明
设最优解为 ans
, 算法解为 cnt
-
我们的方法找到的集合,各个集合中的区间,两两肯定不相交,因此
cnt >= ans
-
按照该算法,各个集合中的最后一个区间一定是两两相交的。如果存在不相交的区间,则这两个区间会被放到同一个集合中。
-
集合的数量,一定是当遍历到某个区间的时候,不能把当前区间放到任意一个集合中,导致了集合数量由
cnt - 1
变为cnt
。也就是当前区间一定各个集合的最后一个区间有重叠部分。 -
综合2 3, 各个集合的最后一个区间两两相交,当前遍历到的区间和各个集合的最后一个区间都相交,因此,当前遍历的区间以及各个集合的最后一个区间两两相交。对于集合数量由cnt - 1 变为 cnt 的时候,一定有 cnt 个区间两两相交。
-
为了将这
cnt
个区间互不相交,至少需要cnt
个集合,因此cnt <= ans
-
有 1 得出
cnt >= ans
,由5
得出cnt <= ans
, 所以cnt == ans
。
代码
#include <iostream>
#include <queue>
#include <algorithm>
#include <vector>
using namespace std;
// 保存各个区间
vector<vector<int>> a(100010, vector<int>(2, 0));
int n;
int main()
{
cin >> n;
// 处理输入
for(int i = 0; i < n; i++)
{
int l, r;
cin >> l >> r;
a[i][0] = l, a[i][1] = r;
}
//按左端点排序
sort(a.begin(),a.begin() + n);
//小根堆,保存所有集合的右端点,它的大小就是集合的个数
priority_queue<int, vector<int>, greater<int>> s;
//遍历区间
for(int i = 0; i < n; i++)
{
// 当前区间不能放到现有集合中
if(s.empty() || s.top() >= a[i][0])
{
// 新开一个集合,并将右端点放入
s.push(a[i][1]);
}
// 当前区间能放到现有集合
else
{
//更新放入集合的右端点
s.pop();
s.push(a[i][1]);
}
}
//小根堆,保存所有集合的右端点,它的大小就是集合的个数
cout << s.size() << endl;
return 0;
}