自由堆叠的屋顶

74 篇文章 0 订阅
44 篇文章 1 订阅

自由堆叠的屋顶

时间限制(普通/Java):1000MS/3000MS          运行内存限制:65536KByte
总提交: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个以上互相重叠的面积。
这。。。

求助度娘。才得知是新的方法。可以用线段树做。cool!

方法①:直接离散化。关于离散化,不做赘述。
离散化x,y坐标的值,排序,去重复。之后判断相邻的是否在覆盖的面积内,再求面积和。从这里发现了一些规范的写法。
实则分割成若干小的矩形,求面积和。方法很简明,实现也比较方便。科学的来说,类似枚举。但是在矩形数量较多的情况下就不适合了。

#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;
}



未完。。。
有很多细节!带更详细的理解后再写。。
线段树+离散化:因为在线段树中l和r都是int型数,而其实真正的数是double型的,所以可以离散化,l和r只是对应了相对的范围。


方法③:写的较好的离散化+线段树


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;
}

花了好久,终于搞定了!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值