• [算法] 扫描线及其应用


    前言

    本文例题链接

    定义

    在一个笛卡尔坐标系内,用一根无限长线在此坐标系内扫描,这根线就叫做扫描线,通俗易懂。

    通常情况下,在坐标系内确定一条线段需要两个端点。但在特殊情况下,如该直线平行于 (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;
    }
    
  • 相关阅读:
    Go语言【第八篇】:Go语言变量作用域
    Go语言【第七篇】:Go函数
    Django 2.0 学习(10):Django 定制化
    Go语言【第六篇】:Go循环语句
    Go语言【第五篇】:Go条件语句
    Go语言【第四篇】:Go运算符
    Go语言【第二篇】:Go语法和数据类型
    苹果电脑自带python安装tensorflow一直有问题
    那些年深度学习所踩过的坑-第一坑
    C++基础知识--DAY3
  • 原文地址:https://www.cnblogs.com/C202202chenkelin/p/14140580.html
Copyright © 2020-2023  润新知