参考文章(侵删):
贪心法求解三种有关区间覆盖问题
贪心算法-最大不相交区间数问题
一. 算法描述
1. 区间完全覆盖问题
- 问题描述:给定一个长度为m的区间,再给出n条线段的起点和终点(注意这里是闭区间),求最少使用多少条线段可以将整个区间完全覆盖。
- 样例:
区间长度8,可选的覆盖线段[2,6],[1,4],[3,6],[3,7],[6,8],[2,4],[3,5]
解题过程:
①将每一个区间按照左端点递增顺序排列,拍完序后为[1,4],[2,4],[2,6],[3,5],[3,6],[3,7],[6,8]
②设置一个变量表示已经覆盖到的区域。再剩下的线段中找出所有左端点小于等于当前已经覆盖到的区域的右端点的线段中,右端点最大的线段在加入,直到已经覆盖全部的区域。
③过程:
假设第一步加入[1,4],那么下一步能够选择的有[2,6],[3,5],[3,6],[3,7],由于7最大,所以下一步选择[3,7],最后一步只能选择[6,8],这个时候刚好达到了8退出,所选区间为3
- 贪心证明:
需要最少的线段进行覆盖,那么选取的线段必然要尽量长,而已经覆盖到的区域之前的地方已经无所谓了,(可以理解成所有的可以覆盖的左端点都是已经覆盖到的地方),那么真正能够使得线段更成的是右端点,左端点没有太大的意义,所以选择右端点来覆盖
2. 最大不相交覆盖问题
- 问题描述: 给定一个长度为m的区间,再给出n条线段的起点和终点(开区间和闭区间处理的方法是不同,这里以开区间为例),问题是从中选取尽量多的线段,使得每个线段都是独立的,就是不和其它有任何线段有相交的地方。
- 样例:
区间长度8,可选的覆盖线段[2,6],[1,4],[3,6],[3,7],[6,8],[2,4],[3,5]
解题过程:
对线段的右端点进行升序排序,每加入一个线段,然后选择后面若干个(也有可能是一个)右端点相同的线段,选择左端点最大的那一条,如果加入以后不会跟之前的线段产生公共部分,那么就加入,否则就继续判断后面的线段
①排序: 将每一个区间按右端点进行递增顺序排列,拍完序后为[1,4],[2,4],[2,6],[3,5],[3,6],[3,7],[6,8]
②第一步选取[2,4],发现后面只能加入[6,8],所以区间的个数为2
- 贪心证明:因为需要尽量多的独立的线段,所以每个线段都尽可能的小,对于同一右端点,左端点越大,线段长度越小。那么为什么要对右端点进行排序呢?如果左端点进行排序,那么右端点是多少并不知道,那么每一条线段都不能对之前所有的线段进行一个总结,那么这就明显不满足贪心的最有字结构了。
3. 最大不相交区间数(很常见)
- 问题描述:数轴上有n个区间[Ai,Bi],要求选择尽量多的区间,并使得这些区间不存在交集。
- 解题思路:贪心策略,按照B1<=B2<=B3……的方式进行排序,然后从前向后遍历区间,每当遇到可以加入集合的区间,就把它加入集合。(集合代表解的集合)
- 证明:我们对A1,A2……的关系分以下几种情况考虑:
1. A1>A2。此时区间2包含区间1。这种情况下显然不会选择区间2,因为选择区间1会留下更多的剩余空间。不仅区间2如此,以后所有区间中只要有一个i满足A1>Ai,i都不要选。即此种情况下,选择区间1是明智的,与策略一致。
2. 排除情况1后,一定有A1<=A2<=A3……。
4. 区间选点问题
- 问题描述:给定一个长度为m的区间,再给出n条线段和这n条线段需要满足的要求(要求是这n条线段上至少有的被选择的点的个数),问题是整个区间内最少选择几个点,使其满足每一条线段的要求.
- 样例:略
解题过程:将每个线段按照终点坐标进行递增排序,相同终点的前点坐标大的在前面,一个个将其满足
- 贪心证明:要想使得剩下的线段上选择的点最少,那么就应该尽量使得已经选择了的点尽量能在后面的线段中发挥作用,而我们是从左往右选择线段的,那么要使得选取的点能满足后面线段的要求,那么必须是从线段的有端点开始选点,那么问题(2)一样涉及到一个问题,如果是按照线段的左端点对线段进行排序的话,不知道右端点的话,每一条线段都不能对之前已经操作过的所有线段进行一个总结,那么这就同样不满足贪心算法的最优子结构性质了。
- 可以解决的实际问题:数轴上面有n个闭区间[a,b],取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)。
二. 相关例题
1. 字节跳动2018秋招题目(看直播)
- 问题描述
①题目描述
小明看直播,想知道最多能完整看几场。
已知: ①他关注了N个主播。②每个主播si开始,ti结束。③同一时间只能看一个。④一天分为M个时间段。
求:他最多能完整看完几场。
(1<=N<10^5,2<=M<=10^6,si>=0)
(当si>ti时候,表示跨天,但一个直播不会超过一天)
②输入输出
输入:
第1行,N
第2行,M
第3行,空格分隔开的N*2个数,代表s t(经过询问工作人员确认这一行st的顺序是这样的:s1 t1 s2 t2……)
输出:
最多完整看完几场
输入例子:
3
10
0 3 3 7 7 0
输出例子:
3
输入例子:
3
10
0 5 2 7 6 9
输出例子:
2
- 原题的测试用例都通过了,但题目中M没用到(除非用在输入时判断输入是否合法,这我就懒得写了)。不知道是否有啥特殊情况我没想到的,或者有更好的解法。
#include<iostream>
#include<vector>
using namespace std;
//测试用例是都通过了,但M没用到,不知道是否有什么特殊值我没想到的
class Solution {
private:
int quicksort(int *l, int *r, int start, int end) {
int start_u = start, end_u = end;
int standard_r = r[start], loc = start;
int standard_l = l[start];
while (start_u != end_u) {
//从右到左找比temp小的
while (r[end_u] >= standard_r) {
end_u--;
if (end_u == start_u)
goto exit;
}
r[loc] = r[end_u];
l[loc] = l[end_u];
r[end_u] = standard_r;
l[end_u] = standard_l;
loc = end_u;
//从左到右找比temp大的
while (r[start_u] <= standard_r) {
start_u++;
if (end_u == start_u)
goto exit;
}
r[loc] = r[start_u];
l[loc] = l[start_u];
r[start_u] = standard_r;
l[start_u] = standard_l;
loc = start_u;
}
exit:
//除非已经不可再排了,否则对左右两部分再进行快排
if (loc != start && loc - 1 != start)
quicksort(l, r, start, loc - 1);
if (loc != end && loc + 1 != end)
quicksort(l, r, loc + 1, end);
return 1;
}
public:
//先按右端点进行排序
void sort_rang(int *l, int *r, int num) {
quicksort(l, r, 0, num - 1);
}
//贪心算法找出最大值
int most(int *l, int *r, int num) {
int ret = 0;
vector<int> special; //用于暂存左边界大于右边界的
int special_ok = 0;
vector<int> l_ok, r_ok;
int last_r = 0;
//正常计算
for (int i = 0; i < num; i++) {
//如果左边界大于右边界
if (l[i] > r[i]) {
special.push_back(l[i]);
special.push_back(r[i]);
}
else {
if (r[i] != last_r) {
if (l[i] >= last_r) {
l_ok.push_back(l[i]);
r_ok.push_back(r[i]);
last_r = r[i];
}
}
}
}
//左边界大于右边界的计算
int min_start = l_ok.at(0);
int max_end = r_ok.at(r_ok.size() - 1);
for (int i = 1; i < special.size(); i = i + 2) {
if (special.at(i) <= min_start) {
if (special.at(i - 1) >= max_end) {
special_ok++;
max_end = special.at(i - 1);
min_start = special.at(i);
}
}
else {
break;
}
}
ret = l_ok.size() + special_ok;
return ret;
}
};
int main() {
int N, M;
int s[1000], t[1000];
int how_many;
Solution test;
while (cin >> N >> M) {
for (int i = 0; i < N; i++) {
cin >> s[i] >> t[i];
}
test.sort_rang(s, t, N);
how_many = test.most(s, t, N);
cout << how_many << endl;
}
getchar();
return 0;
}
2.贝壳找房2018秋招题目(预约教室)
- 问题描述
已知:
① n个社团,同一天申请用同一个教室,第i个社团占用的时间段是[li,ri]。
②必须且最多取消掉1个社团的预约,来满足其他n-1个社团的需求。
③如果有[l1,r1]和[l2,r2],且r1=l2,不算冲突。
求:有多少种取消的方案能满足要求
输入输出:
输入:
第1行是一个整数n。(1=<n<=5000)
后面n行:每行两个整数 li和ri(1=<li,ri<=10^6)
输出:
第1行:m,表示有m种取消的方案
第2行:m个数,表示取消的社团序号(从小到大排列)
例子1:
入:
3
3 10
20 30
1 3
出:
3
1 2 3
例子2:
入:
4
3 10
20 30
1 3
1 39
出:
1
4
例子3:
入:
3
1 5
2 6
3 7
出:
0
- 这题其实思路上没有那么难,因为只有三种情况:
①要不没有重叠的,那么全部输出一次就可以了。
②去掉两个以上才满足的,输出0,这种情况不可能只去一个。
③能找到去的那一个,输出1,并且输出去除的那个。
根据这个思路编程就好:
#include<iostream>
#include<vector>
using namespace std;
class Solution {
private:
int quicksort(int *l, int *r, int start, int end) {
int start_u = start, end_u = end;
int standard_r = r[start], loc = start;
int standard_l = l[start];
while (start_u != end_u) {
//从右到左找比temp小的
while (r[end_u] >= standard_r) {
end_u--;
if (end_u == start_u)
goto exit;
}
r[loc] = r[end_u];
l[loc] = l[end_u];
r[end_u] = standard_r;
l[end_u] = standard_l;
loc = end_u;
//从左到右找比temp大的
while (r[start_u] <= standard_r) {
start_u++;
if (end_u == start_u)
goto exit;
}
r[loc] = r[start_u];
l[loc] = l[start_u];
r[start_u] = standard_r;
l[start_u] = standard_l;
loc = start_u;
}
exit:
//除非已经不可再排了,否则对左右两部分再进行快排
if (loc != start && loc - 1 != start)
quicksort(l, r, start, loc - 1);
if (loc != end && loc + 1 != end)
quicksort(l, r, loc + 1, end);
return 1;
}
public:
//先按右端点进行排序
void sort_rang(int *l, int *r, int num) {
quicksort(l,r, 0, num - 1);
}
//贪心算法找出最大值
int most(int *l, int *r, int num,int& l_rm,int& r_rm) {
int ret=0;
vector<int> ok;
int last_r=0;
for (int i = 0; i < num; i++) {
if (r[i] != last_r) {
if (l[i] >= last_r){
ok.push_back(i);
last_r = r[i];
//cout << i << endl;
}
}
}
if (ok.size() == num)
ret = num;
if (ok.size() + 1 == num) {
ret = 1;
for (int i = 0; i < num; i++) {
if (i > ok.size() - 1) {
l_rm = l[i];
r_rm = r[i];
}
else if (ok.at(i) != i) {
l_rm = l[i];
r_rm = r[i];
}
}
}
if (ok.size() + 1 < num)
ret = 0;
return ret;
}
};
int main(){
int n;
int l[1000000], r[1000000];
int l_c[1000000], r_c[1000000];
int temp_l, temp_r;
int how_many;
Solution test;
while (cin >> n) {
for (int i = 0; i < n; i++) {
cin >> l[i] >> r[i];
l_c[i] = l[i]; r_c[i] = r[i];
}
cout << endl;
test.sort_rang(l, r, n);
how_many = test.most(l, r, n,temp_l,temp_r);
cout << how_many<<endl;
//根据情况选择输出
if (how_many == n) {
for (int i = 1; i <= n; i++)
cout << i << " ";
cout << endl;
}
else if (how_many == 0) {
//Do nothing.
}
else if (how_many == 1) {
for (int i = 0; i < n; i++) {
if (l_c[i] == temp_l && r_c[i] == temp_r) {
cout << i+1 << endl;
break;
}
}
}
}
getchar();
return 0;
}