【學習筆記】掃描線


一.關於掃描線

  基礎是求周長並和面積並的算法。

  注意,掃描線是一條不存在的線。

  假設有一條掃描線從一個圖形的下方掃向上方(或者左方掃到右方),那么通過分析掃描線被圖形截得的線段就能獲得所要的結果。

 

二.掃描線求面積並(由於本人不會做圖,以下圖片均來自洛谷的題解)

  我們看一下這個東西。

  

  我們模擬一條掃描線,從下到上掃過整個平面。

  這條掃描線會在遇到橫向線段的時候停下來更新一些東西。那么整個圖形就可以找出四條線段。

  如圖:

  我們要更新什么呢?當然是計算線段的長度了。

  所以我們要記錄的第一種東西就確定了。是每條線段的左右端點坐標。

  然后我們把這些坐標放到一個數組,就叫X[]吧。

  這個東西是需要排序的。具體原因請往下看。

  那么我們考慮,在掃描線單調向上的過程中,怎么知道哪里有面積,哪里是空的呢?

  我們想到一個矩形有上底和下底,在掃描線單調向上的過程中,總是先遇到一個矩形的下底,再遇到上底,然后這個圖形的面積就被掃描線掃過了。

  所以我們要記錄第二個東西,給每條橫向線段賦上一個權值,如果是下底則賦為1,如果是下底則賦為-1,這樣掃描線掃有權值的部分就有我們要計算的面積。

 

  然后我們考慮面積並的問題,我們知道,兩個矩形相交的部分只能計算一次面積。

  兩個矩形相交,一個矩形的橫向邊上至少有1個另一個矩形邊上的點。

  那么如上圖X[1]和X[3]作為左右端點組成的線段可以分成兩部分,X[1]和X[2]組成線段和X[2]和X[3]組成線段。

  這樣這個圖形就被橫向分成了4條橫向邊3部分,縱向分成4條縱向邊3部分。

  但是掃描線是從下往上掃的,只能處理橫向邊的分割,縱向邊怎么辦呢?

  

  這時我們可以想到使用線段樹。

 

  我們用線段樹的每個節點來儲存一條線段,這條線段不一定是一組左右端點截成的,可能是左-左端點或右-右端點,也可能是非同組的左右端點。

  如下圖,我們可以對這個圖形像這樣建立一棵線段樹。

  對於上面那句話大家可以通過上圖很明顯地看出來。

  當一條線段被掃描線掃到的時候,立即更新線段樹每個節點維護的線段的覆蓋長度和權值。

  比如掃到最下面這條線段的時候,線段樹1,2節點維護的線段覆蓋長度和權值就會被更新。

  掃到下數第二條線段的時候,1,2,3,5,6節點維護的線段覆蓋長度和權值就會被更新。

  那么不難看出線段樹所維護的左右節點實際上是線段的編號,另外維護線段覆蓋長度和權值。

  這樣掃描線掃有權值的部分就有我們要計算的面積,那么就更新線段覆蓋長度。

  那么對於每一條線段我們也記錄它的左右端點和縱坐標以及權值,按照縱坐標優先升序排序,保證掃描線從下向上掃。

  

  還剩下一個問題,X[]數組。

  不難看出這個X[]數組排序的意義是為了方便在線段樹更新節點線段覆蓋長度的時候直接減就行了。

  另外,相同的X[]我們其實只需要一個就夠了,所以我們要離散化處理。

  這樣最后就可以進行計算面積了。

  S=Σ線段覆蓋長度*掃過的高度,即縱坐標的差。

 

  代碼如下:(我覺得上面沒理解透徹的同學看下代碼也應該理解得差不多了吧)

  題目:洛谷【模板】掃描線

#include<bits/stdc++.h>
using namespace std; struct Segment//從下向上的掃描線 那么就是橫向的線段 
{ long long l,r,h;//每一條線段的左端點坐標 右端點坐標 縱坐標。 //其中縱坐標的意義是在讀到它對應的線段 也就是對於一個矩形的下底 讀到它的上底時更改val 
    int val;//val的意義就是在下底使這一個線段權值加1 表示掃描線向下掃的時候 保證這條線段的下方是覆蓋的 //那么在遇到上底時權值為-1 也就是說覆蓋結束了 //對於整個平面 掃描線所加的面積就是掃過的 並且有權值的位置 
    bool operator < (const Segment &k) const { return h<k.h; }//重載運算符 使得縱坐標小的線段 也就是每個矩形的下底優先被掃描線讀到 
}Seg[800010];//空間要開足夠大 每個矩形最少有兩條線段 所以最少應該開兩倍 
struct SegTree//線段樹部分
{ int l,r;//線段樹的左右節點 分別用來存儲 一條橫向線段的編號 //這里注意 線段樹每個節點存儲的不是線段的左右端點 而是對於一對左右端點截出的線段 //並且 這里的左右節點並不嚴格在輸入時對應一條線段 但它們之間一定是有一條線段
            /*就比如說 3號節點所存的線段可能是第二條線段的右端點和第三條線段的右端點所截成的線段 因為第二條線段和第三條線段的縱坐標相同 所以它們有交集但不完全重合 可能會出現這樣的情況*/ 
    int sum;//這里的sum表示線段樹節點的權值 也就是說被覆蓋的次數 //但由於是求面積並 所以被覆蓋一次還是兩次並沒有區別 只是有權值和沒權值的區別 //有權值的線段 在掃描線掃過的時候就會計算面積 否則就不會計算面積 
    long long len;//這是每一個線段樹節點所代表的線段的長度 計算面積時使用 
}Tree[1600010];//空間要開足夠大 一個線段樹四倍 最好開到八倍會好一點 
long long X[800010]; long long n,x1,x2,yy,y2,ans; void Build(int k,long long l,long long r) { Tree[k].l=l; Tree[k].r=r; Tree[k].sum=0;//掃描線沒來之前沒有權值 權值變更是在掃描線到達某一條線段時修改 
    Tree[k].len=0;//同上 
    if(l==r) { return; } int mid=l+r>>1; Build(k<<1,l,mid); Build(k<<1|1,mid+1,r); } void pushup(int k) { int l=Tree[k].l,r=Tree[k].r; if(Tree[k].sum)//這就是上面所說的 只有被覆蓋和沒被覆蓋的區別
 { Tree[k].len=X[r+1]-X[l];//如果線段被掃描線掃到了 則更新掃到的線段長度 
 } else { Tree[k].len=Tree[k<<1].len+Tree[k<<1|1].len;//如果沒被掃到 則從以前掃過的線段更新 
 } } void update(int k,long long L,long long R,int val)//當掃描線掃到一條線段 執行此操作 L,R分別記錄線段的左右端點坐標 val代表這是下底還是上底
{ int l=Tree[k].l,r=Tree[k].r; if(X[r+1]<=L || R<=X[l])//要更新的線段不屬於這個線段樹節點 算是個剪枝吧 
 { return; } if(L<=X[l] && X[r+1]<=R) { Tree[k].sum+=val;//更新 如果是下底 那么sum就有了值 代表掃描線往上的地方需要計算面積 //如果是上底 那么sum不一定沒有值 只是減少 代表它對應的矩形面積計算完畢
 pushup(k); return; } //由於有剪枝 我們就不用判斷了 直接修改子樹就好了
    update(k<<1,L,R,val); update(k<<1|1,L,R,val); pushup(k); } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%lld%lld%lld%lld",&x1,&yy,&x2,&y2); X[2*i-1]=x1,X[2*i]=x2; Seg[2*i-1]=(Segment){x1,x2,yy,1};//記錄線段的信息 
        Seg[2*i]=(Segment){x1,x2,y2,-1};//按照這個操作 開兩倍是正常的 
 } n<<=1;//線段數是矩形數二倍 
    sort(Seg+1,Seg+n+1);//按照縱坐標升序排序 
    sort(X+1,X+n+1);//相同的橫坐標我們只需要一次 
    int cnt=unique(X+1,X+n+1)-X-1;//因此通過離散化來確定不同橫坐標的數量 
    Build(1,1,cnt-1);//這里cnt要-1 因為我們cnt代表橫坐標的數量 //而線段樹存儲的是線段數量 根據植樹問題 線段數=點數-1
    for(int i=1;i<n;i++) //最后一條邊不需要計算和更新了吧
 { update(1,Seg[i].l,Seg[i].r,Seg[i].val); ans+=Tree[1].len*(Seg[i+1].h-Seg[i].h);//計算面積
 } printf("%lld",ans); return 0; }
View Code

 

三.掃描線求周長並

  待補充~

  其實可以理解一下 是掃描線左右掃一次和上下掃一次~

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM