目录
905.区间选点
题目:
给定N个闭区间[ai, bi], 请你在数轴上选择尽量少的点, 使得每个区间内至少包含一个选出的点.
输出选择的点的最小数量
位于区间端点上的点也算作区间内.
输入格式
第一行包含整数N, 表示区间数
接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点
输出格式
输出一个整数, 表示所需的点的最小数量.
数据范围
1 <= N <= 105
-109 <= ai <= bi <= 109
输入样例
3 -1 1 2 4 3 5
输出样例
2
思路:
贪心的规律就是: 每次只看眼前的选择, 选择一个局部最优解, 最后再得到全局的最优解
-
本题将每个区间按右端点从小到大排序.
-
从前往后依次枚举每个区间, 分情况讨论:
如果当前区间中已经包含点, 则直接pass
否则, 选择当前区间的右端点
最优解是当前所有方案里的最小值.
代码:
代码来自acwing y总, 加上了注释, 方便复习
#include <iostream>
#include <algorithm>
//贪心-区间选点
using namespace std;
const int N = 100010; // 最多有10w个区间
int n;
struct Range//定义一个结构体,表示一个区间
{
int l, r;//每个区间的左端点和右端点
//bool operator<(const node &x) const {} 是一个结构内嵌比较函数, {}内进行运算符"<"的重载
bool operator<(const Range &W) const//由于要排序, 重载一下小于号
{
return r < W.r;//重载小于号<, r < W.r表示按照右端点r从小到大排序
}
} range[N]; //定义的结构体就是range[N]
int main() {
scanf("%d", &n);//输入区间的个数
// 读入n个区间的端点
for (int i = 0; i < n; i++) {
int l, r;//读取的左右端点
scanf("%d%d", &l, &r);//读取输入
range[i] = {l, r};//读取左右端点, 放到定义过的结构体
//也可以直接写: scanf("%d%d", &range[i].l, &range[i].r);
// range[i].l表示 输入的第i+1个区间的左端点,range[i].r表示右端点
}
// 给所有区间进行排序
sort(range, range + n);
// res表示当前选择的点的数量,ed表示上一个点的下标
int res = 0, ed = -2e9;//一开始一个点都没选, 所以ed先赋成负无穷
//枚举所有的区间
for (int i = 0; i < n; i++)
if (range[i].l > ed) // 如果当前区间的左端点 严格>ed(上一个点)
{
res++;//点数更新
ed = range[i].r;//当前选择点的下标更新成当前区间的右端点
}
printf("%d\n", res);//最后把结果返回
return 0;
}
代码中的结构内嵌比较函数 bool operator<(const node &x) const {}, 我也是这里头一次见, 可能说的有不清楚的, 引一个讲的比较清楚的链接:
908.最大不相交区间数量
题目:
给定N个闭区间[ai, bi], 请你在数轴上选择若干区间, 使得选中的区间之间互不相交(包括端点).
输出可选取区间的最大数量.
输入格式
第一行包含整数N, 表示区间数,
接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点
输出格式
输出一个整数, 表示可选取区间的最大数量.
数据范围
1 <= N <= 105
-109 <= ai <= bi <= 109
输入样例
3 -1 1 2 4 3 5
输出样例
2
思路:
这个区间不相交的问题也可以对应实际问题中的时间, 比如选时间不重合的课.
-
将每个区间按右端点从小到大排序.
-
从前往后依次枚举每个区间, 分情况讨论:
如果当前区间中已经包含点, 则直接pass
否则, 选择当前区间的右端点
最后累加的端点数量之和就是最大的不相交的区间的数量.
证明:
这个结果是可以证明的: 假设最优解是ans个不相交区间, 以上思路选出区间数为cnt个, 证明ans = count:
1. 证明ans>=count
按照上面选出的cnt个区间是没有交集的, 顺序的, 一种可行方案
而答案是所有可行方案里的最大值, 所以最优解ans >= count
2. 证明ans<=count
按照以上思路同时会选择出来count个点, 每个区间都至少包含一个选择的点,
假设 ans> count, 那么可以选择出来比count更多个不相交的区间, 最多有 ans
个两两不交的区间,至少需要 ans
个点才能将这些区间全部覆盖,
然而实际上只需要 cnt
个点就能覆盖全部区间。则假设不成立, ans <= cnt
3. 证得 ans = count
为什么最大不相交区间数==最少覆盖区间点数呢?
(这一问来自:[区间问题] 最大不相交区间数量(区间问题+贪心)_Ypuyu的博客)
-
因为如果几个区间能被同一个点覆盖,说明他们相交了
-
所以有几个点就是有几个不相交区间。两个问题的本质是等价的
代码:
来自acwing 我添加了注释
#include <iostream>
#include <algorithm>
//贪心-908.最大不相交区间数量
using namespace std;
const int N = 100010; // 最多有10w个区间
int n;
struct Range//定义一个结构体,表示一个区间
{
int l, r;//每个区间的左端点和右端点
//bool operator<(const node &x) const {} 是一个结构内嵌比较函数, {}内进行运算符"<"的重载
bool operator<(const Range &W) const//由于要排序, 重载一下小于号
{
return r < W.r;//重载小于号<, r < W.r表示按照右端点r从小到大排序
}
} range[N]; //定义的结构体就是range[N]
int main()
{
scanf("%d", &n);//输入区间的个数
// 读入n个区间的端点
for (int i = 0; i < n; i++) {
int l, r;//读取的左右端点
scanf("%d%d", &l, &r);//读取输入
range[i] = {l, r};//读取左右端点, 放到定义过的结构体
//也可以直接写: scanf("%d%d", &range[i].l, &range[i].r);
// range[i].l表示 输入的第i+1个区间的左端点,range[i].r表示右端点
}
// 给所有区间进行排序
sort(range, range + n);
// res表示当前选择的点的数量,ed表示上一个点的下标
int res = 0, ed = -2e9;//一开始一个点都没选, 所以ed先赋成负无穷
//枚举所有的区间
for (int i = 0; i < n; i++)
if (range[i].l > ed) // 如果当前区间的左端点 严格>ed(上一个点)
{
res++;//点数更新
ed = range[i].r;//当前选择点的下标更新成当前区间的右端点
}
printf("%d\n", res);//最后把结果返回
return 0;
}
906.区间分组
题目:
给定N个闭区间[ai, bi], 请你将这些区间分成若干组, 使得每组内部的区间两两之间(包括端点)没有交集, 并使得组数尽可能小.
输出最小组数.
输入格式
第一行包含整数N, 表示区间数,
接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点
输出格式
输出一个整数, 表示最小组数.
数据范围
1 <= N <= 105
-109 <= ai <= bi <= 109
输入样例
3 -1 1 2 4 3 5
输出样例
2
思路:
看样例, [-1,1] [2,4]一组, [3,5]一组, 结果是2组
-
将所有区间按左端点从小到大排序
-
从前往后处理每个区间, 判断:
判断能否将其放到某个现有的组中:
比较 L[i] (新的区间的左端点) 和 Max_r (存的某一组的右端点的最大值)
-
如果L[i] > Max_r, 说明不存在这样的组可以放入, 则开新组, 然后将其放进去
-
如果L[i] <= Max_r, 说明存在这样的组可以放入, 区间和原有的组是有交集的, 将其放到这个组, 并更新当前组的Max_r (如果有多个组满足条件, 就随便挑一个放进去, 但事实上应该不可能, 如果多个组满足条件, 说明那两个组应该合并)
-
证明:
ans是结果, 即所有cnt中的最小组数, 所有合法方案中的最小值
cnt是按照以上的算法得到的组的数量, 也就是其中一种合法的方案。
-
证明ans <= cnt
因为cnt是一种合法的方案的组数, ans是所有合法方案中的最小值, 所以ans <= cnt成立
-
证明ans >= cnt
举例子: 一个特殊时刻, 比如我们开第cnt个组时, 需要先看前面的cnt-1个组, 发现当前区间和前面的cnt-1个组都有交集, 因此我们需要新开一个组, 将当前区间放进去, 新开的组就是第cnt个组.
设置当前区间的左端点是L[i], 因为当前区间和前cnt-1个组都有交集, 有L[i]点相交, 因此算上当前区间, 就可以找到cnt个区间包含L[i]点.
那么这cnt个区间有公共点L[i], 无论怎么分, 这cnt个区间都不会分到一组中, 因此所有可行方案组的数量是>= cnt, ans >= cnt 成立
-
证得 ans == cnt
这道题对于数据结构上的选择也要考虑,用最小堆,也就是优先队列来存储每一个组的最右端点。
代码
我加了下注释~
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n;
struct Range {//定义一个结构体,表示一个区间
int l, r;//每个区间的左端点和右端点
//重载小于号
bool operator<(const Range &W) const {
return l < W.l;//按照左端点从小到大排序
}
} range[N];
int main() {
scanf("%d", &n);//输入区间的个数
for (int i = 1; i < n; i++)//读取n个区间的左右端点
{
int l, r;
scanf("%d%d", &l, &r);//读取左右端点
range[i] = {l, r};//赋值给range区间
}
// 给n个区间进行排序
sort(range, range + n);
// 使用小根堆维护所有组的右端点的最大值 Max_r,
priority_queue<int, vector<int>, greater<int> > heap;
// 依次处理每一个区间
for (int i = 0; i < n; i++)
{
auto r = range[i];//取出区间, 用r表示
// 如果小根堆为空(一个组都没有), 或者最小的右端点(堆顶)仍然比当前区间的左端点大(没任何交点)
if (heap.empty() || heap.top() >= r.l) {//空代表没有组
// 创建新组, 区间右端点就是新组的Max_r, 存入
heap.push(r.r);
} else {
//否则的话, 当前区间可以加入这一组, 当前区间的r就成为这一组最右端点,更新
heap.pop();//删掉原最小值
heap.push(r.r);//
}
}
printf("%d\n", heap.size());//堆里的端点个数就是组数
return 0;
}
907.区间覆盖
题目:
给定N个闭区间[ai, bi], 以及一个线段区间[s,t], 请你选择尽量少的区间, 将指定线段区间完全覆盖.
输出最小区间数, 如果无法完全覆盖则输出-1.
输入格式
第一行包含两个整数s和t, 表示给定线段区间的两个端点.
第二行包含整数N, 表示给定区间数.
接下来N行, 每行包含两个整数ai, bi, 表示一个区间的两个端点.
输出格式
输出一个整数, 表示所需最少区间数
如果无解, 则输出-1
数据范围
1 <= N <= 105
-109 <= ai <= bi <= 109
-109 <= s <= t <= 109
输入样例
1 5 //目标区间 [s,t] 3 //区间数3 -1 3 //区间端点 2 4 3 5
输出样例
2
思路:
思路来自acwing~
-
先将所有区间按照左端点从小到大排序 struct Range
-
从前往后依次枚举每个区间, 选择能够覆盖左端点 s 的区间中, 右端点最大的一个
然后将 s 更新成右端点的最大值, 继续向后
证明做法的正确性:
证明:
ans是最后的结果, 即最优解, cnt是记录的一组合理的区间的数量
-
证明Ans <= cnt
ans是最优解, 是所有可行解中最小的, cnt是一种可行解, 所以ans <= cnt 成立
-
证明ans >= cnt
反证法, 假设存在一个方案 ans < cnt, 那么对比两个方案找到第一个选择不同的区间, 说明cnt方案的某个选中区间是比ans的同个左端点的选定区间是短的, 说明不存在, 因为都是选一个右端点长的区间. 所以不成立, ans >= cnt.
-
以上可证得ans = cnt
或者也可以说: cnt可以逐个区间去替换为ans内的区间, 直接证得ans = cnt
(但我感觉这里不需要任何证明的, 按照思路求得的结果本来就是最小值)
代码:
代码来自acwing y总, 和博客:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range {
int l, r;
bool operator<(const Range &W) const {
return l < W.l;
}
} range[N];
int main() {
int st, ed;//目标区间的起止点
scanf("%d%d", &st, &ed);//输入目标区间端点
scanf("%d", &n);//输入区间数量
for (int i = 0; i < n; i++)//枚举每个区间
{
int l, r;
scanf("%d%d", &l, &r);//输入区间的左右端点
range[i] = {l, r};//赋给range
}
//给所有区间排序
sort(range, range + n);
//res记录选择的区间数量
int res = 0;
//success标记是否成功得到一个方案
bool success = false;
//从前往后依次考虑每个区间 一共n个区间
for (int i = 0; i < n; i++) {
//双指针算法扫描一边
int j = i, r = -2e9;//r记录当前start的最大右端点
//用j扫描 比较,选择右端点更大的区间
while (j < n && range[j].l <= st)//range[j].l <= st 表示起始点在st左边的线段
{
r = max(r, range[j].r);//选更靠右的
j++;//继续向后, 直到扫描全部
}
// 如果最大右端点小于st, 所有区间都在目标区间的左侧, 则无解
if (r <= st) {
res = -1;
break;
}
//每次跳出一次while循环, 代表选中了一个区间, 就 res++
res++;
//如果这个选中的区间的右端点直接比目标区间还靠右, 直接全部覆盖目标区间了, 就完成了
if (r >= ed) {
success = true;
break;
}
// 更新st, 每次选中一个区间, 把目标区间新起点更新为上一个选中的区间的右端点
st = r;
i = j - 1;//更新i, 因为j指针扫描的都是原起始点左边的, 之后换了起始点就不必再扫描了直接跳过
}
//判断有没有成功找到方案
if (!success) res = -1;
printf("%d\n", res);//输出结果
return 0;
}