自由堆叠的屋顶
总提交:63 测试通过:20
描述
sed 同学最近突发奇想,认为伟大的建筑物的屋顶应该是“自由堆叠”出来的,他的设计方案是:将各种颜色的长方形建筑板材堆叠在一起,并保证各个板材长边、宽边均相互平行或在一条直线上,板材之间的重叠部分用连接装置固定在一起。
你的任务是计算这个“自由堆叠的屋顶”所覆盖的面积。sed 将会在屋顶平面上建立一个二维坐标系,提供给你每个长方形建筑板材左上角、右下角的坐标。为简化计算,这里忽略板材的厚度,假设它们都在同一个平面上。
输入
输入数据包含多组测试案例。
每组测试案例由N(0≤N≤100)开头,后续N行每行包含4个实数x1;y1;x2;y2 (0 <= x1 < x2 <= 100000建筑单位;0 <= y1 < y2 <= 100000建筑单位)。
(x1; y1) 、 (x2;y2)分别是长方形建筑板材左上角、右下角的坐标。
单独一个行输入0表示输入结束,无需处理。
输出
对于每个测试用例,输出以下信息: 第1行形如“Build #k”,这里k是测试用例序号(以1开始),第二行形如“Total area: a”,a是总覆盖面积,保留两位小数。
样例输入
2
10 10 20 20
15 15 25 25.5
1
10 10 20 20
0
样例输出
Build #1
Total explored area: 180.00
Build #2
Total explored area: 100.00
题目来源
南京邮电大学计算机学院首届ACM程序设计大赛(2009)
分析:离散化+线段树经典题目。
hdu 1542 (poj1151) Atlantis —— 多做题的优势体现出来了!Fighting!
一开始我还机智想求总的面积减去重叠的面积。任意两个重叠的面积还是好求的:
double sum = 0.0, chongdie = 0.0;
for(int i=0;i<n;i++)
{
scanf("%lf %lf %lf %lf",&x1[i],&y1[i],&x2[i],&y2[i]);
sum += (x2[i]-x1[i]) * (y2[i]-y1[i]);
}
for(int i=0;i<n-1;i++)
{
for(int j=i+1;j<n;j++)
{
if(min(x2[i],x2[j]) - max(x1[i],x1[j]) >= 0 &&
min(y2[i],y2[j]) - max(y1[i],y1[j]) >= 0)
{
chongdie += ( min(x2[i],x2[j]) - max(x1[i],x1[j]) ) *( min(y2[i],y2[j]) - max(y1[i],y1[j]) );
}
}
}
事实证明,有问题。因为如果任意3个或者更多的矩形之间有重叠的时候,这样直接减的做法是有问题的,忽略了3个以上互相重叠的面积。
#include<stdio.h>
#include<math.h>
#include<string.h>
#include<stdlib.h>
#define M 1000
const double eps = 1e-6;
//离散化坐标
typedef struct{
double x1,x2,y1,y2;
void set(double x1, double y1, double x2, double y2)
{this->x1 = x1; this->y1 = y1; this->x2 = x2; this->y2 = y2;}
}Rec;
Rec r[M];
bool contain[M][M];
double x[M],y[M];
int n,nx,ny;
int Cmp(const void *a, const void *b) // qsort必须这样写
{
return *(double *)a>*(double *)b ? 1 : -1;
}
int Find(double arr[], int low, int up, double v)
{
//二分查找,并且一定数组中一定存在与v相等的值
int mid = (low+up)>>1;
if(fabs(arr[mid]-v) < eps) return mid;
if(arr[mid] > v)
return Find(arr, low, mid-1, v);
else
return Find(arr, mid+1, up, v);
}
int main()
{
double x1, y1, x2, y2, ans;
int counts = 1, i, j, i1, i2, j1, j2, k;
while(scanf("%d",&n) && n != 0){
ans = 0.0;
for(nx=ny=0,i=0;i<n;i++){
scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
r[i].set(x1, y1, x2, y2);
x[nx++] = x1; x[nx++] = x2;
y[ny++] = y1; y[ny++] = y2;
}
//排序,进而离散化点
qsort(x, nx, sizeof(x[0]), Cmp);
qsort(y, ny, sizeof(y[0]), Cmp);
//消除相等的值,进行离散化
for(i=0,j=0;i<nx;i++){
if(fabs(x[i]-x[j]) < eps) continue;
x[++j] = x[i];
} nx=j;
for(i=0,j=0;i<ny;i++){
if(fabs(y[i]-y[j]) < eps) continue;
y[++j] = y[i];
} ny=j;
memset(contain, false, sizeof(contain));
for(i=0;i<n;i++){
i1 = Find(x, 0, nx, r[i].x1);
i2 = Find(x, 0, nx, r[i].x2);
j1 = Find(y, 0, ny, r[i].y1);
j2 = Find(y, 0, ny, r[i].y2);
for(j=i1;j<i2;j++){ // 标记覆盖的区域
for(k=j1;k<j2;k++)
contain[j][k] = true;
}
}
for(i=0;i<nx;i++){
for(j=0;j<ny;j++){
if(contain[i][j])
ans += (x[i+1]-x[i]) * (y[j+1]-y[j]);
}
}
printf("Build #%d\nTotal explored area: %.2f\n",counts++,ans);
}
return 0;
}
顾名思义,扫描法就是用一根想象中的线扫过所有矩形,在写代码的过程中,这根线很重要。方向的话,可以左右扫,也可以上下扫。方法是一样的,这里我用的是由下向上的扫描法。
如上图所示,坐标系内有两个矩形。位置分别由左下角和右上角顶点的坐标来给出。上下扫描法是对x轴建立线段树,矩形与y平行的两条边是没有用的,在这里直接去掉。如下图。
现想象有一条线从最下面的边开始依次向上扫描。线段树用来维护当前覆盖在x轴上的线段的总长度,初始时总长度为0。用ret来保存矩形面积总和,初始时为0。
由下往上扫描,扫描到矩形的底边时将它插入线段树,扫描到矩形的顶边时将底边从线段树中删除。而在代码中实现的方法就是,每条边都有一个flag变量,底边为1,顶边为-1。
用cover数组(通过线段树维护)来表示某x轴坐标区间内是否有边覆盖,初始时全部为0。插入或删除操作直接让cover[] += flag。当cover[] > 0 时,该区间一定有边覆盖。
开始扫描到第一条线,将它压入线段树,此时覆盖在x轴上的线段的总长度L为10。计算一下它与下一条将被扫描到的边的距离S(即两条线段的纵坐标之差,该例子里此时为3)。
则 ret += L * S. (例子里增量为10*3=30)
结果如下图
橙色区域表示已经计算出的面积。
扫描到第二条边,将它压入线段树,计算出此时覆盖在x轴上的边的总长度。
例子里此时L=15。与下一条将被扫描到的边的距离S=2。 ret += 30。 如下图所示。
绿色区域为第二次面积的增量。
接下来扫描到了下方矩形的顶边,从线段树中删除该矩形的底边,并计算接下来面积的增量。如下图。
蓝色区域为面积的增量。
此时矩形覆盖的总面积已经计算完成。 可以看到,当共有n条底边和顶边时,只需要从下往上扫描n-1条边即可计算出总面积。
/*
这里线段树是对x轴建树。
用y轴的垂直线划分矩形。
然后对于每个输入的矩形我们把它的上下两条边记录下来(包括这条边两个端点的x轴坐标,高度,上边或者下边等信息)。
依次从高度小的边开始,把这条边对应的x轴区域在线段树中覆盖。
当然这里矩形的下边是覆盖,上边是删除。
每插入一条边后由于pushup的作用,sum[1]记录了当前边插入后整个x轴被覆盖的长度!!!
然后用这条边所在的直线与紧邻它的上一条水平直线间的距离乘以sum[1]就是被夹在这两条水平线间的面积。不断累加就可以了;
*/
#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 1000
#define eps 1e-6
//线段树
struct segTree
{
double l, r, h; // x轴l,r。h为y轴坐标
int flag; // 上边or下边
}seg[MAX];
int cnt[MAX] = {0}; // 线段树中节点所对应的区间被覆盖的次数
double sum[MAX] = {0}; // 覆盖的区间长度
double X[MAX]; // x坐标
int Cmp(segTree a, segTree b)
{
return a.h < b.h;
}
/*
int cmp1(const void *a,const void *b)
{
segTree *p=(segTree *)a;
segTree *q=(segTree *)b;
if(fabs(p->x-q->x)<eps)
return p->flag-q->flag;
return p->x>q->x?1:-1;
}
qsort...
*/
int Find(double key, int n) // 二分查找 n个数
{
int l = 0, r = n-1;
while(l <= r)
{
int mid = (l+r) / 2;
if(fabs(X[mid] - key) < eps) return mid;
if(X[mid] < key)
l = mid+1;
else
r = mid-1;
}
//return -1; // 失败返回-1,不用,因为肯定找得到
}
void Update(int l,int r,int flag,int start, int end, int rt) // ★
{
if(l == start && r == end)
{
cnt[rt] += flag; // +1 / -1
if(cnt[rt] > 0) sum[rt] = X[end+1] - X[start];
else if(start == end) sum[rt] = 0;
else sum[rt] = sum[rt*2] + sum[rt*2+1]; // lChild + rChild
return ;
}
int mid = (start+end) / 2;
if(r <= mid) Update(l, r, flag, start, mid, rt*2);
else if(l > mid) Update(l, r, flag, mid+1, end, rt*2+1);
else
{
Update(l, mid, flag, start, mid, rt*2);
Update(mid+1, r, flag, mid+1, end, rt*2+1);
}
if(cnt[rt] > 0) sum[rt] = X[end+1] - X[start];
else if(start == end) sum[rt] = 0;
else sum[rt] = sum[rt*2] + sum[rt*2+1]; // lChild + rChild
}
int main()
{
double x1, y1, x2, y2;
int n, counts = 1;
while(scanf("%d",&n) && n != 0)
{
int len = 0; double ans = 0.0;
for(int i=0;i<n;i++)
{
scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
X[len] = x1; seg[len].l = x1; seg[len].r = x2; seg[len].h = y1; seg[len].flag = 1;
len ++;
X[len] = x2; seg[len].l = x1; seg[len].r = x2; seg[len].h = y2; seg[len].flag = -1;
len ++;
}
sort(X, X+len); // 注意sort 与 qsort 区别
sort(seg, seg+len, Cmp);
int len2 = 1;
for(int i=1;i<len;i++) // x轴去重
if(X[i] != X[i-1]) X[len2++]=X[i];
for(int i=0;i<len;i++)
{
int l = Find(seg[i].l, len);
int r = Find(seg[i].r, len) - 1; // -1 注意!
if(l <= r)
Update(l, r, seg[i].flag, 0, len-1, 1); // len-1 下标
ans += sum[1] * (seg[i+1].h-seg[i].h);
}
printf("Build #%d\nTotal explored area: %.2f\n",counts++,ans);
}
return 0;
}
有很多细节!带更详细的理解后再写。。
方法③:写的较好的离散化+线段树
build建立的线段树:
[1,4] --node[1]:l=1,r=4,ml=10.00,mr=25.50
(10~25.5)
/ \
[1,2] [2,4] --node[2](叶子节点) node[3]
(10~15) (15~25.5)
/ \
[2,3] [3,4] --node[6](叶子节点) node[7] 叶子节点(叶子节点)
(15~20) (20~25.5)
len = 10 / 15.5 / 10.5 分别* 邻近的x的差(5 / 5 / 5),再求和= ans
父节点的len = 子节点的len之和。
第一次加进去一根线之后的len即为update之后的node[1].len
先加进去line[1]:x=10,y1=10,y2=20,flag=1。要分开来分别插入node[1]的左右儿子里面。
此时,node[2]( [1,2] )的cover为1,len=5。node[6]( [2,3] )的cover为1,len=5。node[3]( [2,4] )的cover为0,len=5。
node[1].len=node[2].len+node[3].len= 5 + 5 = 10。
第二次加进去line[2]:x=12,y1=15,y2=25.5,flag=1。
此时,node[3]( [2,4] )的cover为1,len=10.5。所以node[1].len=node[2].len+node[3].len=5+10.5=15.5。
第三次加进去line[3]:x=20,y1=10,y2=20,flag=-1。
此时,node[2]( [1,2] )的cover为0,len=0,node[3]( [2,3] )的cover为1,len=10.5。
所以node[1].len=0+10.5=10.5。
结束~
这里的一种写法是将线段树节点的两端对应的double型数 定义在SegTree结构体里面,还有一种写法是不定义在里面,使用数组y来判断。方法一样。
另外,对于离散化,先排序,再取出相同的值。排序的时候,一种是sort,另一种是qsort
区别:qsort比较函数必须这样写:
int cmp(const void *a,const void *b) {}
当然,它们的调用方式也有区别。
#include<iostream>
#include<algorithm>
#include<math.h>
using namespace std;
#define MAX 1000
#define eps 1e-6
//比较好的线段树写法
//root=1开始
struct Line // 垂线,即扫描线
{
double x; double y1; double y2; // 竖线,左边1,右边-1
int flag;
}line[MAX];
struct SegTree
{
int l; int r; double ml; double mr; int cover; double len;
}node[MAX]; // 线段树节点
int cmp1(const void *a,const void *b)
{
Line *p=(Line *)a;
Line *q=(Line *)b;
if(fabs(p->x-q->x) < eps)
return p->flag - q->flag;
return p->x>q->x ? 1 : -1;
}
int cmp2(const void *a,const void *b)
{
return *(double *)a > *(double *)b?1:-1;
}
double y[MAX];
void build(int i, int left, int right)
{
node[i].l = left;
node[i].r = right;
node[i].ml = y[left];
node[i].mr = y[right];
node[i].cover = 0; // 初始化都为0
node[i].len = 0;
if(node[i].l + 1 == node[i].r) // 叶子节点
return ;
int mid = (left+right)/2;
build(i*2, left, mid);
build(i*2+1, mid, right); // 这里是mid:1~2 2~3
}
void cal(int i) // 计算len,注意覆盖问题
{
if(node[i].cover > 0)
node[i].len = node[i].mr - node[i].ml;
else if(node[i].r - node[i].l == 1)
node[i].len = 0;
else
node[i].len = node[i*2].len + node[i*2+1].len;
return ;
}
void update(int i,Line b) // 插入的根节点(i == 1),线段b
{
if(node[i].ml == b.y1 && node[i].mr == b.y2)
{
node[i].cover += b.flag; // 插入的线段匹配则此条线段的记录+flag
cal(i);
return ; // 插入结束返回
}
if(b.y2 <= node[i*2].mr) // 插入到左儿子
update(i*2, b);
else if(b.y1 >= node[i*2+1].ml) // 插入到右儿子
update(i*2+1, b);
else // 中点一定在两端之间,把待插线段分成两半分别插到左右儿子里面
{
Line tmp = b;
tmp.y2 = node[i*2].mr;
update(i*2, tmp);
tmp = b;
tmp.y1 = node[i*2+1].ml;
update(i*2+1, tmp);
}
cal(i);
return ;
}
int main()
{
int n, counts =1;
double x1, x2, y1, y2;
while(scanf("%d",&n) && n != 0)
{
double ans = 0.0;
int t = 1; // root == 1
for(int i=0;i<n;i++){
scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
line[t].x=x1; line[t].y1=y1; line[t].y2=y2; line[t].flag=1;//入边
y[t++]=y1;
line[t].x=x2; line[t].y1=y1; line[t].y2=y2; line[t].flag=-1;//出边
y[t++]=y2;
}
qsort(line+1, t-1, sizeof(line[0]), cmp1);
qsort(y+1, t-1, sizeof(y[0]), cmp2);
//sort(line, line+t, cmp);
//sort(y, y+t);
int j=1;
for(int i=1;i<t;i++){ // y轴坐标离散化,此处消去相同的y值
if(fabs(y[j]-y[i]) < eps) continue;
y[++j]=y[i];
}
build(1, 1, t-1); // 离散化之后
for(int i=1;i<t;i++)
{
ans += node[1].len * (line[i].x - line[i-1].x);
update(1, line[i]); // root == 1
}
printf("Build #%d\nTotal explored area: %.2f\n",counts++,ans);
}
return 0;
}
花了好久,终于搞定了!