掃描線(計算幾何)

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.

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