文章目录
扫描线(Sweep Line)
扫描线定义
扫描线法是一种求矩形面积并/周长并的好方法。
扫描线:假设有一条扫描线从一个图形的下方扫向上方(或者左方扫到右方),那么通过分析扫描线被图形截得的线段就能获得所要的结果。该过程可以用线段树进行加速。
面积的求法:
面积的求法其实可以简化为sum
截线段长度times
扫过的高度。这也是扫描线算法最基础的应用。具体:
我们可以画出一根线来,扫过这个多边形的所有面积。
这根线从最左边的边开始,每遇到一条边,就停下来计算之前扫到的面积,即为距上一条边的距离与扫描线落在图形上的长度之积,再加到之前的和上面。
用动画来做就是这样的:
**我们怎么才能知道每一次遇到一条边之后扫描线落在图像上的长度是多少 ?**如果我们每一次都去枚举、去找,我们程序的时间复杂度就达不到我们的要求了。
我们只需要计算每一段所对应的在 x 轴上的长度就行了。
我们可以使用线段树这一强大的数据结构来帮助我们更快地进行区间修改。
标记:
我们在遇到某个矩形的一条边的时候,我们需要看一下这两个东西:
- 它的长度;
- 他是起始边还是终止边。
如果是起始边的话,我们把他这条边所包含的所有区块都打上一个标记;如果是终止边的话,我们就把之前起始边打上的标记去掉。这样,只要有标记就是存在,不管有多少个;没有标记就是没有被覆盖,忽略不计。
所以,我们的线段树节点需要存储这个区段被标记的次数。同时,我们还需要存储基本的线段树信息,还有它的长度。
同时,我们用来存储矩形边的位置我们也需要改一下,使之能够存储这条边的上端点和下端点,还要标记这条边是起始边还是终止边。
最终我们使用动画模拟一下就是这个样子的:
391. 完美矩形
难度困难249
给你一个数组 rectangles
,其中 rectangles[i] = [xi, yi, ai, bi]
表示一个坐标轴平行的矩形。这个矩形的左下顶点是 (xi, yi)
,右上顶点是 (ai, bi)
。
如果所有矩形一起精确覆盖了某个矩形区域,则返回 true
;否则,返回 false
。
示例 1:
输入:rectangles = [[1,1,3,3],[3,1,4,2],[3,2,4,4],[1,3,2,4],[2,3,3,4]]
输出:true
解释:5 个矩形一起可以精确地覆盖一个矩形区域。
示例 2:
输入:rectangles = [[1,1,2,3],[1,3,2,4],[3,1,4,2],[3,2,4,4]]
输出:false
解释:两个矩形之间有间隔,无法覆盖成一个矩形。
示例 3:
输入:rectangles = [[1,1,3,3],[3,1,4,2],[1,3,2,4],[2,2,4,4]]
输出:false
解释:因为中间有相交区域,虽然形成了矩形,但不是精确覆盖。
提示:
1 <= rectangles.length <= 2 * 104
rectangles[i].length == 4
提示:
1 <= rectangles.length <= 2 * 104
rectangles[i].length == 4
-105 <= xi, yi, ai, bi <= 105
题解:https://leetcode.cn/problems/perfect-rectangle/solution/gong-shui-san-xie-chang-gui-sao-miao-xia-p4q4/
扫描线解法
将每个矩形 rectangles[i]
看做两条竖直方向的边,使用 (x, y1, y2)
的形式进行存储(其中 y1
代表该竖边的下端点,y2
代表竖边的上端点),同时为了区分是矩形的左边还是右边,再引入一个标识位,即以四元组 (x, y1, y2, flag)
的形式进行存储。
一个完美矩形的充要条件为:对于完美矩形的每一条非边缘的竖边,都「成对」出现(存在两条完全相同的左边和右边重叠在一起);对于完美矩形的两条边缘竖边,均独立为一条连续的(不重叠)的竖边。
图(红色框的为「完美矩形的边缘竖边」,绿框的为「完美矩形的非边缘竖边」):
- 绿色:非边缘竖边必然有成对的左右两条完全相同的竖边重叠在一起;
- 红色:边缘竖边由于只有单边,必然不重叠,且连接成一条完成的竖边。
class Solution {
public boolean isRectangleCover(int[][] rectangles) {
int len = rectangles.length * 2, ids = 0;
int[][] re = new int[len][4];
//初始化re数组,组成[横坐标,纵坐标下顶点,纵坐标上顶点,矩形的左边or右边标志]
for(int[] i:rectangles){
re[ids++] = new int[]{i[0],i[1],i[3],1};
re[ids++] = new int[]{i[2],i[1],i[3],-1};
}
//排序,按照横坐标进行排序,横坐标相等就按纵坐标排序
Arrays.sort(re,(o1,o2)-> o1[0]!=o2[0]?o1[0]-o2[0]:o1[1]-o2[1]);
//操作每一个顶点,判断是否符合要求
for(int i = 0; i < len;){
//如果该边是矩形的左边界,就加入left
List<int[]> left = new ArrayList<>();
//如果该边是矩形的左边界,就加入right
List<int[]> right = new ArrayList<>();
//标志该边是不是 矩形的左边
boolean flag = i == 0;
// 找到所有等于i的边的右边界
int x = i;
while(x < len && re[x][0] == re[i][0]) x++;
while(i < x){ //判断该横坐标的 边是不是符合要求
// 判断当前坐标是矩形的左边还是右边,操作对应的边集合
List<int[]> list = re[i][3] == 1 ? left : right;
if(list.isEmpty()){
list.add(re[i++]);
}else{
int[] pre = list.get(list.size() - 1);
int[] cur = re[i++];
// 有重叠,直接返回false
if(cur[1] < pre[2]) return false;
// 如果正好接触,则直接将前一个的点的上顶点变成当前点的上顶点
if(cur[1] == pre[2]) pre[2] = cur[2];
else list.add(cur); // 不接触,可能有空隙
}
}
//判断边是中间边还是边界边
if(!flag&&x<len){
//如果是中间边 判断左右是不是相等
if(left.size()!=right.size()) return false;
for(int j = 0; j < left.size(); ++j){
if(left.get(j)[2]==right.get(j)[2]&&left.get(j)[1]==right.get(j)[1]) continue;
return false;
}
} else {
//如果是边界边判断是不是一条
if (left.size()!=1&&right.size()==0||left.size()==0&&right.size()!=1) return false;
}
}
return true;
}
}
850. 矩形面积 II
难度困难246
给你一个轴对齐的二维数组 rectangles
。 对于 rectangle[i] = [x1, y1, x2, y2]
,其中(x1,y1)是矩形 i
左下角的坐标, (xi1, yi1)
是该矩形 左下角 的坐标, (xi2, yi2)
是该矩形 右上角 的坐标。
计算平面中所有 rectangles
所覆盖的 总面积 。任何被两个或多个矩形覆盖的区域应只计算 一次 。
返回 总面积 。因为答案可能太大,返回 109 + 7
的 模 。
输入:rectangles = [[0,0,2,2],[1,0,2,3],[1,0,3,1]]
输出:6
解释:如图所示,三个矩形覆盖了总面积为6的区域。
从(1,1)到(2,2),绿色矩形和红色矩形重叠。
从(1,0)到(2,3),三个矩形都重叠。
朴素扫描线模板题
题解:https://leetcode.cn/problems/rectangle-area-ii/solution/gong-shui-san-xie-by-ac_oier-9r36/
class Solution {
//将所有给定的矩形的左右边对应的 x 端点提取出来并排序,每个端点可看作是一条竖直的线段(红色),
//问题转换为求解「由多条竖直线段分割开」的多个矩形的面积总和(黄色):
//由于数据范围只有 200,我们可以对给定的所有矩形进行遍历,统计所有对该矩形有贡献的 y 值线段
//(即有哪些 rs[i] 落在该矩形中),再对线段进行求交集(总长度),即可计算出该矩形的「高度」,
// 从而计算出来该矩形的面积。
int mod = (int)1e9+7;
public int rectangleArea(int[][] rectangles) {
//将所有矩形的x坐标存到list中
List<Integer> list = new ArrayList<>();
for(int[] info : rectangles){
list.add(info[0]);
list.add(info[2]);
}
Collections.sort(list);//对list中x坐标从此小到大排序
long ans = 0;
for(int i = 1; i < list.size(); i++){
//每次取出两个相邻x坐标
//令相邻x坐标距离为len,如果len=0跳过循环
int a = list.get(i-1),b = list.get(i),len = b-a;
if(len == 0) continue;
//定义lines存储能够覆盖(x1,x2)的y坐标对(y1,y2)
List<int[]> lines = new ArrayList<>();
for(int[] info : rectangles){
if(info[0] <= a && info[2] >= b){//当矩形覆盖当前x区间,则将y坐标记录下来
lines.add(new int[]{info[1],info[3]});
}
}
//对所有的y坐标对,按照y1,y2,从小到大排序
Collections.sort(lines, (l1, l2)->{
return l1[0] != l2[0] ? l1[0] - l2[0] : l1[1] - l2[1];
});
//定义tot存储当前x区间下,y区间的并集,l,r为上一个y区间端点
long tot = 0,l = -1,r = -1;
for(int[] cur : lines){
//如果和上次的区间不相交,则将上次区间计入总和,同时更新l,r
if(cur[0] > r){
tot += r-l;
l = cur[0];r = cur[1];
}else if(cur[1] > r){//如果和上次区间相交,则只更新r
r = cur[1];
}
}
tot += r-l;//将最后一个区间求和
ans += tot * len;//面积为区间长度乘以高度和
ans %= mod;
}
return (int)ans;
}
}
离散化 + 线段树 + 扫描线
线段树将整个区间分割为多个不连续的子区间,子区间的数量不超过 log(width)。更新某个元素的值,只需要更新 log(width) 个区间,并且这些区间都包含在一个包含该元素的大区间内。区间修改时,需要使用懒标记保证效率。
- 线段树的每个节点代表一个区间;
- 线段树具有唯一的根节点,代表的区间是整个统计范围,如 [1, N];
- 线段树的每个叶子节点代表一个长度为 1 的元区间 [x, x];
- 对于每个内部节点 [l, r],它的左儿子是 [l, mid],右儿子是 [mid + 1, r] 其中 mid = ⌊(l + r) / 2⌋ (即向下取整)。
对于本题,线段树节点维护的信息有:
- 区间被覆盖的次数 cnt;
- 区间被覆盖的长度 length。
另外,由于本题利用了扫描线本身的特性,因此,区间修改时,不需要懒标记,也无须进行 pushdown 操作。
关于扫描线,可以参考下面这张图。注意,本题代码采用的是从左到右扫描。
class Node{
int l,r,cnt,length;
}
class SegmentTree{
private Node[] tr;
private int[] nums;
public SegmentTree(){}
public SegmentTree(int[] nums){
this.nums = nums;
int n = nums.length-1;
tr = new Node[n << 2];//开n的4倍大小
for(int i = 0; i < tr.length; i++){
tr[i] = new Node();
}
build(1,0,n-1);
}
private void build(int u, int l, int r) {
tr[u].l = l;
tr[u].r = r;
if (l != r) {
int mid = (l + r) >> 1;
build(u << 1, l, mid);//u*2
build(u << 1 | 1, mid + 1, r);//u*2+1
}
}
public void modify(int u, int l, int r, int k) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].cnt += k;
} else {
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) {
modify(u << 1, l, r, k);
}
if (r > mid) {
modify(u << 1 | 1, l, r, k);
}
}
pushup(u);
}
//向上更新,结点里保存矩阵的长度
private void pushup(int u) {
if (tr[u].cnt > 0) {//当前区间里覆盖有矩阵
tr[u].length = nums[tr[u].r + 1] - nums[tr[u].l];
} else if (tr[u].l == tr[u].r) {
tr[u].length = 0;
} else {
tr[u].length = tr[u << 1].length + tr[u << 1 | 1].length;
}
}
public int query() {
return tr[1].length;
}
}
class Solution {
private static final int MOD = (int) 1e9 + 7;
public int rectangleArea(int[][] rectangles) {
int n = rectangles.length;
int[][] segs = new int[n << 1][4];
int i = 0;
// Y 坐标去重排序,离散化
TreeSet<Integer> ts = new TreeSet<>();
//[横坐标,纵坐标下顶点,纵坐标上顶点,矩形的左边or右边标志]
for (var e : rectangles) {
int x1 = e[0], y1 = e[1], x2 = e[2], y2 = e[3];
segs[i++] = new int[] {x1, y1, y2, 1};
segs[i++] = new int[] {x2, y1, y2, -1};
ts.add(y1);
ts.add(y2);
}
//按横坐标排序
Arrays.sort(segs, (a, b) -> a[0] - b[0]);
Map<Integer, Integer> m = new HashMap<>(ts.size());
i = 0;
int[] nums = new int[ts.size()];//用来初始化线段树的
//把所有去重后的y索引放到map表中,记录每个y在线段树中的位置索引
for (int v : ts) {
m.put(v, i);
nums[i++] = v;
}
//扫描线,记录大小
SegmentTree tree = new SegmentTree(nums);//初始并建立线段树
//线段树包含的信息有:区间被覆盖的次数 cnt 和 区间被覆盖的长度 length。
long ans = 0;
for (i = 0; i < segs.length; ++i) {
var e = segs[i];//依次遍历所有点,按照x从小到大的顺序
int x = e[0], y1 = e[1], y2 = e[2], k = e[3];
if (i > 0) {
ans += (long) tree.query() * (x - segs[i - 1][0]);
}
tree.modify(1, m.get(y1), m.get(y2) - 1, k);
//矩形的左边标志为1,右边标志为-1,有点像差分数组,记录当前重叠次数
}
ans %= MOD;
return (int) ans;
}
}