[算法] 扫描线及其应用
前言
定义
在一个笛卡尔坐标系内,用一根无限长线在此坐标系内扫描,这根线就叫做扫描线,通俗易懂。
通常情况下,在坐标系内确定一条线段需要两个端点。但在特殊情况下,如该直线平行于 \(y\) 轴,只需要三个信息来确定:端点的纵坐标,任意一点的横坐标。
即是:
struct Scan_Line {
int CoordX, CoordY_Up, CoordY_Down;
Scan_Line() {}
Scan_Line(int X, int YD, int YU, int A) {
CoordX = X;//任意一点的横坐标
CoordY_Down = YD;//下端点的纵坐标
CoordY_Up = YU;//上端点的纵坐标
}
#define X(x) Line[x].CoordX
#define YD(x) Line[x].CoordY_Down
#define YU(x) Line[x].CoordY_Up
};
扫描线比较特殊,一般情况下取用平行或垂直于 \(y\) 轴的直线。
应用
给一道例题来理解。
题目大意
在笛卡尔坐标系内,有 \(n\) 个矩形,求这 \(n\) 个矩形共同覆盖的面积。
输入的第一行一个正整数 \(n\) 。
接下来 \(n\) 行每行四个非负整数 \(x_1, y_1, x_2, y_2\) ,表示一个矩形的左下角坐标为 \((x_1, y_1)\) ,右上角坐标为 \((x_2, y_2)\) 。
输出答案即可。
思路
First
题目的意思用图像来表示:
那么这 \(5\) 个矩形的面积并就为:
这些图形都是很不规则的,但是把他们细分成以下几个部分。
这样划分后,答案就为每一个部分的低和高来求了。注意,高可能是断断续续的,如上图的蓝色部分,虽然是两个矩阵,但是底相同。
那么就可以使用扫描线从左到右扫描,计算每个部分的面积,每个部分的面积就为底乘高。
Second
大思路有了,现在来探讨怎么实现。
不难发现,扫描线扫描到了矩形的边的时候,长度发生改变。在具体一点,当扫描线扫描到矩形的左边的边时,长度可能增加,扫描到右边的边时,长度可能缩短。
进一步按照扫描线的横坐标进行排序,则扫描线的宽就出来了。按照这样的扫描顺序,就应该求出高为多少。
这又涉及到区间覆盖的问题,扫描线究竟覆盖了多少长度?前面说过,扫描线其实并不是连续的,有可能是断断续续的,而线段树这种数据结构就成了首选。
线段树主要维护的是这个区间内的线段覆盖的情况,是一颗权值线段树,由于数据较大,可以先使用类似离散化的思想来优化空间。
一共有 \(2n\) 条线段,因为有 \(n\) 个矩形,而每个矩形都有两条边。每扫描到一条边的时候,这条线段对应的区间就改变。设 \(C\) 为该区间内有多少线段覆盖,\(Len\) 为线段的被覆盖总长度,不难推出线段树的子节点更新父节点的式子:
void Push_Up(int pos) {
if(C(pos))//该区间被全覆盖
LEN(pos) = R(pos) - L(pos);
else
LEN(pos) = LEN(LC(pos)) + LEN(RC(pos));
}
初始化建树:
void Make_Tree(int pos, int l, int r) {
Tree[pos].Init_Tree(y[l], y[r]);
if(r - l <= 1)
return;
int mid = (l + r) >> 1;
Make_Tree(LC(pos), l, mid);
Make_Tree(RC(pos), mid, r);
}
若有区间被全覆盖,则 \(Len\) 就等于左右的距离,若没有全覆盖,就为两个儿子的距离总和。可能会觉得多此一举,但是若该区间已经被全覆盖了,就不用递归到子节点了,可以省去很多时间,将 \(O(n^2)\) 的算法降到 \(O(nlog(n))\) ,其实 \(C\) 就相当于延迟标记,故而时间复杂度就可以类比线段树的 “区间修改 区间查询” 问题。修改代码如下:
void Update_Tree(int pos, int l, int r, int k) {
//若该边为左边,k=1;若该边为右边,k=-1。因为C为该区间覆盖边的边数
if(l <= L(pos) && R(pos) <= r) {
C(pos) += k;
Push_Up(pos);
return;
}
if(l < R(LC(pos)))//与左儿子有交叉
Update_Tree(LC(pos), l, r, k);
if(r > L(RC(pos)))//与右儿子有交叉
Update_Tree(RC(pos), l, r, k);
Push_Up(pos);
}
既然不递归到子节点,那么程序会不会出错?注意,我们想要查找的是整个扫描线被覆盖的长度,即是根节点被覆盖的长度,不会去查询子节点,所以不用担心。
C++代码
#include <cstdio>
#include <algorithm>
using namespace std;
void Quick_Read(int &N) {
N = 0;
char c = getchar();
int op = 1;
while(c < '0' || c > '9') {
if(c == '-')
op = -1;
c = getchar();
}
while(c >= '0' && c <= '9') {
N = (N << 1) + (N << 3) + (c ^ 48);
c = getchar();
}
N *= op;
}
const int MAXN = 2e5 + 5;
struct Segment_Tree {//权值线段树
int Left_Section, Right_Section;
int Cover_Section;
long long Linear_Length;
void Init_Tree(int l, int r) {
Left_Section = l;
Right_Section = r;
Linear_Length = 0;
Cover_Section = 0;
}
#define LC(x) (x << 1)
#define RC(x) (x << 1 | 1)
#define L(x) Tree[x].Left_Section
#define R(x) Tree[x].Right_Section
#define C(x) Tree[x].Cover_Section
#define LEN(x) Tree[x].Linear_Length
};
struct Scan_Line {//扫描线
int CoordX, CoordY_Up, CoordY_Down;
int AddSub;
Scan_Line() {}
Scan_Line(int X, int YD, int YU, int A) {
CoordX = X;
CoordY_Down = YD;
CoordY_Up = YU;
AddSub = A;
}
#define X(x) Line[x].CoordX
#define YD(x) Line[x].CoordY_Down
#define YU(x) Line[x].CoordY_Up
#define A(x) Line[x].AddSub
};
Segment_Tree Tree[MAXN << 3];
Scan_Line Line[MAXN];
int y[MAXN];
int n, now;
bool cmp(Scan_Line x, Scan_Line y) {
return x.CoordX < y.CoordX;
}
void Push_Up(int pos) {
if(C(pos))
LEN(pos) = R(pos) - L(pos);
else
LEN(pos) = LEN(LC(pos)) + LEN(RC(pos));
}
void Make_Tree(int pos, int l, int r) {//初始化线段树
Tree[pos].Init_Tree(y[l], y[r]);
if(r - l <= 1)//r-l就结束,若l=r就结束,那么l~l这条线段就是一个点,没有修改的意义
return;
int mid = (l + r) >> 1;
Make_Tree(LC(pos), l, mid);
Make_Tree(RC(pos), mid, r);
//这里不能mid+1,因为线段l~mid加上mid+1~r不等价于l~r
}
void Update_Tree(int pos, int l, int r, int k) {
if(l <= L(pos) && R(pos) <= r) {//全覆盖
C(pos) += k;//覆盖的线段个数+1或-1
Push_Up(pos);//C该边L有可能该边
return;
}
if(l < R(LC(pos)))
Update_Tree(LC(pos), l, r, k);
if(r > L(RC(pos)))
Update_Tree(RC(pos), l, r, k);
Push_Up(pos);//子节点该边父节点也有可能改变
}
void Build() {
sort(y + 1, y + 1 + (n << 1));//类似离散化的思想
sort(Line + 1, Line + 1 + (n << 1), cmp);//对扫描线排序
Make_Tree(1, 1, (n << 1));
}
void Scan() {
unsigned long long res = 0;
for(int i = 1; i <= n << 1; i++) {
res += LEN(1) * (X(i) - X(i - 1));//“低×高”
Update_Tree(1, YD(i), YU(i), A(i));//修改区间覆盖的长度
}
printf("%llu", res);
}
void Read() {
Quick_Read(n);
int X_1, X_2, Y_1, Y_2;
for(int i = 1; i <= n; i++) {
Quick_Read(X_1);
Quick_Read(Y_1);
Quick_Read(X_2);
Quick_Read(Y_2);
y[i] = Y_1;
y[i + n] = Y_2;
Line[i] = Scan_Line(X_1, Y_1, Y_2, 1);//左边
Line[i + n] = Scan_Line(X_2, Y_1, Y_2, -1);//右边
}
}
int main() {
Read();
Build();
Scan();
return 0;
}