西安段素扫描线

如果我们要统计一个由多个矩形重叠组成的图形的面积。

暴力太麻烦,而计算机又不能想人一样计算,那怎么求解呢?

我们可以使用扫描线fa

想象一下,有一条线,按照一个顺序(从左到右呀,从上到下呀...)扫描一个图形。

我们很容易可以得到,两条最近的相邻线段间,所包含的这一个图形的面积是规整的矩形,又因为这些矩形的长or宽我们已经知道(就是数据中给定矩形的边),所以我们只需要统计另一条边就可以了。

维护线段,我们就可以使用西安段素了。

同一个矩形,在遇到他的第一条和扫描线(是一个线段树)平行的的边时,在这条边和扫描线重叠的部分上累加1(一定是累加),然后继续往后扫描,遇到其他矩形的第一条边时亦是。

知道碰到某个矩形的第二条边,在减去。

那在扫描中怎么算面积呢? 我们需要统计的是扫描线上有多少长度被覆盖,然后乘上两条线段之间的垂直距离就可以了。

那统计周长怎么算呢?

(现在都是从下往上扫)先来看平行于扫描线的边。

两次扫描后,扫描线上的被覆盖长度的差的绝对值就是所增加的长度。

为什么是差呢?又为什么是绝对值呢?

我们通过一张图来解释:

可以看出,这是两个矩形嵌套,然后自己在宽扁的矩形的左边第一条边的扫描线上的被覆盖的长度,和另一个矩形的第一条边时的差就是第二个矩形延展出来的长度,然后绝对值的意思是当一个矩形结束时,他有可能会与他有重叠的矩形凹陷,然后绝对值就将这种情况的边取了出来。

那于扫描线垂直的呢?

我们可以发现,两次扫描中间的那些与扫描线垂直的线段,他的长度都是两次扫描之间的距离,然后我们就需要统计他的个数

可以发现, 两次扫描中,有多少个不相连的空隙,就有相同个数*2条如此线段

然后具体实现会从详解。请先从main()看起

#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
const int manx=5010;
struct Edge
{
	int x1,x2;
	int y;
	int f;
	void change()
	{
		if(x1>x2)
			swap(x1,x2);
		return ;
	}
};
struct node
{
	int f;//有多少条不想交的线段
	int ha;//是否被整体覆盖,不下传
	int len;//总共长度
	int fl,fr;//左右端点是否被覆盖,在合并时会用到
	int l,r;//实际左右端点
};
int te,tx,k;
Edge line[manx<<1];
int base[manx<<1];
int datx[manx<<1];
node t[manx<<5];
void add(int a,int b,int c,int d)
{
	if(b>d)	swap(b,d);
	int x1=min(a,c),x2=max(a,c);
	line[++te].x1=x1;
	line[te].x2=x2;//x1,x2为左右端点,y为横坐标
	line[te].y=b;
	line[te].f=1;//这里是精髓,下边是一个矩阵的开始,上边是一个矩阵的结束,如此处理,我们到时候就可以直接传进更新扫描线的函数里,就不用判断了
	line[++te].x1=x1;
	line[te].x2=x2;
	line[te].y=d;
	line[te].f=-1;
}
bool compare(const Edge &a,const Edge &b)
{
	return a.y==b.y ? a.f > b.f : a.y < b.y;
}
void make_base()
{
	int now=0x7fffffff;
	for(int i=1;i<=tx;i++)
		if(now!=datx[i])
		{
			now=datx[i];
			base[++k]=now;
		}//简简单单的去重
	return ;
}
void build(int root,int l,int r)
{
	t[root].f=0;t[root].len=0;
	t[root].fl=t[root].fr=0;
	t[root].l=base[l];t[root].r=base[r+1];//因为是存的是节点之间的间隙,而且离散化了,所以说在处理左右的时候一定要小心,至于为什么r要+1,因为我是第i个点后的间隙的标号为i
	if(l==r)	return ;//剩下的就很普通了
	int mid=(l+r)>>1;
	build(root<<1,l,mid);
	build(root<<1|1,mid+1,r);
	return ;
}
int check(int val)
{
	int l=1,r=k;
	int mid;
	while(l<r)
	{
		mid=(l+r)>>1;
		if(base[mid]<val)	l=mid+1;
		else	r=mid;
	}
	return l;
}
void push_up(int root,int l,int r)
{
	if(t[root].ha)//如果这个节点被整体覆盖,ha永远也不会变成负数。为什么,可以自己小小的思考一下
	{
		t[root].len=t[root].r-t[root].l;//计算长度
		t[root].fl=t[root].fr=1;//左右端点都被覆盖
		t[root].f=1;//其中有一条线段
		return ;
	}//此处永远不会出现l==r的情况
	else//一般情况
	{
		t[root].len=t[root<<1].len+t[root<<1|1].len;//合并
		t[root].fl=t[root<<1].fl;//大区间的左端点
		t[root].fr=t[root<<1|1].fr;//大区间的右端点
		t[root].f=t[root<<1].f+t[root<<1|1].f-(t[root<<1].fr&t[root<<1|1].fl);//如果左儿子的右端点和右儿子的左端点都被覆盖,那条数就要减1
		return ;
	}
}
void updata(int root,int l,int r,int al,int ar,int k)
{
	if(l>ar||r<al)	return ;
	if(l>=al&&r<=ar)
	{
		t[root].ha+=k;//蛤呸,ha为被整体覆盖的次数,有可能被多次覆盖
		push_up(root,l,r);//更新
		return ;
	}
	int mid=(l+r)>>1;
	updata(root<<1,l,mid,al,ar,k);
	updata(root<<1|1,mid+1,r,al,ar,k);
	push_up(root,l,r);//合并,这里没有push_down
}
int main()
{
	int n;
	scanf("%d",&n);
	int a,b,c,d;
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d%d%d",&a,&b,&c,&d);
		add(a,b,c,d);//保存边,我们这里只需要存与我们扫描线平行的边,然后我是自下而上的。所以我存的是上下边,然后请仔细看add函数   -》add
		datx[++tx]=a,datx[++tx]=c;//用于离散化的数组,当然数据小的话可以不离散化
	}
	sort(line+1,line+1+te,compare);//排序,有重边一定要下边先被处理
	sort(datx+1,datx+1+tx);//离散化
	make_base();//真离散化
	build(1,1,k-1);//建树,注意k为离散化后的坐标个数,这里要建立k-1个叶子节点,线段树中存的是相邻两个坐标之间的间隙
	int ans=0; //周长
	int last=0;//用于记录两次扫描的被覆盖的长度的变量
	for(int i=1;i<=te;i++)
	{
		int pos1=check(line[i].x1),pos2=check(line[i].x2);//二分查找离散化后的位置
		updata(1,1,k-1,pos1,pos2-1,line[i].f);//更新,这里就体现出我们标记上下边的好处了
		int pas=t[1].len;//最近一次扫描的长度
		ans+=abs(last-pas);//处理平行边
		ans+=t[1].f*2*(line[i+1].y-line[i].y);//处理垂直边,一定要与后一个待扫描线之间的距离
		last=pas; //迭代
	}
	printf("%d",ans);
}

posted @ 2018-06-20 16:46  Lance1ot  阅读(176)  评论(0编辑  收藏  举报