HDU1255 覆盖的面积(线段树+扫描线+离散化)
模板题
给定平面上若干矩形,求出被这些矩形覆盖过至少两次的区域的面积.
Input
输入数据的第一行是一个正整数T(1<=T<=100),代表测试数据的数量.每个测试数据的第一行是一个正整数N(1<=N<=1000),代表矩形的数量,然后是N行数据,每一行包含四个浮点数,代表平面上的一个矩形的左上角坐标和右下角坐标,矩形的上下边和X轴平行,左右边和Y轴平行.坐标的范围从0到100000.注意:本题的输入数据较多,推荐使用scanf读入数据.
Output
对于每组测试数据,请计算出被这些矩形覆盖过至少两次的区域的面积.结果保留两位小数.
Sample Input
2 5
1 1 4 2
1 3 3 7
2 1.5 5 4.5
3.5 1.25 7.5 4
6 3 10 7
3
0 0 1 1
1 0 2 1
2 0 3 1
Sample Output
7.63
0.00
题解
让你求解所有矩形覆盖的面积和,或者是周长和,如果用寻常的方法,非常之麻烦,而且效率也不高,这里就会用到线段树的扫描线
扫描线应对方案:
由于题目提供的矩形比较多,坐标也很大,所以坐标需要离散化,可以按照题目要求或者自己的喜好,离散横坐标或者纵坐标都可以,这里讲的都是离散横坐标,不离散纵坐标。
假设有一条扫描线,从下往上扫描过整个多边形叠加的区域。如果是竖直方向上扫描,则是离散化横坐标,如果是水平方向右扫描,在进行扫描前,要保存好所有矩形的上边和下边,并且按照它们所处的高度进行排序,另外我们给上边赋值为-1,给下边赋值为1,接着我们用一个结构体来保存所有矩形的上边和下边,其中还有两条边不用管,因为他们已经包括在了高度中,高度差便能得到边的长度。
然后将扫描线从下往上扫描,每遇到一条上边或者下边就停下来,将这条线段加入到总区间上,下边赋值是1,扫描到下边的话相当于往总区间插入一条线段,上边为-1,扫描到上边相当于在总区间删除一条线段(也可以理解为当插入1的时候我们的扫描线在一个矩形中,当插入-1的时候证明我们的扫描线离开了一个矩形的区域,如此可以知道,区间不会出现负数的情况,因为永远都是下边1 >= 上边-1,他们相加是永远大于等于零的)
然后用下一条边的高度减去当前这条边的高度,乘上总区间被覆盖的长度,就能得到最终的面积
下面是pushup具体含义 主要依靠Ctrl+v
如果当前的位置为叶子节点,Cnt[o] == 1话证明被完全覆盖了,进行赋值。
如果为Cnt[o] == 0且还是叶子节点,这个节点一定为0。
如果当前位置不是叶子节点那么我们就要小心一点了,如果Cnt[o] == 0话,我们能说他没有被覆盖吗,不能,只能说没有被完全覆盖,为什么呢,因为他的子节点可能被完全覆盖了,但是父亲节点没有覆盖,那么怎么完全的不漏的得到这个区间被完全覆盖的区间大小呢,可以直接通过子节点覆盖的值
Sum[o] = Sum[o << 1] + Sum[o << 1|1]得到rt这个位置总覆盖区间
上面是覆盖一次的做法,那覆盖两次的做法是什么呢?
那么我们就要多增加一个数组用来记录覆盖了两次或者两次以上的区间大小。
Sum[o]表示覆盖了一次的区间
Sum2[o]表示覆盖了两次的区间
最开始跟上面的没有什么区别
如果Cnt[o] >= 2的话,很明显这个区间一定都被覆盖了至少是两次
如果Cnt[o] == 1的话,这里大家可以思考一下,直接从Sum2[o] = Sum[o << 1] + Sum[o << 1|1]
这里为什么是从子节点传上来呢,如果Cnt[o] == 1的话,证明此时这个位置已经被覆盖了一次,如果子节点也被覆盖了一次那不就是子节点被覆盖了两次吗,如此直接传给父亲节点,表示这个区间被覆盖了两次的答案
如果Cnt[o] == 0的话且还是叶子节点,sum2就是0。
如果不是,证明这个地方没有被覆盖一次,那么我们只有从子节点将覆盖了两次的结果传来了,Sum2[o] = Sum2[o << 1] + Sum2[o << 1|1];
线段是维护的是区间没有l==r 注意注意在注意 还要注意左右儿子的范围
当根节点为1-4时左儿子为1-2,右儿子为2-4.不要漏掉区间
上代码
#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include <cstdlib>
#include <functional>
#define mem(a,x) memset(a,x,sizeof(a))
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull; // %llu
const double PI = acos(-1.0);
const double eps = 1e-6;
const int mod=1e9+7;
const int INF = -1u>>1;
const int maxn = 1e4+5;
struct edge //存每一条平行x轴的边
{
double x1,x2,h; //分别表达此条边的左右端点和y坐标
int cnt; //入边为1,出边为-1,方便更新。。重点
edge () {}
edge(double x1,double x2,double h,int cnt):x1(x1),x2(x2),h(h),cnt(cnt) {};
friend bool operator < (edge a,edge b)
{
return a.h<b.h;
}
} p[maxn];
struct node //tree
{
int l,r,o,cnt;
double sum,sum2; //sum为覆盖大于一次的长度,sum2为覆盖大于两次的长度
} t[4*maxn];
double X[maxn];
void build(int l,int r,int o)
{
t[o].l=l;
t[o].r=r;
t[o].sum=0;
t[o].sum2=0;
t[o].cnt=0;
if(l==r-1) //注意叶子节点的范围,
return ;
int mid=(l+r)>>1;
build(l,mid,o<<1); //注意左右儿子的范围 当根节点为1-4时左儿子为1-2,右儿子为2-4.不要漏掉区间。
build(mid,r,o<<1|1); //维护的是l-r这个区间的cnt,sum,sum2,不存在l==r这种情况
}
void pushup(int o) //关键
{
//维护覆盖一次的面积
if(t[o].cnt>=1)
t[o].sum=X[t[o].r]-X[t[o].l]; //如果当前节点被覆盖大于一次,记录到sum中 此值即为此区间范围
else if(t[o].l==t[o].r-1) //如果当前节点没有被覆盖并且为叶子节点 值就是0 很好理解8
t[o].sum=0;
else //如果当前节点没有被覆盖 注意这里覆盖的意思是指没有完全被覆盖,所以当前值需要左右儿子相加求得
t[o].sum=t[o<<1].sum+t[o<<1|1].sum;
//维护覆盖两次的面积
if(t[o].cnt>=2) //如果当前节点被覆盖大于两次,直接记录sum3中, 此值即为此区间范围
t[o].sum2=X[t[o].r]-X[t[o].l];
else if(t[o].l==t[o].r-1) //如果当前节点为叶子节点 值就是0 因为我sum2维护的就是大于2次覆盖的长度
t[o].sum2=0;
else if(t[o].cnt==1) //如果当前节点被覆盖了一次 表示当前节点肯定被完全覆盖了一次 但不知哪些区域覆盖了两次 所以只需加上左右儿子覆盖1次的长度即为当前节点覆盖2次以上的长度
t[o].sum2=t[o<<1].sum+t[o<<1|1].sum;
else //此处表示节点没有被完全覆盖,所以结果只能由左右儿子覆盖2次的长度相加得出来
t[o].sum2=t[o<<1].sum2+t[o<<1|1].sum2;
}
void update(int o,int l,int r,edge x)
{
if(t[o].l==l&&t[o].r==r) //如果当前范围属于当前节点
{
t[o].cnt+=x.cnt; //加入cnt 表示次节点被覆盖的次数 方便pushup
pushup(o);
return ;
}
int mid=(t[o].l+t[o].r)>>1;
if(r<=mid)
update(o<<1,l,r,x);
else if(l>=mid)
update(o<<1|1,l,r,x);
else
{
update(o<<1,l,mid,x);
update(o<<1|1,mid,r,x);
}
pushup(o); //不要忘记
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n;
scanf("%d",&n);
mem(t,0);
double ans=0;
for(int i=1; i<=n; i++)
{
double x1,x2,y1,y2;
scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
p[2*i-1]=edge(x1,x2,y1,1); //存入每条平行x轴的边
p[2*i]=edge(x1,x2,y2,-1);
X[2*i-1]=x1; //存入每条边的左右端点
X[2*i]=x2;
}
sort(X+1,X+2*n+1); //按照x坐标从小到大排序
sort(p+1,p+2*n+1); //按照y坐标从小到大排序边
int m=unique(X+1,X+2*n+1)-X-1; //去重x坐标相同元素,方便离散化
build(1,m,1); //建树 这里主根的l=1,r=m,表示的是1-r这个区间所维护的值。所以叶子节点只可能是l=x,r=x+1,不存在l==r。
for(int i=1; i<=2*n-1; i++)
{
int l=lower_bound(X+1,X+m+1,p[i].x1)-X; //从下到上遍历每一条边,二分确定左右区间
int r=lower_bound(X+1,X+m+1,p[i].x2)-X;
update(1,l,r,p[i]); //更新
ans+=t[1].sum2*(p[i+1].h-p[i].h); //记录答案(当前被覆盖2次以上的长度乘上2条扫描线的差值)
}
printf("%.2lf\n",ans);
}
}