重构贪心算法(二)
上节课我们初步感受了一下贪心算法,那么我们这一次就来讲述一下常见的贪心模型,今天的主题是区间覆盖
请注意,我所指的贪心模型是一大类,但是在同种大类贪心模型下,还有许多小类。
基本概念
区间覆盖指定一段大区间,再给定多个小区间,每个小区间都有长度,用这些小区间去覆盖大区间,问满足条件时最少需要用到的小区间数量。
活动安排
Q:有n个活动血药使用教室,每个都有一个对应的起始时间和终止时间,最多能举办多少个活动?
我们要举办更多的活动,也就是说当我选择一个活动之后,剩余的时间更多,这样后面的选择也就更多,所以我们要保证我们选择的活动的终止时间最短。那么很明显,所有活动中终止时间最早的那个活动一定要选。
比如左边这三个活动,绿色的活动结束时间最早,那么选择这个活动,剩下的时间最多。
当我们选了第一个终止时间最短的活动后,我们再看终止时间第二早的活动,如果和前一个活动不冲突,就选择这个活动,如果冲突,那么说明这个活动一定不选,所以我们需要根据区间右端点对活动排序。
int last=0,ans=0;
for (int i=1;i<=n;i++){
if (s[i[.r>last){
last=s[i].r;
ans++;
}
}
贪心模型1
使用时机:用这些小区间去覆盖这一段完整的区间,如果区间之间不可以重叠,可以缺失,最多可以选多少个小区间。
解题策略:为了给后面的活动留出更多的空间,这样才能保证后面可以选择的活动更多,所以我们根据区间右端点从小到大排序,每次选择右端点最小的一个区间,如果和前面区间不重叠,就贪心的选择,如果重叠,那么说明这个区间一定不可以选,舍弃。
人员安排
Q:已知一个活动的起止时间,需要安排人员值班,每个人员都有固定的上下班时间(左闭右开),问最少安排多少人才能保证从活动开始到结束都有人值班
该题题意是需要最少的人把整个活动完全覆盖。
首先,我们肯定先选择开始上班时间最早的工作人员,只有这样才能保证从一开始就完全覆盖。所以我们需要按照起点排序。
其次,在当前选择的情况下,我们选择的下一个人是要和我们当前区间相交(相切)的,如果可以选择的人有多个呢?我们要如何取舍呢?
既然是要求人员最少,那么很明显,如果这个满足条件的人工作时间越晚(终止时间越晚),这样我们需要的人就会相对更少。
#include<bits/stdc++.h>
using namespace std;
struct f{
int b,e;
}a[101000];
bool cmp(f a1,f a2){
return a1.b<a2.b;
}
int main(){
int n,m,cnt=0;
cin>>n>>m;
for (int i=0;i<n;i++){
cin>>a[i].b>>a[i].e;
}
sort(a,a+n,cmp);
int last=1;
bool flag=false;
for (int i=0;i<n;i++){
if (last>=m){
flag=true;
cout<<cnt;
return 0;
}
if (a[i].b>last){
cout<<-1;
return 0;
}else {
cnt++;
int j=i,tmp=INT_MIN;
while (a[j].b<=last&&j<n){
tmp=max(tmp,a[j].e);
j++;
}
last=tmp;
}
}
if (flag==false){
cout<<-1;
}else cout<<cnt;
return 0;
}
贪心模型2
使用时机:用这些小区间去覆盖这一段完整的区间,如果区间之间可以重叠,但是不可以缺失,最少需要选多少个小区间。
解题策略:因为要完整覆盖整个区间,那么先根据起点来选择,从头开始覆盖,对于下一次选择的区间,应该是区间有重叠并且终点最大的,这样后面未被覆盖的范围才会越小,用的小区间才会越少,所以先根据区间左端点排序,再从可以选择的区间中选择一个区间终点最大的,其他重叠的就可以舍弃了。
区间覆盖
Q:已知有N个区间,每个区间的范围是[si,ti],请求出区间覆盖后的总长
该题做法有很多种,可以排序贪心,也可以差分,这里主要讲贪心的做法。
当两个区间有重叠时,重叠部分是不计算的,当两个区间中间有空隙时,空隙是不计算的。为了保证空隙和重叠部分都有序,我们把区间根据起点排序,这样我们就能保证每次重叠部分或者空余部分都在上一个区间的末尾。对于当前线段,如果和上一次覆盖的最远距离有重叠,新增覆盖长度为
s
[
i
]
.
r
−
l
a
s
t
s[i].r-last
s[i].r−last。如果和上一次覆盖的最远距离有空隙,新增覆盖长度为
s
[
i
]
.
l
e
n
s[i].len
s[i].len,同时更新覆盖的最远距离。
#include<bits/stdc++.h>
using namespace std;
struct f{
long long b,e;
}a[101000];
bool cmp(f a1,f a2){
return a1.b<a2.b;
}
int main(){
int n;
long long cnt=0;
cin>>n;
for (int i=0;i<n;i++){
cin>>a[i].b>>a[i].e;
}
sort(a,a+n,cmp);
long long last=0;
for (int i=0;i<n;i++){
if (a[i].b<=last&&a[i].e>last){
cnt+=a[i].e-last;
last=a[i].e;
}else if (a[i].b<=last&&a[i].e<=last)continue;
else {
cnt+=a[i].e-a[i].b+1;
last=a[i].e;
}
}
cout<<cnt;
return 0;
}
贪心模型3
使用时机:用小区间去覆盖一个长区间,可以重叠,可以缺失,但是不可以舍去,求覆盖区间的总长
解题策略:为了保证空隙和重叠部分都有序,我们把区间根据起点排序,这样我们就能保证每次重叠部分或者空余部分都在上一个区间的末尾。,然后判断当前线段是否与上一次覆盖的最远距离有重叠。
教室安排
Q:有若干个活动,第i个活动的开始时间和结束时间分别是[Si,Ti)(左闭右开),同一个教室安排的活动之间不能交叠,求要安排所有的活动,最少需要几个教室?
该题不再是求最多可以举办多少活动,而是问如果所有活动都要举行,需要多少教室。
首先,为了方便我们判断时间是否有冲突,先根据终止时间排序。
接下来,来了一个活动,我们要判断是否需要新开一间教室或者在哪一间教室的基础上举办该活动。
(1)新开一间教室
如果当前所有教室的终止时间>该活动的开始时间那么没有任何教室可以举办办活动,只能新开
(2)选择一间教室举办活动
为了让结果最优,假设有多间教室满足条件,我们该如何选取。通过画图我们发现每次加到结束时间最晚的效果最好。
#include <bits/stdc++.h>
using namespace std;
struct node{
long long l;
long long r;
}a[1000100];
bool cmp(node x,node y){
return x.r<y.r;
}
long long b[1000100];
int main(){
long long n,cnt=0;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i].l>>a[i].r;
b[i]=1;
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++){
int k=0;
for(int j=1;j<=cnt;j++){
if(a[i].l>=b[j]&&b[j]>b[k]){
k=j;
}
}
if(k==0){
cnt++;
b[cnt]=a[i].r;
}else{
b[k]=a[i].r;
}
}
cout<<cnt;
return 0;
}
贪心模型4
使用时机:当多个小区间都不能舍去且不能重叠时,让我们求可以最少用多少个轨道(也就是上面的教室)可以办到。
贪心策略:为了方便我们判断时间是否有冲突,先根据终止时间排序。现在我们只需要判断当前区间的开始时间是否比某个轨道的末尾时间晚,如果有,则选择开始时间最晚的,如果没有,则重新开一个轨道。
种树
Q:一条街的一边有几座房子,因为环保原因居民想要在路边种些树。路边的地区被分割成块,并被编号成1,2,…,n。每个部分为一个单位尺寸大小并最多可种一棵树。每个居民都想在门前种些树,并指定了三个号码 b,e,t。这三个数表示该居民想在地区 b 和 e 之间(包括 b 和 e)种至少 t 棵树。居民们想种树的各自区域可以交叉。你的任务是求出能满足所有要求的最少的树的数量。
1、按区间终点从小到大排序 2、先统计区间已经种树的数量,再从末尾向前栽剩下的树。注意已经栽过树的位置要忽略
#include <bits/stdc++.h>
using namespace std;
int vis[1000000];
struct node{
int s,f,tree;
}a[1000000];
bool cmp(node c,node b){
return c.f<b.f;
}
int min(int a,int b){
if(a<=b)return a;
else return b;
}
int main(){
int m,n,flag=0;
cin>>m>>n;
for(int i=0;i<n;i++){
cin>>a[i].s>>a[i].f>>a[i].tree;
}
sort(a,a+n,cmp);
int count;
count=0;
for(int i=0;i<n;i++){
for(int j=a[i].s;j<=a[i].f;j++){
if(!a[i].tree)break;
if(vis[j])a[i].tree--;
}
for(int j=a[i].f;j>=a[i].s;j--){
if(!a[i].tree)break;
if(!vis[j]){
a[i].tree--;
flag++;
vis[j]=1;
}
}
}
cout<<flag;
return 0;
}
贪心模型5
使用时机:区间固定,要求固定,要求覆盖面积最小。
解题策略:因为要求覆盖面积最小,那就应该尽可能把小区间堆在区间的重叠区域,为了方便查找重叠区域,我们尽量保证新加进来一个区间后,重叠部分是有序增加的,如重叠部分在末尾或者开头。
总结
对于区间覆盖这一类问题,一般情况下都是根据起点或者终点来排序,然后每次贪心的选择最优解从而得到最终答案。