区间贪心¶
固定长度区间选最多的不重叠区间个数¶
n个区间,选择尽量多个区间,使得这些区间两两不相交。(实际上就是抽象版的区间调度问题)
核心思路:
右端点排序(<),再从左到右遇到不相交的就选
核心代码:
vector<pair<int, int>> job(n);
int res = 0, t = 0;
sort(job.begin(), job.end());
for (int i = 0; i < n; ++i) {
if (t < job[i].second) {
++res;
t = job[i].first;
}
}
cout << res << endl;
在构建pair
的时候使用了一个小技巧,因为目标是让先结束的任务排在前面,所以键选择结束时间,而值选择任务开始时间。用变量t
来代表上一个选取的任务的结束时间,如果遍历数组job
,发现下一个对象的开始时间大于t
,则说明这个任务可选,并且一定是后面所有可选任务里面最先结束的(因为之前排序的原因)
典型题目:
- 《挑战程序设计竞赛》2.2.2 区间问题 => 固定长度区间选最多的不重叠区间个数
选点覆盖坐标值¶
n个坐标值,每个点左右可以覆盖一定的范围,选择尽量少的点覆盖所有的坐标值。这种题目也可能伪装成区间覆盖问题,比如:
设x1 , x2,... , xn是实直线上的n个点。用固定长度的闭区间覆盖这n个点,至少需要多少个这样的固定长度闭区间?设计解此问题的有效算法,并证明算法的正确性。
核心思路:
对于输入的坐标值序列先排序,然后找第一个没有被标记的点,以此开始,去寻找最后一个小于当前坐标+覆盖范围
的点,这个点作为标记点,然后找下一个不在覆盖范围内的点,作为新的起点,循环直到序列结束。
核心代码:
sort(sequence.begin(), sequence.begin() + n);
int mark()
{
int cnt = 0;
int pos = 0;
while (true) {
int target = sequence[pos] + range; //下面去寻找最后一个不大于目标值的数
while (pos < n && sequence[pos] <= target) {++pos;}
++cnt;
if (pos >= n) break;
pos = pos - 1;
target = sequence[pos] + range; //寻找第一个大于目标值的数
while (pos < n && sequence[pos] <= target) {++pos;}
if (pos >= n) break;
}
return cnt;
}
典型题目:
- POJ 3069 Saruman's Army
最少区间覆盖¶
数轴上有n个闭区间[ai,bi],选择尽量少的区间覆盖一条指定的线段[s,t]
左端点排序(<)兼顾右端点(<),每次选择从当前起点能覆盖到的最长的区间
//POJ 2376 Cleaning Shifts
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int INF = 0x0ffffff;
struct Node
{
int left, right;
bool operator<(const Node & obj) const
{
return left < obj.left;
}
};
int n = 25000, t;
vector<Node> sequence(n);
int cowNum()
{
sort(sequence.begin(), sequence.begin() + n);
int cnt = 0; //计数需要多少头牛
//pos是下一轮搜索的下标,end是上一轮的最右边界,start是辅助去搜索下一个点
int pos = 0, start = 0, end = 0;
while (pos < n) {
//之所以+1是因为需要覆盖的是整数值
start = end + 1;
for (int i = pos; i < n; ++i) {
if (sequence[i].left <= start) {
if (sequence[i].right > end) end = sequence[i].right;
if (end >= t) {++cnt; return cnt;}
}
else {
//更新pos
pos = i;
break;
}
}
//没有找到一个合适的区间
if (end < start) return -1;
++cnt;
}
//搜索结束仍没有return,代表全部区间都无法覆盖,所以无解
return -1;
}
int main()
{
std::ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
cin >> n >> t;
for (int i = 0; i < n; ++i) {
cin >> sequence[i].left >> sequence[i].right;
}
cout << cowNum() << endl;
return 0;
}
典型题目:
- POJ 2376 Cleaning Shifts(区间长度固定,用最少重叠区间覆盖)
- POJ 1089
- ECNU 区间覆盖
- HDU 1050 Moving Tables
- HDU 3397
区间选点¶
数轴上有N个闭区间[Ai, Bi]。取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)。
核心算法:
int solve()
{
sort(sequence.begin(), sequence.begin() + n);
int cnt = 1;
double range = sequence[0].right;
for (int i = 1; i < n; ++i) {
if (sequence[i].left <= range) range = min(range, sequence[i].right);
else {
++cnt;
range = sequence[i].right;
}
}
return cnt;
}
典型题目:
- POJ-1328 Radar Installation(贪心,区间选点)
区间划分¶
时间轴上有n个开区间(ai, bi),把这些区间至少划分成多少个集合,使得每个集合中的区间两两没有公共点。因为是开区间,所以(1, 2)和(2,3)可在一个集合中
核心思路:
仍然是读取区间的左右端点,但是排序上会有所不同,左端是升序排列,如果左端点相同,那么右端点按降序排列,这样做的原因是,考虑一个区间,它是否可以加入到某个集合,取决于当前区间的左端点和所有集合的最右端点的大小比较,如果当前区间左端点大于集合的右端点,那么肯定选两者最接近的加入,这样为更多的区间加入留下位置。发现只和集合的右端点有关,那么考虑如何记录集合的最右端点,可以利用一个向量去保存。那么刚才的右端点降序排列就起了作用,只要顺序比较,只要发现一个集合的右端点小于当前区间的左端点,那么就立刻加入,两者之间的差值一定是所有可以加入的集合里面最小的。
//LeetCode 253.Merting Rooms II
class Solution {
public:
int minMeetingRooms(vector<vector<int>>& intervals) {
std::ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
sort(intervals.begin(), intervals.end(), [](vector<int> & a, vector<int> & b){return a[0] < b[0] || (a[0] == b[0] && a[1] < b[1]);});
int cnt = 0;
//使用小根堆
priority_queue<int, vector<int>, greater<int>> pq;
int n = intervals.size();
for (int i = 0; i < n; ++i) {
if (pq.empty()) { //会议室为空
pq.push(intervals[i][1]);
++cnt;
}
else {
int num = pq.top(); pq.pop();
//可以安排进去
if (num <= intervals[i][0]) {
pq.push(intervals[i][1]);
}
else {
++cnt;
pq.push(num);
pq.push(intervals[i][1]);
}
}
}
return cnt;
}
};
典型例题:
- POJ 3190 Stall Reservations
- LeetCode 253.Merting Rooms II
另外在一篇博客上找到的问题:
题目描述
时间限制: 1 Sec 内存限制: 128 MB 时间轴上有n个开区间(ai, bi),把这些区间至少划分成多少个集合,使得每个集合中的区间两两没有公共点。因为是开区间,所以(1, 2)和(2,3)可在一个集合中
输入
第1行:一个整数N(1 <= N <=10^5) 接下来N行,每行2个整数Ai,Bi(0<= Ai < Bi < 10^7)
输出
第1行:1个整数,需要划分成的最少集合数。
样例输入
6
4 7
2 5
1 2
3 5
3 6
7 9
样例输出
4
#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
const int INF = 0x0ffffff;
struct Node
{
int left, right;
//左端点升序,右端点降序
bool operator<(const Node & n) const
{
return (left < n.left || (left == n.left && right > n.right));
}
};
int n = 1001;
vector<Node> sequence(n); //记录所有区间的左右端点
vector<int> record(n); //记录各个集合的最右端点
int main()
{
std::ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
cin >> n;
for (int i = 0; i < n; ++i) cin >> sequence[i].left >> sequence[i].right;
sort(sequence.begin(), sequence.begin() + n);
int cnt = 0;
record[0] = sequence[0].right;
for (int i = 1; i < n; ++i) {
bool havePos = false; //判断已知的集合是否可以加入当前区间
//遍历集合的右端点
for (int j = 0; j <= cnt; ++j) {
if (sequence[i].left >= record[j]) {
record[j] = sequence[i].right;
havePos = true;
break;
}
}
//遍历完发现没有集合可以加入,那么需要增加一个集合
if (!havePos) {
++cnt;
record[cnt] = sequence[i].right;
}
}
cout << (cnt + 1) << endl;
return 0;
}
但是注意,这种方法在处理POJ 3190的时候会超时,因为考虑特殊情况,如果每个区间一个集合,那么这就是O(n^2)的算法,所以应该考虑用优先级队列来进行优化。
上面的算法之所以会超时,是因为对于整个过程分析的不够细致,因为考虑如果每一次加入新的一头奶牛就要全部对比,这样很耗时间,是否可以利用数据结构来优化这个过程?
首先肯定是要对输入的序列进行排序,那么目前见过的排序技巧有区间调度里面的右端点排序,初始排序左端点升序,左端点相同,右端点升序。
利用优先级队列的优化,每次取出最早结束的时间,如果最早结束的时间小于序列的开始时间,那么就可以加入围栏,否则就需要新开一个围栏。这样就只需要去进行一次比较,我们需要对这个过程进行证明。
首先考虑队列为空和不为空的两种情形。队列为空的情形比较好处理,不为空的情形需要细节分析。
首先队列的优先级是根据结束时间来确定的,结束的时间越早,优先级就越高。那么需要证明为什么只需要去比较一次。考虑最早结束的时间,如果其小于序列的开始时间,那么第二早结束的时间虽然也有可能小于序列开始的时间,所以会不会存在一种情况,就是最早结束的时间和序列开始的时间内可以插入一个新的序列点。答案是不会的,因为排序是按照序列开始时间升序排列的,当前序列的后面的点的开始时间是不会比当前的开始时间早的。那么此时去更新这个结束时间即可(删掉队列原有的点,加入一个新的点)。
如果最早结束的时间大于等于序列的开始时间,因为队列是按照最早结束的时间来确定优先级的,那么意味着队列里的其他的结束时间肯定也比序列的开始时间要大,意味着已经没有围栏合适了,那么就需要去增加一个围栏。
题目特殊的地方是需要“输出路径”,也就是需要按照输入的顺序去输出每头牛被分配的围栏号,所以需要去记录初始的输入顺序和用一个数组record
去记录每头牛被分配的围栏号。
典型题目:
- leetcode 56 合并区间
- Leetcode 986(两个区间序列求并) 双指针的方法
- LeetCode 57 插入区间
- LeetCode 715 range module
- LeetCode 352 将数据流变为多个不相交区间
- SJTU OJ 1025 (使用map在输入的时候就处理区间重叠和排序)
- openJ_Bailian 3253 集合的划分
- HihoCoder 1726 区间覆盖问题
- POJ 3069 Saruman's Army(区间贪心)
- POJ 1065 Wooden Sticks (区间划分)
- POJ 3190 Stall Reservations(贪心,区间划分+路径输出)
- POJ 2376 Cleaning Shifts(贪心,最少区间覆盖)
- POJ 1328 Radar Installation(贪心,区间选点)