扫描线(计算几何)

semi-AFO 选手的 DS 记录(

您将在这里见到最垃圾的扫描线写法.

1. 面积

扫描线本身还是很好理解的. 偷一张图 (图源 OI-wiki)

下面的 \(cnt\) 表示对应区域被矩形覆盖的次数. 容易发现只要 \(cnt>0\) 对应的区域就会被计算到.
具体地, 我们用从下往上扫描, 遇到一个矩形的下边就对 \(cnt\) 区间加 \(1\), 上边就区间减 \(1\). 与此同时, 我们维护 \(cnt>0\) 的位置的总长度. 这样每一段扫描的面积就是这个总长度乘上扫描的高度差.

大体思路就说完了! 下面是具体实现. 先不要管线段树怎么写, 主体部分是这样的:

#define int long long//不开 long long 见祖宗
const int maxn=100010;
struct scan{int l,r,h,tag;}lines[maxn<<1];
//l,r 为左右端点的 x 座标, h 为 y 座标, tag=1/-1 表示下/上边
//温馨提示: 二倍空间.
bool cmp(scan s,scan ss)//按照 y 座标排序
{
	if(s.h!=ss.h)return s.h<ss.h;
	return s.tag>ss.tag;//注意 tag 的比较顺序. 对于面积无所谓, 但之后算周长这是很重要的细节
}
int n,ans;
int xcnt,xaxis[maxn<<1];//x 座标的范围很大, 所以我们要离散化. 这些都是离散化用的.
map<int,int> mp;
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)//简单读入
	{
		int x=read(),y=read(),xx=read(),yy=read();
		lines[2*i-1]=(scan){x,xx,y,1};
		lines[2*i]=(scan){x,xx,yy,-1};
		xaxis[2*i-1]=x;xaxis[2*i]=xx;
	}
	sort(xaxis+1,xaxis+2*n+1);
	xcnt=unique(xaxis+1,xaxis+2*n+1)-xaxis-1;
	for(int i=1;i<=xcnt;i++)mp[xaxis[i]]=i;//以上为离散化.
	sort(lines+1,lines+2*n+1,cmp);//扫描线排序
	build(1,1,xcnt-1);//线段树建树, 注意线段树维护的是端点之间的区间的值, 要做好对应关系
	for(int i=1;i<=2*n;i++)
	{
		ans+=getlen()*(lines[i].h-lines[i-1].h);//算一下面积
		modify(1,mp[lines[i].l],mp[lines[i].r]-1,lines[i].tag);//区间加 1/-1, 上面说过
	}
	printf("%lld\n",ans);
	return 0;
}

很简单吧.
然后你发现问题在于线段树怎么维护 \(cnt>0\) 的位置对应的长度和.
很多人用奇怪的类似标记永久化的操作进行维护. 不过我是无脑选手, 所以我 (参考某篇题解) 这样做:
维护一下区间的 \(\min\) 和值为 \(\min\) 对应的长度和. 这样如果 \(\min=0\) 就把 \(\min\) 的长度和减掉即可.
具体实现如下 (你会发现并不需要维护 \(cnt\) 的值)

struct point{int l,r,minn,minlen,add;}tree[maxn<<3];//2*4=8 倍空间
inline void pushup(int x)
{
	int lson=x<<1,rson=lson|1;
	tree[x].minn=min(tree[lson].minn,tree[rson].minn);
	tree[x].minlen=0;
	if(tree[x].minn==tree[lson].minn)tree[x].minlen+=tree[lson].minlen;
	if(tree[x].minn==tree[rson].minn)tree[x].minlen+=tree[rson].minlen;//合理 pushup
}
inline void pushadd(int x,int k)
{
	tree[x].add+=k;
	tree[x].minn+=k;
}
inline void pushdown(int x)
{
	if(tree[x].l==tree[x].r)return;
	int lson=x<<1,rson=lson|1;
	if(tree[x].add!=0)
	{
		pushadd(lson,tree[x].add);
		pushadd(rson,tree[x].add);
		tree[x].add=0;
	}
}
void build(int x,int l,int r)
{
	tree[x]=(point){l,r,0,0,0};
	if(l==r)
	{
		tree[x].minlen=xaxis[tree[x].r+1]-xaxis[tree[x].l];//初始化对应的长度
		return;
	}
	int mid=(tree[x].l+tree[x].r)>>1,lson=x<<1,rson=lson|1;
	build(lson,l,mid);build(rson,mid+1,r);
	pushup(x);
}
void modify(int x,int l,int r,int k)
{
	pushdown(x);
	if(l<=tree[x].l&&r>=tree[x].r){pushadd(x,k);return;}
	int mid=(tree[x].l+tree[x].r)>>1,lson=x<<1,rson=lson|1;
	if(l<=mid)modify(lson,l,r,k);
	if(r>mid)modify(rson,l,r,k);
	pushup(x);
}
inline int getlen()
{
	if(tree[1].minn>0)return xaxis[tree[1].r+1]-xaxis[tree[1].l];
	else return xaxis[tree[1].r+1]-xaxis[tree[1].l]-tree[1].minlen;//如果 min 为 0 就把 minlen 减掉
}

总的代码把两段拼一起就行了(

2. 周长

把面积的代码稍微改改就行(
只需要注意下面几个点.

  1. 具体计算方法

最简单的做法 (就是懒得多维护东西了) 是横竖分别扫一遍, 每次统计平行的边长.
你发现每次对周长的贡献就是相邻两次扫描线长度的差的绝对值. 所以就做完了!

//以计算平行于 x 轴的边长和为例
int lastlen=0;
for(int i=1;i<=2*n+1;i++)//记得把两端的也给算上
{
	int now=getlenx();
	ans+=llabs(now-lastlen);
	lastlen=now;
	if(i<2*n+1)modify(1,mpx[lines[i].l],mpx[lines[i].r]-1,lines[i].tag);
}
  1. 一个重要细节

实际上上面说过了. 就是这里对于 \(tag\) 顺序的规定:

struct scan{int l,r,h,tag;}lines[maxn<<1],lines2[maxn<<1];
bool cmp(scan s,scan ss)
{
	if(s.h!=ss.h)return s.h<ss.h;
	return s.tag>ss.tag;//这里让 tag 是 1 的放在 -1 前面
}

这是因为对于两个矩形, 如果一个的下边恰好与另一个的上边重合, 就不能急着把原来的减掉, 要不然会把中间那条多算两遍.

经典样例:

2
0 0 4 4
0 4 4 8

你的程序应当输出 24.

总代码懒得放了(

扫描线好像还能搞三角形面积并等奇怪操作, 但是我的评价是不如自适应 Simpson.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章