这个算法理解还是挺好理解的,就是到后面解决面积并问题的时候开始难理解了,看了半天,主要是还有其他的知识没理解就开始搞这个了。虽然最后还是直接懂了。
文章目录
- 扫描线算法的介绍
- 一维问题
- LintCode 391 数飞机
- 题目描述
- 解决
- letcode 218. 天际线问题
- 题目描述
- 题解
- 二维问题
- 洛谷 P5490 【模板】扫描线
- 题目描述
- 题目分析
扫描线算法的介绍
扫描线顾名思义也就是一条线进行扫描,一般从左到右,从下到上也是可以的。
算法适用于,前后时间的问题,一维区间重合的问题,然后还有最难的二维矩形面积重合问题。
总的来说就类似于需要一条线冲起来一样的。
一维问题
LintCode 391 数飞机
题目描述
给出飞机的起飞和降落时间的列表,用序列 interval 表示. 请计算出天上同时最多有多少架飞机?
样例
样例 1:
输入: [(1, 10), (2, 3), (5, 8), (4, 7)]
输出: 3
解释:
第一架飞机在1时刻起飞, 10时刻降落.
第二架飞机在2时刻起飞, 3时刻降落.
第三架飞机在5时刻起飞, 8时刻降落.
第四架飞机在4时刻起飞, 7时刻降落.
在5时刻到6时刻之间, 天空中有三架飞机.
样例 2:
输入: [(1, 2), (2, 3), (3, 4)]
输出: 1
解释: 降落优先于起飞.
解决
以时间为线,扫描时刻。
我们可以以起飞设置为1.
落地设置为-1.
但是需要注意的是落地有优先权
public int countOfAirplanes(List<Interval> airplanes) {
// write your code here
int[][] arr = new int[airplanes.size() * 2][2];
for (int i = 0; i < airplanes.size(); i++) {
arr[2 * i] = new int[]{airplanes.get(i).start, 1};
arr[2 * i + 1] = new int[]{airplanes.get(i).end, -1};
}
Arrays.sort(arr, (o1, o2) -> o1[0] == o2[0] ? o1[1] - o2[1] : o1[0] - o2[0]);
int ans = 0,count = 0;
for (int[] ints : arr) {
if (ints[1] == 1)
count++;
else
count--;
ans = Math.max(ans,count);
}
return ans;
}
letcode 218. 天际线问题
题目描述
题解
这里提供的代码不是特别快。但是写起来舒服点。
还是老样子进行排序,然后入的高度设置为正,出设置为负。
我们每次只需要最高处的高度就可以了。所以用到pq。
pq的删除是一个线性
o
(
n
)
o(n)
o(n) 的删除,需要的时间比较长。
public List<List<Integer>> getSkyline(int[][] buildings) {
ArrayList<Point> a = new ArrayList<>(buildings.length * 2 + 2);
for (int[] building : buildings) {
a.add(new Point(building[0], building[2]));
a.add(new Point(building[1], -building[2]));
}
a.sort((o1, o2) -> o1.x == o2.x ? o2.h - o1.h : o1.x - o2.x);
PriorityQueue<Integer> h = new PriorityQueue<>(Comparator.comparing(Integer::intValue).reversed());
h.add(0);
ArrayList<List<Integer>> ans = new ArrayList<>();
int height = 0;
for (Point point : a) {
if (point.h > 0) {
h.offer(point.h);
} else {
h.remove(-point.h);
}
if (h.peek() != height) {
ans.add(Arrays.asList(point.x, h.peek()));
}
height = h.peek();
}
return ans;
}
record Point(int x, int h) {}
二维问题
这个问题比较一维问题的难点就在于,除了对x的记录外,我们还需要y的记录。一维的最后一个题目上,y其实不需要记录下面,因为下一定是0.
而二维的矩阵上下y都需要记录,需要得到长度。
现在我们从一道模板题来分析
好像知道线段树会好理解,我搞完在去学。
洛谷 P5490 【模板】扫描线
题目描述
求
n
n
n 个四边平行于坐标轴的矩形的面积并。
输入格式
第一行一个正整数
n
n
n。
接下来 n n n 行每行四个非负整数 x 1 , y 1 , x 2 , y 2 x_1, y_1, x_2, y_2 x1,y1,x2,y2,表示一个矩形的四个端点坐标为 ( x 1 , y 1 ) , ( x 1 , y 2 ) , ( x 2 , y 2 ) , ( x 2 , y 1 ) (x_1, y_1),(x_1, y_2),(x_2, y_2),(x_2, y_1) (x1,y1),(x1,y2),(x2,y2),(x2,y1)。
输出格式
一行一个正整数,表示 n n n 个矩形的并集覆盖的总面积。
样例 #1
样例输入 #1
2
100 100 200 200
150 150 250 255
样例输出 #1
18000
提示
对于
20
%
20\%
20% 的数据,
1
≤
n
≤
1000
1 \le n \le 1000
1≤n≤1000。
对于
100
%
100\%
100% 的数据,
1
≤
n
≤
10
5
1 \le n \le {10}^5
1≤n≤105,
0
≤
x
1
<
x
2
≤
10
9
0 \le x_1 < x_2 \le {10}^9
0≤x1<x2≤109,
0
≤
y
1
<
y
2
≤
10
9
0 \le y_1 < y_2 \le {10}^9
0≤y1<y2≤109。
题目分析
这算是扫描线的一个经常算的题目了,计算矩形的面积。
对于下面这个图来说
计算面积可以是
(
4
−
1
)
∗
(
3
−
1
)
+
(
6
−
3
)
∗
(
4.5
−
2
)
−
(
4
−
3
)
∗
(
3
−
2
)
(4-1)*(3-1)+(6-3)*(4.5-2) - (4-3)*(3-2)
(4−1)∗(3−1)+(6−3)∗(4.5−2)−(4−3)∗(3−2)
我们也可以拆分来计算
拆成abc3个部分来计算
面积就是
(
3
−
1
)
∗
(
3
−
1
)
+
(
4
−
3
)
∗
(
4.5
−
1
)
+
(
6
−
4
)
∗
(
4.5
−
2
)
(3-1)*(3-1)+(4-3)*(4.5-1)+(6-4)*(4.5-2)
(3−1)∗(3−1)+(4−3)∗(4.5−1)+(6−4)∗(4.5−2)
我们就可以像一维那样,从左到右,分为入边和出边来计算。
△
x
\triangle x
△x很好处理。但是高度就比较难处理了。
首先我想到的就是可以入边则加区间,出边则将区间减去。
每次都对区间来在进行一次区间计算高度。
但是这样复杂度比较高。
我们采取的是离散化来进行计算。
将y离散化,即下面这样的区间。
如果在区间则,入边则令所在区间+1,出边则-1
这个题的y离散后就是
[
1
,
2
,
3
,
4.5
]
[1,2,3,4.5]
[1,2,3,4.5]
进行模拟:
- 第一条入边为[1,3] 长度为2,宽度为2 s u m = 4 sum = 4 sum=4
- 第二条入边为[2,4] 长度为3.5 宽度为1 s u m = 7.5 sum = 7.5 sum=7.5
- 第三条出边为[1,3] 则剩下[3,4] 长度为2.5 宽度为2 s u m = 11.5 sum = 11.5 sum=11.5
接下来就是实现了:
采用的是建树。
至于原因,我的理解是更快的求得长度。
如我想要获得
[
1
,
3
]
[1,3]
[1,3]就直接有想要
[
2
,
4
]
[2,4]
[2,4]就用
[
1
,
4
]
[1,4]
[1,4]减去
12
12
12
接下来就是代码了
先实现建树
树维护着
- 树的节点个数
- 访问的节点
- 每个节点对应的区间
- 区间所占用的长度
去重手写会更快,不过stream写的很爽。
本来还用set试试了,但是转化为Integer[]还行int[]太麻烦了。不如stream爽。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Arrays;
public class test {
static final StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static int nextInt() throws IOException {
st.nextToken();
return (int) st.nval;
}
public static void main(String[] args) throws IOException {
//初始化
int n = nextInt();
int size = n << 1;
Line[] lines = new Line[size + 1];
int[] yPos = new int[size];
// HashSet<Integer> yPos = new HashSet<>();
for (int i = 1; i <= n; i++) {
int x1 = nextInt();
int y1 = nextInt();
int x2 = nextInt();
int y2 = nextInt();
int now = i << 1;
int next = now - 1;
lines[now] = new Line(y1, y2, x1, 1);
lines[next] = new Line(y1, y2, x2, -1);
yPos[now - 1] = y1;
yPos[next - 1] = y2;
}
// 离散化排序
yPos = Arrays.stream(yPos).distinct().sorted().toArray();
int len = yPos.length;
uniqueY = new int[len + 2];
System.arraycopy(yPos, 0, uniqueY, 1, len);
uniqueY[len + 1] = Integer.MAX_VALUE;
Arrays.sort(lines, 1, size + 1);
uniqueYCnt = len;
// duplicateRemoval(yPos, size);
// 线段树
SegmentTree tree = new SegmentTree(uniqueYCnt, uniqueY);
// 扫描线
long ans = 0;
for (int i = 1; i < size; i++) {
Line line = lines[i];
tree.update(line.y1, line.y2, line.weight);
ans += tree.getSum() * (lines[i + 1].x - line.x);
}
System.out.println(ans);
}
//去重后的个数
static int uniqueYCnt;
//去重数组
static int[] uniqueY;
// 手写去重会更快
// static void duplicateRemoval(int[] arr, int len) {
// int[] array = Arrays.stream(arr).distinct().toArray();
// int last = uniqueY[1] = arr[1];
// uniqueYCnt = 1;
// for (int i = 2; i <= len; i++) {
// if (arr[i] == last)
// continue;
// last = uniqueY[++uniqueYCnt] = arr[i];
// }
// uniqueY[uniqueYCnt + 1] = Integer.MAX_VALUE;
// }
static class Line implements Comparable<Line> {
int y1;
int y2;
int x;
int weight;
public Line(int y1, int y2, int x, int weight) {
this.y1 = y1;
this.y2 = y2;
this.x = x;
this.weight = weight;
}
@Override
public int compareTo(Line o) {
return x - o.x;
}
}
}
class SegmentTree {
private final long[] tree;// 线段树
private final int size;// 离散化后的长度
private final int[] mapping;// 离散化
private final int[] visitCount;// 访问次数
// 初始化
public SegmentTree(int size, int[] mapping) {
this.size = size;
this.mapping = mapping;
this.tree = new long[size * 10 + 1];
this.visitCount = new int[size * 10 + 1];
}
// 获取区间左边
private int updateL;
// 获取区间右边
private int updateR;
/**
* 获取区间和
*
* @param l 区间左边
* @param r 区间右边
* @param value 访问更新值 1 为访问 -1 为取消访问
*/
public void update(int l, int r, int value) {
updateL = l;
updateR = r;
getUpdate(1, size, 1, value);
}
/**
* 更新
*
* @param currentL 当前区间左边
* @param currentR 当前区间右边
* @param pos 当前节点
* @param value 访问更新值 1 为访问 -1 为取消访问
*/
private void getUpdate(int currentL, int currentR, int pos, int value) {
int leftValueMapping = mapping[currentL];
int rightValueMapping = mapping[currentR + 1];
// 不相交
if (rightValueMapping <= updateL || leftValueMapping >= updateR) {
return;
}
// 完全覆盖
if (updateL <= leftValueMapping && rightValueMapping <= updateR) {
visitCount[pos] += value;
} else {
// 递归子节点
int mid = (currentL + currentR) >> 1;
// 左子节点
getUpdate(currentL, mid, pos << 1, value);
// 右子节点
getUpdate(mid + 1, currentR, pos << 1 | 1, value);
}
if (visitCount[pos] > 0) {
// 访问次数大于0,说明这个区间被访问过
// 那么这个区间的值就是区间长度
tree[pos] = rightValueMapping - leftValueMapping;
} else {
int temp = pos << 1;
// 没有访问=子节点相加
tree[pos] = tree[temp] + tree[temp | 1];
}
}
// 获取区间和
public long getSum() {
return tree[1];
}
}