【基础算法】贪心 (四) :区间问题
文章目录
- 区间问题
- 1. 线段覆盖 ⭐⭐
- (1) 解题思路
- (2) 代码实现
- 2. Radar Installation ⭐⭐⭐
- (1) 解题思路
- (2) 代码实现
- 3. Sunscreen ⭐⭐⭐
- (1) 解题思路
- (2) 代码实现
- 4. Stall Reservations S ⭐⭐⭐
- (1) 解题思路
- (2) 代码实现
区间问题
区间问题是另一种比较经典的贪心问题。题目面对的对象是一个一个的区间,让我们在每个区间上做出取舍。 这种题目的解决方式一般就是按照区间的左端点或者是右端点排序,然后在排序之后的区间上,根据题目要求,制定出相应的贪心策略,进而得到最优解。 具体是根据左端点还是右端点排序?升序还是降序?一般是假设一种排序方式,并且制定贪心策略去尝试看能不能解决问题, 当没有明显的反例时,就可以尝试去写代码。
1. 线段覆盖 ⭐⭐
【题目链接】
P1803 凌乱的yyy / 线段覆盖 - 洛谷
【题目背景】
快 noip 了,yyy 很紧张!
【题目描述】
现在各大 oj 上有 nnn 个比赛,每个比赛的开始、结束的时间点是知道的。
yyy 认为,参加越多的比赛,noip 就能考的越好(假的)。
所以,他想知道他最多能参加几个比赛。
由于 yyy 是蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加 222 个及以上的比赛。
【输入格式】
第一行是一个整数 nnn,接下来 nnn 行每行是 222 个整数 ai,bi(ai<bi)a_{i},b_{i}\ (a_{i}<b_{i})ai,bi (ai<bi),表示比赛开始、结束的时间。
【输出格式】
一个整数最多参加的比赛数目。
【示例一】
输入
3 0 2 2 4 1 3
输出
2
【说明/提示】
- 对于 20%20\%20% 的数据,n≤10n \le 10n≤10;
- 对于 50%50\%50% 的数据,n≤103n \le 10^3n≤103;
- 对于 70%70\%70% 的数据,n≤105n \le 10^{5}n≤105;
- 对于 100%100\%100% 的数据,1≤n≤1061\le n \le 10^{6}1≤n≤106,0≤ai<bi≤1060 \le a_{i} < b_{i} \le 10^60≤ai<bi≤106。
(1) 解题思路
按照区间左端点从小到大排序,当两个区间重叠的时候,我们必须要舍弃一个。为了能够在移除某个区间后,保留更多的区间,我们应该把区间范围较大的区间移除。 因此以第一个区间为基准,遍历所有的区间:
-
如果重叠,选择最小的右端点作为新的基准;
-
如果不重叠,那么我们就能多选一个区间,以新区间为基准继续向后遍历。
(2) 代码实现
#include<iostream>
#include<algorithm>using namespace std;const int N = 1e6 + 10;struct node
{int l, r;
} a[N]; // 结构体存区间bool cmp(node& x, node& y) // 按区间的左端点进行从小到大排序
{return x.l < y.l;
}int main()
{int n; cin >> n;for(int i = 1; i <= n; i++) cin >> a[i].l >> a[i].r;sort(a + 1, a + n + 1, cmp);int ans = 1;int right_st = a[1].r; // 以第一个区间的右端点为基准for(int i = 2; i <= n; i++){int left = a[i].l, right = a[i].r;if(left < right_st) // 有重叠{right_st = min(right_st, right);}else // 没有重叠{ans++;right_st = right;}}cout << ans;return 0;
}
2. Radar Installation ⭐⭐⭐
【题目链接】
UVA1193 Radar Installation - 洛谷
【原版题目描述(英文)】
p1193.pdf
【题目描述】
假设海岸线是一条无限长的直线,陆地位于海岸线的一边,大海位于海岸线的另一边。大海中有许多小岛。某安全部门为了监视这些岛上是否有敌人入侵,打算在海岸线上安装若干个雷达来检测岛屿的情况。每个雷达的覆盖范围是以雷达中心为圆心,半径为 ddd 的圆形区域。
我们用平面之间坐标系来表示整个区域,海岸线为 xxx 轴,大海位于 xxx 轴上方,陆地位于 xxx 轴下方。为了节约成本,安全部门想使用最少的雷达覆盖所有的岛屿。现在已知每个岛屿的坐标 (x,y)(x,y)(x,y) 和雷达的覆盖半径 ddd,你的任务就是计算出能够覆盖所有岛屿的最少雷达数量。
【输入格式】
输入包含若干组数据。
每组数据的第一行有两个整数 nnn(1 ≤ nnn ≤ 1000) 和 ddd,分别表示岛屿的数量和雷达的覆盖半径,之后的 nnn 行,每行有两个整数,表示第 iii 个岛屿的坐标 (xi,yi)(x_i,y_i)(xi,yi)。
相邻的两组数据使用一个空行隔开。输入 0 0 表示输入结束。
【输出格式】
对于每一组数据的输出格式为
Case i: x
,表示第 i 组数据中最少需要 xxx 个雷达来覆盖所有的岛屿;x=−1x=−1x=−1 表示这组数据无解(无法覆盖所有的岛屿)。
【示例一】
输入
3 2 1 2 -3 1 2 11 2 0 20 0
输出
Case 1: 2 Case 2: 1
(1) 解题思路
如果我们把这道题放在一个二维的坐标系中的话,那就太复杂了,这道题的关键在于把二维转化成一维。
如图,对于每一个岛屿,我们都可以计算出雷达放在哪一个区间上能够覆盖此岛屿。
通过得知 (x,y)(x, y)(x,y) 和 ddd 的信息,我们可以求出雷达能够覆盖的区域 [a, b]
。那么对于每一个岛屿都会有一个对应的区间,原问题就转化成了**给定一些区间,所有互相重叠的区间一共有多少组**。
之后我们按照区间左端点从小到大排序,当两个区间重叠的时候,为了后面能够尽可能多的选出互相重叠的区间,我们应该把区间范围较大的区间移除,因为选择较大区间会造成选出来的区间 不是互相重叠的。 因此以第一个区间为基准,遍历所有的区间:
-
如果重叠,选择最小的右端点作为新的基准;
-
如果不重叠,那么我们就能多选一个区间,以新区间为基准继续向后遍历。
(2) 代码实现
#include<iostream>
#include<algorithm>
#include<cmath>using namespace std;const int N = 1005;struct node
{double l, r;
} a[N];int n, d;bool cmp(node& x, node& y) // 按照左端点排序
{return x.l < y.l;
}int main()
{for(int T = 1; T > 0; T++){bool flag = 0; // 看后续是否能够覆盖所有区间cin >> n >> d;if(n == 0 && d == 0) break;for(int i = 1; i <= n; i++){int x, y;cin >> x >> y;double tmp = d * d - y * y;if(tmp < 0){flag = 1; // 说明不能覆盖所有区间break;}double dir = sqrt(tmp);a[i].l = x - dir; // 区间左端点a[i].r = x + dir; // 区间右端点}if(flag){printf("Case %d: %d\n", T, -1);continue;}sort(a + 1, a + 1 + n, cmp);int ans = 1;double right_st = a[1].r; // 以第一个区间右端点为基准for(int i = 2; i <= n; i++){double left = a[i].l, right = a[i].r;if(left <= right_st) // 有重叠{right_st = min(right_st, right); // 以右端点较小的作为新基准}else // 无重叠{right_st = right;ans++;}}printf("Case %d: %d\n", T, ans);}return 0;
}
3. Sunscreen ⭐⭐⭐
【题目链接】
[P2887 USACO07NOV] Sunscreen G - 洛谷
【题目描述】
有 CCC 头奶牛进行日光浴,第 iii 头奶牛需要 minSPF[i]minSPF[i]minSPF[i] 到 maxSPF[i]maxSPF[i]maxSPF[i] 单位强度之间的阳光。
每头奶牛在日光浴前必须涂防晒霜,防晒霜有 LLL 种,涂上第 iii 种之后,身体接收到的阳光强度就会稳定为 SPF[i]SPF[i]SPF[i],第 iii 种防晒霜有 cover[i]cover[i]cover[i] 瓶。
求最多可以满足多少头奶牛进行日光浴。
【输入格式】
第一行输入整数 CCC 和 LLL。
接下来的 CCC 行,按次序每行输入一头牛的 minSPFminSPFminSPF 和 maxSPFmaxSPFmaxSPF 值,即第 iii 行输入 minSPF[i]minSPF[i]minSPF[i] 和 maxSPF[i]maxSPF[i]maxSPF[i]。
再接下来的 LLL 行,按次序每行输入一种防晒霜的 SPFSPFSPF 和 covercovercover 值,即第 iii 行输入 SPF[i]SPF[i]SPF[i] 和 cover[i]cover[i]cover[i]。
每行的数据之间用空格隔开。
【输出格式】
输出一个整数,代表最多可以满足奶牛日光浴的奶牛数目。
【示例一】
输入
3 2 3 10 2 5 1 5 6 2 4 1
输出
2
【说明/提示】
样例解释:给第一头奶牛涂第一种防晒霜,第二头奶牛涂第二种防晒霜。
对于 100%100\%100% 的数据,1≤C,L≤25001\le C,L\le 25001≤C,L≤2500,1≤minSPF[i]≤maxSPF[i]≤10001\le minSPF[i]\le maxSPF[i]\le 10001≤minSPF[i]≤maxSPF[i]≤1000,1≤SPF[i]≤10001\le SPF[i]\le 10001≤SPF[i]≤1000,1≤cover[i]≤25001\le cover[i]\le 25001≤cover[i]≤2500。
(1) 解题思路
思考具体解法,从下面的情况中,筛选出没有特别明显的反例的组合:
区间按照左端点从小到大 + 防晒霜从小到大(优先选小);
区间按照左端点从小到大 + 防晒霜从大到小(优先选大);
区间按照左端点从大到小 + 防晒霜从小到大(优先选小);
区间按照左端点从大到小 + 防晒霜从大到小(优先选大);
区间按照右端点从小到大 + 防晒霜从小到大(优先选小);
区间按照右端点从小到大 + 防晒霜从大到小(优先选大)。
区间按照右端点从大到小 + 防晒霜从小到大(优先选小);
区间按照右端点从大到小 + 防晒霜从大到小(优先选大)。
虽然看似很多,但是很容易在错误的策略中举出反例。
综上所述,有两种组合没有明显的反例,分别是:
- 区间按照左端点从大到小排序,防晒霜从大到小排序,优先选择较大的防晒霜;
- 区间按照右端点从小到大排序,防晒霜从小到大排序,优先选择较小的防晒霜。
(2) 代码实现
#include<iostream>
#include<algorithm>using namespace std;const int N = 2510;struct cow // 奶牛
{int l, r;
} c[N];struct sunscr // 防晒霜
{int spf, cnt;
} ss[N];// 注:奶牛和防晒霜可以写成一个结构体,用这个结构体定义两个数组bool cmp_cow(cow& x, cow& y) // 按照左端点从大到小排序
{return x.l > y.l;
}bool cmp_sun(sunscr& x, sunscr& y) // 防晒霜从大到小排序
{return x.spf > y.spf;
}int main()
{int C, L;cin >> C >> L;for(int i = 1; i <= C; i++) cin >> c[i].l >> c[i].r;for(int i = 1; i <= L; i++) cin >> ss[i].spf >> ss[i].cnt;sort(c + 1, c + 1 + C, cmp_cow);sort(ss + 1, ss + 1 + L, cmp_sun);int ans = 0;for(int i = 1; i <= C; i++) // 枚举每一个奶牛{int left = c[i].l, right = c[i].r;// 对于每一个奶牛遍历一遍防晒霜(因为数据范围较小,所以不会超时)for(int j = 1; j <= L; j++){int _spf = ss[j].spf;if(ss[j].cnt == 0) continue; // 如果防晒霜没有了,就继续枚举下一个防晒霜if(_spf < left) break; // 如果防晒霜太小了,那么就应该枚举下一头奶牛if(_spf > right) continue; // 如果防晒霜太大了,应该继续枚举防晒霜// 到了这里,说明 left <= _spf <= rightans++; // 把这个防晒霜给这头牛ss[j].cnt--;break;}}cout << ans;return 0;
}
4. Stall Reservations S ⭐⭐⭐
【题目链接】
[P2859 USACO06FEB] Stall Reservations S - 洛谷
【题目描述】
约翰的 NNN(1≤N≤500001\leq N\leq 500001≤N≤50000)头奶牛实在是太难伺候了,她们甚至有自己独特的产奶时段。对于某一头奶牛,她每天的产奶时段是固定的时间段 [A,B][A,B][A,B](即 AAA 到 BBB,包括 AAA 和 BBB)。这使得约翰必须开发一个调控系统来决定每头奶牛应该被安排到哪个牛棚去挤奶,因为奶牛们并不希望在挤奶时被其它奶牛看见。
请帮约翰计算:如果要满足奶牛们的要求,并且每天每头奶牛都要被挤过奶,至少需要多少牛棚和每头牛应该在哪个牛棚被挤奶。如果有多种答案,输出任意一种均可。
【输入格式】
第 111 行,一个整数 NNN。
第 2∼(N+1)2\sim (N+1)2∼(N+1) 行,每行两个数字,第 (i+1)(i+1)(i+1) 行的数字代表第 iii 头奶牛的产奶时段。
【输出格式】
第 111 行输出一个整数,代表需要牛棚的最少数量。
第 2∼(N+1)2\sim (N+1)2∼(N+1) 行,每行一个数字,第 (i+1)(i+1)(i+1) 行的数字代表第 iii 头奶牛将会被安排到哪个牛棚挤奶。
【示例一】
输入
5 1 10 2 4 3 6 5 8 4 7
输出
4 1 2 3 2 4
(1) 解题思路
按照起始时间对所有奶牛从小到大排序,然后从前往后依次安排每一头奶牛,设这头奶牛的产奶的时间区间是 [a, b]
,那么:
-
在已经有牛的所有牛棚里,如果某头牛的产奶结束时间小于
a
,就可以把这头奶牛放在这个牛棚里面;如果有很多符合要求的,可以随便找一个。因为我们是按照起始时间从小到大排序,只要这些牛棚都符合要求,对于后面的奶牛而言也都符合要求。不妨找结束时间最早的,方便判断。如何快速找到结束时间最小的那个牛棚呢?我们可以用一个小根堆来存牛棚,而由于输出格式需要知道牛棚的编号,所以小根堆中的每一个位置应存一个结构体,结构体内存牛棚的结束时间和牛棚编号。
-
如果所有已经有牛的牛棚的结束时间都大于
a
,那么这头牛只能自己单独开一个牛棚。
(2) 代码实现
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>using namespace std;const int N = 5e4 + 10;struct cow // 奶牛
{// 起始时间、终止时间、对应下标int l, r, index;
} c[N];struct stall
{// 牛棚结束时间,牛棚编号int end, index;
};bool cmp_c(cow& x, cow& y) // 按照左端点从小到大排序
{return x.l < y.l;
}struct cmp_s // 小根堆中比较规则:牛棚结束时间小的优先级高
{bool operator()(const stall& x, const stall& y) const{return x.end > y.end;}
};int res[N]; // 存放最终结果int main()
{int n; cin >> n;for(int i = 1; i <= n; i++){cin >> c[i].l >> c[i].r;c[i].index = i;}sort(c + 1, c + 1 + n, cmp_c);priority_queue<stall, vector<stall>, cmp_s> heap;int cnt = 1; // 牛棚数量heap.push({c[1].r, 1}); // 把原本的第一头牛放到第一个牛棚中res[c[1].index] = 1;for(int i = 2; i <= n; i++){int left = c[i].l, right = c[i].r;if(left <= heap.top().end) // 此时不能放到任意一个已经分配过奶牛的牛棚{cnt++; // 新分配一个牛棚heap.push({right, cnt});res[c[i].index] = cnt;}else // 此时可以放在已经分配过奶牛的牛棚里{int t = heap.top().index;heap.pop();heap.push({right, t});res[c[i].index] = t;}}cout << cnt << endl;for(int i = 1; i <= n; i++) cout << res[i] << endl;return 0;
}