线段树+扫描线入门(hdu1542+hdu1828)

题目链接 hdu1542 hdu1828

求矩形面积并(hdu1542)

我们可以在y轴上用扫描线,如果是矩形的下边就让该段区间+1,如果是上边就让区间-1。那么维护x轴上的区间操作就可以用线段树了。对于答案统计,就是后一条扫描线与当前扫描线的y坐标的差值乘上当前区间下覆盖的长度。

几个注意点

  1. 坐标有浮点数,范围也比较大,需要离散化处理(可以用unique或者map或者直接for循环去重)。
  2. 线段树上每个节点维护的区间【l,r】对应坐标轴上的【l,r+1】。可以考虑两个区间【1,2】和【2,3】,如果都是闭区间,覆盖数cover就是2,而实际上这是两个连续区间,cover应该是1,所以用左闭右开来存(很多区间题都要化闭为开)。
  3. 不需要lazy标记,跟维护sum,max不一样,lazy没法下放。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int N = 205;
double x[N], len[N<<2];
int cover[N<<2];
struct node
{
	double l, r, y;
	int flag;
	friend bool operator < (node a,node b) //扫描线按y轴坐标排序后,从小到大依次扫
	{
		return a.y < b.y;
	}
}s[N];
void pushup(int p,int l,int r)
{
	if(cover[p]) len[p] = x[r+1] - x[l];  //如果当前区间被线段覆盖,长度=区间长度
	else if(l==r) len[p] = 0; //叶子结点
	else len[p] = len[p<<1] + len[p<<1|1];   //儿子结点信息向上更新
}
void update(int p,int l,int r,int x,int y,int val)
{
	if(r<x||l>y) return;
	if(x<=l&&y>=r)
	{
		cover[p] += val;
		pushup(p,l,r);  //不用lazy标记
		return;
	}
	int mid = (l+r) >> 1;
	update(p<<1,l,mid,x,y,val);
	update(p<<1|1,mid+1,r,x,y,val);
	pushup(p,l,r);
}
int main()
{
	int n, kas = 0;
	while(~scanf("%d",&n)&&n)
	{
		double x1, y1, x2, y2;
		int m = 0, k = 1;
		for(int i=1;i<=n;i++)
		{
			scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
			x[m] = x1;  //存储横坐标去重
			s[m++] = {x1,x2,y1,1};  //存储扫描线
			x[m] = x2;
			s[m++] = {x1,x2,y2,-1};
		}
		sort(x,x+m);
		sort(s,s+m);
		for(int i=1;i<m;i++)  //离散化处理
		{
			if(x[i]!=x[i-1]) x[k++] = x[i];
		}
		memset(cover,0,sizeof(cover));
		memset(len,0,sizeof(len));
		double ans = 0;
		for(int i=0;i<m-1;i++)
		{
			int l = lower_bound(x,x+k,s[i].l) - x;
			int r = lower_bound(x,x+k,s[i].r) - x;
			update(1,0,k-1,l,r-1,s[i].flag); //维护的是[l,r-1]
			ans += len[1]*(s[i+1].y-s[i].y);  //统计答案 len[1]就是当前整个区间被线段覆盖的长度 乘以两个扫描线的高度差
		}
		printf("Test case #%d\nTotal explored area: %.2f\n\n",++kas,ans);
	}
	return 0;
}

求矩形周长并(hdu1828)

一种简单的方法就是x轴和y轴分别用扫描线做一次。但是其实可以只扫一维即可。
整体思想和求面积相似。但相较于面积,我们用线段树多维护三个信息,num:该区间上线段端点个数 ld:该区间左端点是否被覆盖 rd:该区间右端点是否被覆盖。对于答案统计,我们分成横向竖向两部分。

  1. 横向:这次区间被覆盖的长度与上次覆盖的长度差的绝对值
  2. 竖向:区间上线段端点数乘上下条扫描线与这条的高度差。端点数就是num,就相当于竖着的边数。ld和rd的作用就是用来更新num(可以考虑两段区间[1,2]和[3,4],本来合并后端点数num是4,但是两个区间是连续的,所以num-2。

几个注意点

  1. 数据比较小,而且都是整数,所以可以不用离散化。
  2. 同样不需要lazy下放,区间左闭右开。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const int N = 2e4 + 5;
struct node
{
	int l, r, y, flag;
	friend bool operator < (node a,node b)
	{
		return a.y < b.y;
	}
}s[N];
int cover[N<<2], len[N<<2], num[N<<2], x[N];
bool ld[N<<2], rd[N<<2];
void pushup(int p,int l,int r)
{
	if(cover[p]) //整段区间被覆盖
	{
		ld[p] = rd[p] = 1;
		len[p] = x[r+1] - x[l];
		num[p] = 2;
	}
	else if(l==r) //叶子结点
	{
		len[p] = num[p] = ld[p] = rd[p] = 0;
	}
	else  
	{
		ld[p] = ld[p<<1]; //左端点由左儿子更新
		rd[p] = rd[p<<1|1];  //右端点由右儿子更新
		len[p] = len[p<<1] + len[p<<1|1];
		num[p] = num[p<<1] + num[p<<1|1];
		if(ld[p<<1|1]&&rd[p<<1]) num[p] -= 2; //两个区间合并成连续区间的情况,端点-2
	}
}
void update(int p,int l,int r,int x,int y,int val)
{
	if(r<x||l>y) return;
	if(x<=l&&y>=r)
	{
		cover[p] += val;
		pushup(p,l,r);
		return;
	}
	int mid = (l+r) >> 1;
	update(p<<1,l,mid,x,y,val);
	update(p<<1|1,mid+1,r,x,y,val);
	pushup(p,l,r);
}
int main()
{
	int n;
	while(~scanf("%d",&n))
	{
		int x1, y1, x2, y2, m = 0, k = 1;
		for(int i=0;i<n;i++)
		{
			scanf("%d%d%d%d",&x1,&y1,&x2,&y2);	
			x[m] = x1;
			s[m++] = {x1,x2,y1,1};
			x[m] = x2;
			s[m++] = {x1,x2,y2,-1};
		}
		sort(x,x+m);
		sort(s,s+m);
		for(int i=1;i<m;i++)
		{
			if(x[i]!=x[i-1]) x[k++] = x[i];  //可以不离散化
		}
		for(int i=0;i<k;i++)
		{
			len[i] = cover[i] = rd[i] = ld[i] = num[i] = 0;  //memset
		}
		int ans = 0, last = 0;
		for(int i=0;i<m;i++)
		{
			int l = lower_bound(x,x+k,s[i].l) - x;
			int r = lower_bound(x,x+k,s[i].r) - x;
			update(1,0,k-1,l,r-1,s[i].flag);
			ans += num[1] * (s[i+1].y - s[i].y);  //竖线答案
			ans += abs(len[1]-last); //横线答案
			last = len[1];  
		}
		printf("%d\n",ans);
	}
	return 0;
}

总结

对于这种几何题,还是画图更好理解,画几个矩形,模拟一下扫描线的移动应该就很容易,最后要注意线段树的空间别开小了,不然会wa的很迷。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值