如果我们要统计一个由多个矩形重叠组成的图形的面积。
暴力太麻烦,而计算机又不能想人一样计算,那怎么求解呢?
我们可以使用扫描线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);
}