数据结构与算法学习笔记----贪心区间问题
@@ author: 明月清了个风
@@ first publish time: 2025.4.3
ps⭐️一个月没更了,三月初出去玩了,然后大论文写到现在哈哈哈
贪心问题其实不像其他问题能够有非常具体的解决思路,往往是通过尝试找到一种方法后再证明其正确性,贪心的关键在于关注最优子结构向最优全局解的证明。
Acwing 905. 区间选点
原题链接\]([905. 区间选点 - AcWing题库](https://www.acwing.com/problem/content/907/))
给定 N N N个闭区间 \[ a i , b i \] \[a_i, b_i\] \[ai,bi\],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
#### 输入格式
第一行包含整数 N N N,表示区间数。
接下来 N N N行,每行包含两个整数 a i , b i a_i, b_i ai,bi,表示一个区间的两个端点。
#### 输出格式
输出一个整数,表示所需的点的最小数量。
#### 数据范围
1 ≤ N ≤ 1 0 5 1 \\le N \\le 10\^5 1≤N≤105,
− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10\^9 \\le a_i \\le b_i \\le 10\^9 −109≤ai≤bi≤109
#### 思路
这里先直接给出结论:
1. 将所有区间按右端点从小到大排序。
2. 从前往后依次枚举每个区间,如果当前区间中已经包含点,则不选择;若不包含点,则选择当前区间的右端点。
接下来证明此策略的正确性:
因为我们要选择尽可能少的点,因此目标也就是使**选择的每个点覆盖尽可能多的区间**。
* 首先进行贪心选择的正确性证明,也就是**为什么选择右端点?**
对于第一个区间 \[ a 1 , b 1 \] \[a_1,b_1\] \[a1,b1\]来说,其右端点 b 1 b_1 b1经过排序后为所有区间的右端点中的最小值,那么剩余区间就只有两种情况,一是与 \[ a 1 , b 1 \] \[a_1,b_1\] \[a1,b1\]有交集,也就是 a i \< b 1 a_i \< b_1 ai\ b 1 a_i \> b_1 ai\>b1。那么对于第一种情况,当我们选择了右端点 b 1 b_1 b1后,该区间一定包含 b 1 b_1 b1,所以不用选择点;对于第二种情况,该区间不包含 b 1 b_1 b1,一定会另选点。也就是说,当我们选择了第一个区间的右端点 b 1 b_1 b1后,我们就覆盖了所有左端点 a i \< b 1 a_i \< b_1 ai\
#include
#include
#include
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator < (const Range &W)const
{
return r < W.r;
}
}range[N];
int main()
{
cin >> n;
for(int i = 0; i < n; i ++)
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
int res = 0, ed = -2e9;
for(int i = 0; i < n; i++)
{
if(range[i].l > ed)
{
res ++;
ed = range[i].r;
}
}
cout << res << endl;
return 0;
}
```
### Acwing 908. 最大不相交区间数量
\[原题链接\]([908. 最大不相交区间数量 - AcWing题库](https://www.acwing.com/problem/content/910/))
给定 N N N个闭区间 \[ a i , b i \] \[a_i, b_i\] \[ai,bi\],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。
输出可选取区间的最大数量。
#### 输入格式
第一行包含整数 N N N,表示区间数。
接下来 N N N行,每行包含两个整数 a i , b i a_i, b_i ai,bi,表示一个区间的两个端点。
#### 输出格式
输出一个整数,表示可选取区间的最大数量。
#### 数据范围
1 ≤ N ≤ 1 0 5 1 \\le N \\le 10\^5 1≤N≤105,
− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10\^9 \\le a_i \\le b_i \\le 10\^9 −109≤ai≤bi≤109
#### 思路
直接说结论吧这一题,做法和上一题完全一致,回顾一下上一题的思路其实也能看出来。
同样可以从两个方向来证明,
根据上一题选法,选择的相邻的两个点一定是没有交集的两个区间的点,因此就可以认为我们的答案 a n s ans ans一定是一种可行方案,且 a n s ≥ c n t ans \\ge cnt ans≥cnt。
下一步证明 a n s ≤ c n t ans \\le cnt ans≤cnt,这里可以用反证法,假设我们的答案 a n s ans ans是大于 c n t cnt cnt的,也就意味着可以选择更多的存在于没有交集的区间的点,但是根据我们的选法来看,该选法覆盖了所有的区间,不存在额外的区间中的点,因此得证。
代码和上一题一样。
#### 代码
```cpp
#include
#include
#include
#include
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator < (const Range &W)const
{
return r < W.r;
}
}range[N];
int main()
{
cin >> n;
for(int i = 0; i < n; i ++)
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
int res = 0, ed = -2e9;
for(int i = 0; i < n; i++)
{
if(range[i].l > ed)
{
res ++;
ed = range[i].r;
}
}
cout << res << endl;
return 0;
}
```
### Acwing 906. 区间分组
\[原题链接\]([906. 区间分组 - AcWing题库](https://www.acwing.com/problem/content/908/))
给定 N N N个闭区间 \[ a i , b i \] \[a_i, b_i\] \[ai,bi\],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
输出最小组数。
#### 输入格式
第一行包含整数 N N N,表示区间数。
接下来 N N N行,每行包含两个整数 a i , b i a_i, b_i ai,bi,表示一个区间的两个端点。
#### 输出格式
输出一个整数,表示最小组数。
#### 数据范围
1 ≤ N ≤ 1 0 5 1 \\le N \\le 10\^5 1≤N≤105,
− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10\^9 \\le a_i \\le b_i \\le 10\^9 −109≤ai≤bi≤109
#### 思路
同样直接给出思路,因为是经典的模型。
1. 将所有区间按左端点从小到大排序
2. 从前往后处理每个区间,判断能否放在某个组里,也就是判断当前区间的左端点是否大于该组中的最大的右端点
3. 如果上一步中未能找到一个组,那么就开一个新的组。
下一步就是证明该做法的正确性,首先证明我们的算法得到的 c n t cnt cnt是否大于等于正确答案 a n s ans ans,因为选取的规则保证了能够按照题意得到结果,因此肯定是一种合法方案,因此 c n t ≥ a n s cnt \\ge ans cnt≥ans肯定成立
而对于 c n t ≤ a n s cnt \\le ans cnt≤ans可以从特殊时间节点来看,也就是创建第 c n t cnt cnt个组的时刻,当我们新开第 c n t cnt cnt个组时,根据选法来看,保证了前 c n t − 1 cnt - 1 cnt−1个组的最大右端点值一定大于当前这个区间的右端点,因此无法放入任何一个组,进而新开了第 c n t cnt cnt个组,这也意味着这 c n t cnt cnt个组的第一个区间一定是相互之间有交集的,也就是新开到第 c n t cnt cnt个组的这个区间的左端点一定被包含在了前 c n t cnt cnt个组里,因此得证。
还有一个问题就是,如果存在多个能够放入的组应该放在哪一个,会不会影响最终的结果,答案是不会,任取一个就行,体现在代码中就是选最小的组。这个问题的核心就是存在多个可用组的情况下,为什么选择任意一个得到的答案都是最小的,这是因为答案其实就是所有区间的最大重叠数,也就是最多的存在同一点的区间数。假设这个数是 k k k,那么说明有 k k k个区间存在相交点,那么就至少需要 k k k组才能容纳这 k k k个组,而剩余区间的重叠数一定小于等于 k k k,因此可以分别分到这 k k k个组中。
#### 代码
```cpp
#include
#include
#include
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 = 0; i < n; i ++)
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
priority_queue, greater> heap;
for(int i = 0; i < n; i ++)
{
auto r = range[i];
if(heap.empty() || heap.top() >= r.l) heap.push(r.r);
else
{
int t = heap.top();
heap.pop();
heap.push(r.r);
}
}
printf("%d\n", heap.size());
return 0;
}
```
### Acwing 907. 区间覆盖
\[原题链接\]([907. 区间覆盖 - AcWing题库](https://www.acwing.com/problem/content/909/))
给定 N N N个闭区间 \[ a i , b i \] \[a_i, b_i\] \[ai,bi\]以及一个区间 \[ s , t \] \[s,t\] \[s,t\],请你选择尽量少的区间,将指定区间完全覆盖。
输出最少区间数,如果无法完全覆盖则输出 − 1 -1 −1。
#### 输入格式
第一行包含两个整数 s s s和 t t t,表示给定区间的两个端点。
第二行包含整数 N N N,表示给定区间数。
接下来 N N N行,每行包含两个整数 a i , b i a_i, b_i ai,bi,表示一个区间的两个端点。
#### 输出格式
输出一个整数,表示所需最少区间数。
如果无解,则输出 − 1 -1 −1。
#### 数据范围
1 ≤ N ≤ 1 0 5 1 \\le N \\le 10\^5 1≤N≤105,
− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10\^9 \\le a_i \\le b_i \\le 10\^9 −109≤ai≤bi≤109,
− 1 0 9 ≤ s ≤ t ≤ 1 0 9 -10\^9 \\le s \\le t \\le 10\^9 −109≤s≤t≤109
#### 思路
同样地给出思路:
1. 将所有区间按左端点从小到大排序
2. 从前往后枚举每个区间,在所有能覆盖 s t st st的区间中选择右端点最大的区间,然后将 s t st st更新为这个值
这个思路很容易证明是正确的,首先选法一定是合法的,并且保证了每一步的覆盖范围最大,且当前步骤的选取不会影响后续的选择,因为已经排过序了。
#### 代码
```cpp
#include
#include
#include
#include
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 ;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
int res = 0;
bool success = false;
for(int i = 0; i < n; i ++)
{
int j = i, r = -2e9;
while(j < n && range[j].l <= st)
{
r = max(r, range[j].r);
j ++;
}
if(r < st)
{
res = -1;
break;
}
res ++;
if(r >= ed)
{
success = true;
break;
}
st = r;
i = j - 1;
}
if(!success) res = -1;
cout << res << endl;
return 0;
}
```