基本知识:
线段树:存储线段的二叉排序树,以start作为key, 同时维护树中最大的右端点值 max
class Node {
int start;
int end;
int max;
}
经典应用:query any interval that intersect(overlap) with a given interval. 如果是查询所有与给定线段重合的线段,则查到一个,从树种删去,再查。最后再插回来。复杂度为RlgN
区间树:区间[l, r]的左右子树分别为[l, (l + r) / 2], [(l + r) / 2 + 1, r],叶节点都是一个元素的区间[a,a]。主要用于查询区间上的属性,比如和,最大值最小值,满足某种条件的元素个数等。
经典应用,给定一个数组,需要高效的查询任意区间的和(最大值,满足某种条件的元素个数等)。对区间建立区间树,然后每次查询都是lgN的。原理是这样,原区间的值,都都是由左右两个子区间的值得来的,类似merge,比如求整个区间的和,可以分别求左右区间的和,然后相加。这是一个递归的过程,递归到只有一个元素。当区间只有一个元素时,和、最大值等都是自己。
一些经典题目:
1. 给定一组飞机的起飞和降落时间,问同时最多几架飞机在飞行?类似的问法,一组火车出发到达时刻表,问最多同时几趟火车在运行?一组括号中最多几层嵌套?
solution:经典的扫描线法,把线段(start, end)拆成点(time, start/end),排序,如果时间值相同,终点在前起点在后。遇到一个起点count++,遇到一个终点count --。
int countOfAirplanes(vector &airplanes) {
vector> times;
for (auto &it : airplanes) {
times.push_back(make_pair(it.start, 1));
times.push_back(make_pair(it.end, 0));
}
sort(times.begin(), times.end());
int maxNum = 0, curNum = 0;
for (auto &t : times) {
if (t.second == 1) curNum++;
else curNum--;
maxNum = max(maxNum, curNum);
}
return maxNum;
}
solution: 按起点排序,
循环不变式:result 是一个merge过有序的线段列表,初始化就是第一个线段。当前线段跟result.里最后一个线段比,当前线段的start肯定晚于result.back()的start, 因为之前已经按start排过序,主要看是否和其end相交:
1)如果小于等于其end,说明有相交,只需要更新上一个选段的终点 result.back().end = max(result.back().end, cur.end())
2)否则不想交,直接append到result里
vector merge(vector &intervals) {
if (intervals.empty()) return intervals;
sort(intervals.begin(), intervals.end(),[](Interval a, Interval b) -> bool { return a.start < b.start;});
vector output(1, intervals[0]);
for(int i = 1; i < intervals.size(); i++) {
if (intervals[i].start <= output.back().end)
output.back().end = max(output.back().end, intervals[i].end);
else
output.push_back(intervals[i]);
}
return output;
}
思路
1)跳过在前面的、不相交的线段:新线段起点在其终点后面
2)处理重合的,更新线段的起、终点,并且删除原线段:重合条件:新线段终点大于等于其起点(之前已经保证了新线段起点早于其终点)
3)append后面的不相交的线段
版本一:原地插入
vector insert(vector& intervals, Interval newInterval) {
auto i = intervals.begin();
while (i != intervals.end() && newInterval.start > i->end) i++;
while (i != intervals.end() && newInterval.end >= i->start) {
newInterval.start = min(newInterval.start, i->start);
newInterval.end = max(newInterval.end, i->end);
i = intervals.erase(i);
}
intervals.insert(i, newInterval);
return intervals;
}
版本二:用另一个列表保存结果
public ArrayList insert(ArrayList intervals, Interval newInterval) {
ArrayList result = new ArrayList();
int i = 0;
// proceding intervals
while (i < intervals.size() && intervals.get(i).end < newInterval.start) {
result.add(intervals.get(i));
i++;
}
//overlaping intervals, just keeps a longest one
for (; i < intervals.size() && intervals.get(i).start <= newInterval.end; i++) {
newInterval.start = Math.min(newInterval.start, intervals.get(i).start);
newInterval.end = Math.max(newInterval.end, intervals.get(i).end);
}
result.add(newInterval);
// intervals going behind
for (; i < intervals.size(); i++)
result.add(intervals.get(i));
return result;
}
4 带颜色的刷线段问题,输入是一系列把一个区间刷成某种颜色的操作,(start, end, color),输出最后的状态
分析;和insert 线段有点像,分三个部分:
1)首先要skip前面不相交的部分,toInsert.start > intervals[i].end || toInsert.start == intervals[i].end && toInsert.color != intervals[i].color,这里的条件稍有不同,和上一个线段的end刚好连上,但是颜色不同,也skip。
2) merge的部分:不带颜色的线段的merge,overlap的部分连在一起,就是保留一个最长的线段。带颜色的话,可能是生成1个,2个,3个,两头部分覆盖,中间完全覆盖
3)后面不相交的部分,toInsert.end < intervals[i].start || toInsert.end == intervals[i].start && toInsert.color != intervals[i]