线段树+扫描线

本文介绍了一种利用扫描线和线段树求解n个四边平行于坐标轴的矩形面积的方法,通过切割线和区间查询策略,结合线段树的区间维护,解决边界判断和覆盖长度计算的问题,同时优化了线段树节点的对应关系和懒标记机制。
摘要由CSDN通过智能技术生成

扫描线是一种求矩形面积并/周长并的方法。
下面由一道经典题目来引出扫描线—:
n n n 个四边平行于坐标轴的矩形的面积并,如图:

在这里插入图片描述
对于这种由矩形构成的图形,我们可以在 y y y 轴上添加一条扫描线,这样面积就等于 ∑ \sum 与图形相交的长度( d l d l dl × × × 扫过的高度( d h dh dh) ,有种微积分的思想,效果如图:

在这里插入图片描述

那么问题就是实现这样的效果是十分困难,因为并不能很好的确定这条扫描线每次具体移动多远,但是,我们观察可以知道,并不是需要边扫边计算,只需要当扫描线遇到矩形的底边或顶边时,进行计算这段过程扫过的面积即可。即原图形被划分成:

在这里插入图片描述
其中,将这个图形进行分割的四条线我们就称为分割线,为了快速计算出截线段长度,可以将横边赋上不同的权值,具体为:对于一个矩形,其下边权值为1,上边权值为−1。将所有这样的分割线按照y轴坐标从小到大排序,这样扫描线进行扫描的时候,总会先碰到一个矩形的下边,再碰到一个矩形的上边,那么就能保证扫描线所截的长度永远非负了。

定义切割线结构体如下图:

struct line	//切割线
{
	int h,l,r;
	int tag;	//出边+1,入边-1
	bool operator<(const line &t)const
	{
		return h < t.h ;
	}
}L[N*2];

l 、 r l、r lr 是扫描线和矩形相交的左右端点, h h h 是扫描线的y轴坐标, t a g tag tag 表示这条扫描线是矩形的下面的一条边还是上面的一条边,由于题中会告诉我们矩形的左下角坐标和右上角坐标,因此很容易得到两条切割线,{ y 1 , x 1 , x 2 , 1 y1,x1,x2,1 y1,x1,x2,1}、{ y 2 , x 1 , x 2 , − 1 y2,x1,x2,-1 y2,x1,x2,1}。

在这里插入图片描述
然后将它们的端点在 x x x 轴上进行投影,整个问题就转化为了一个区间查询问题,用线段树进行维护,即:每次遇到一条切割线的时候,需要对线段树进行区间修改操作、查询根节点的覆盖长度计算面积。

在这里插入图片描述
我们已经知道,这棵线段树的每个节点都对应了一条线段。但是,仔细观察会发现,线段树维护区间和线段的区间有区别:

在这里插入图片描述

如果我们用线段树节点对应的区间来表示所对应的线段,那么会出现几个问题:

  • 对于叶子节点, t [ u ] . l = = t [ u ] . r t[u].l==t[u].r t[u].l==t[u].r,对应一个点,而非一个线段;
  • 对于非叶子节点左右儿子的区间不会重合(交集为空),但是看这样两条相邻的两条线段 [ 3 , 6 ] 、 [ 6 , 9 ] [3,6]、[6,9] [3,6][6,9],它们是会出现相交的部分的,即一个6号点,而我们并不能简单的把这个点划分到左边或者右边。

因此,我们需要改变所对应的关系,将节点 x x x 所对应区间仍然为 t [ u ] . l 、 t [ u ] . r t[u].l、t[u].r t[u].lt[u].r,但对应的线段为 t [ u ] . l 、 t [ u ] . r + 1 t[u].l、t[u].r+1 t[u].lt[u].r+1,这样的话我们就很完美的解决了上面的两个问题。

注意,只是改变所有节点所对应的线段,并未改变节点所对应的区间。

在这里插入图片描述

当我们进行修改操作的时候,也要进行改变,如果我们要修改线段 [ 1 、 3 ] [1、3] [13],那么对应线段树所要修改的节点区间应该为 [ 1 、 2 ] [1、2] [12]。(线段树节点的区间对应线段为 t [ u ] . l 、 t [ u ] . r + 1 t[u].l 、t[u].r+1 t[u].lt[u].r+1

线段树进行区间修改需要用到懒标记,查询的时候需要知道区间被覆盖的长度,所以,线段树节点存储信息如下:

struct tree
{
	int l,r;
	int tag;	//区间覆盖次数
	ll len;	//区间覆盖长度
}t[N*4];

p u s h u p pushup pushup 函数:

  • 节点 t a g > 0 tag > 0 tag>0 时,表明该节点所对应的线段全部被覆盖, 就用它自身的信息更新
  • 节点 t a g = 0 tag =0 tag=0 时,用左右孩子的信息更新区间覆盖长度。
void pushup(int u)
{
	int l = t[u].l , r = t[u].r;
	if(t[u].tag)	//区间被覆盖过	直接更新不需要孩子的信息
		t[u].len = r+1 - l;
	else	//没被覆盖过,利用孩子更新区间信息
		t[u].len = t[ls].len + t[rs].len;
}

仔细观察可以发现,对区间 [ l 、 r ] [l、r] [lr] 赋值的操作会进行两次,一次是 + 1 +1 +1、一次是 − 1 -1 1,那我们就不需要下传懒标记。懒标记需要下传时对应的条件是:

  1. 进行区间修改:当节点有懒标记,如果需要对其孩子进行修改时,不下传懒标记,它孩子的信息肯定是错误的(因为父节点的标记没有下传,可能会少一些区间覆盖),但是,由于该节点 有懒标记 t a g > 0 tag > 0 tag>0,而 t a g > 0 tag > 0 tag>0 是用自身的信息进行更新,并没有用到孩子错误的信息,因此不会产生错误,当该节点的 t a g = 0 tag=0 tag=0 时,该节点用它的孩子进行更新,此时节点没有懒标记,不存在下传信息缺失的情况,因此也不会产生错误。
  2. 当进行区间询问的时候:一般情况,如果询问的区间是某一个含有懒标记节点的一部分,需要进行下传懒标记,由于我们只需要询问根节点的信息,因此就不需要下传懒标记了。

c h a n g e change change 函数:

void change(int u,int ql,int qr,int k)
{
	if(ql<=t[u].l && t[u].r<=qr)
	{
		t[u].tag += k;
		pushup(u);	//更新区间
		return ;
	}
	int mid = (t[u].l + t[u].r) / 2;
	if(ql<=mid)
		change(ls,ql,qr,k);
	if(mid+1<=qr)
		change(rs,ql,qr,k);
	pushup(u);
}

如果,坐标比较大或者坐标是浮点数需要进行离散化处理。

下面给出洛谷的一道题目扫描线的参考代码:

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10,INF=0x3f3f3f3f;
#define ll long long
struct line	//扫描线
{
	int h,l,r;
	int tag;	//出边+1,入边-1
	bool operator<(const line &t)const
	{
		return h < t.h ;
	}
}L[N*2];

struct tree
{
	int l,r;
	int tag;	//区间覆盖次数
	ll len;	//区间覆盖长度
}t[N*4];
int X[N];
int n;

#define ls u<<1
#define rs u<<1|1

void build(int u,int l,int r)
{
	t[u] = {l,r,0,0};
	if(l==r)	return ;
	int mid = (l+r)/2;
	build(ls,l,mid);
	build(rs,mid+1,r);
	return ;
}

void pushup(int u)
{
	int l = t[u].l , r = t[u].r;
	if(t[u].tag)	//区间被覆盖过	直接更新不需要孩子的信息
		t[u].len = X[r+1] - X[l];
	else	//没被覆盖过,利用孩子更新区间信息
		t[u].len = t[ls].len + t[rs].len;
}
void change(int u,int ql,int qr,int k)
{
	if(ql<=t[u].l && t[u].r<=qr)
	{
		t[u].tag += k;
		pushup(u);	//更新区间
		return ;
	}
	int mid = (t[u].l + t[u].r) / 2;
	if(ql<=mid)
		change(ls,ql,qr,k);
	if(mid+1<=qr)
		change(rs,ql,qr,k);
	pushup(u);
}

int main()
{
	ios::sync_with_stdio(false) , cin.tie(0);

	cin>>n;
	for(int i=1; i<=n; i++)
	{
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		L[2*i-1] = {y1,x1,x2,1} , L[2*i] = {y2,x1,x2,-1};
		X[2*i-1] = x1 , X[2*i] = x2 ;
	}

	sort(L+1,L+2*n+1);	//将扫描线排序
	sort(X+1,X+2*n+1);
	int tot = unique(X+1,X+2*n+1)-X-1;	//去重
	build(1,1,tot-1);	//tot个点 tot-1个空隙


	ll s = 0;
	for(int i=1; i<2*n; i++)
	{
		int ql = lower_bound(X+1,X+tot+1,L[i].l)-X;
		int qr = lower_bound(X+1,X+tot+1,L[i].r)-X;
		change(1,ql,qr-1,L[i].tag);
		s += (L[i+1].h-L[i].h)*t[1].len;
	}

	cout << s << '\n' ;

	return 0;
}

  • 42
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

离你很远的地方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值