ACM學習筆記:線段樹


title : 線段樹
date : 2021-8-15
tags : ACM,數據結構

 

線段樹

線段樹基礎

首先上個板子來復習一下線段樹的基本寫法。

//基礎板 P3372 【模板】線段樹 1
#include<bits/stdc++.h>
using namespace std;
int n,m,l,r,k,q;
long long arr[100005],tree[270000],lazy[270000];

void build(int node,int l,int r){  //建樹
if(l==r){
tree[node]=arr[l];
return;
}
int mid=(l+r)/2;
build(node*2,l,mid); //左區間建樹
build(node*2+1,mid+1,r); //右區間建樹
tree[node]=tree[node*2]+tree[node*2+1]; //區間和
}

void pushdown(int node,int start,int end){ //下傳操作
int mid=(start+end)/2;
if(lazy[node]){
tree[node*2]+=lazy[node]*(mid-start+1); //更新區間和
tree[node*2+1]+=lazy[node]*(end-mid);
lazy[node*2]+=lazy[node]; //懶標記下傳
lazy[node*2+1]+=lazy[node];
}
lazy[node]=0;
}

void update(int node,int start,int end,int l,int r,int c){ //更新操作
if(l<=start&&end<=r){ //如果區間在更新范圍內,直接標記返回
tree[node]+=(end-start+1)*c; //區間和加上Len倍的c
lazy[node]+=c; //打標記
return;
}
int mid=(start+end)/2;
pushdown(node,start,end); //下傳
if(l<=mid){update(node*2,start,mid,l,r,c);} //更新左區間
if(r>mid){update(node*2+1,mid+1,end,l,r,c);}//更新右區間
tree[node]=tree[node*2]+tree[node*2+1];  //pushup
}

long long query(int node,int start,int end,int l,int r){ //查詢操作
int mid=(start+end)/2;
if(l<=start&&end<=r) return tree[node]; //如果在查詢范圍內,直接返回
pushdown(node,start,end);
long long sum=0;
if(l<=mid) sum=query(node*2,start,mid,l,r); //查詢左區間
if(r>mid) sum+=query(node*2+1,mid+1,end,l,r); //查詢右區間
return sum;
}

int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&arr[i]);
build(1,1,n);  
while(m--){
scanf("%d%d%d",&q,&l,&r);
if(q==1){
scanf("%d",&k);
update(1,1,n,l,r,k); //區間修改
}else printf("%lld\n",query(1,1,n,l,r)); //區間查詢
}
return 0;
}
延遲標記

延遲標記,也叫lazy tag,是在區間中新增一個標記,在下一次訪問該區間時,向左右區間下放(pushdown)節點的標記,以便完成區間修改+區間詢問。

 

區間染色

例題:POJ 2528 Mayor's posters

區間離散化

對於區間[1,5],[2,7],[7,100],[3,1e7],我們肯定不能直接對區間[1,1e7]進行修改,而是應該先進行排序並離散化,1->1,2->2,3->3,5->4,7->5,100->6,1e7->7,之后區間可以表示為[1,4],[2,5],[5,6],[3,7]。然而這樣的表示實際上擴大了訪問的區間,因此我們要在間隔大於1的兩個元素中間再加數字,正確的表示方法如下:

$$
x[1]=1,x[2]=2,x[3]=3,x[4]=4,x[5]=5,x[6]=6,x[7]=7,x[8]=100,x[9]=101,x[10]=1e7
$$

 

發現了新增的節點x[4],x[6]和[x8],這有什么用呢?

比如我要塗色[1,3]->顏色1,[5,7]->顏色2,加點前[1,3]->顏色1,[4,5]顏色2,可以發現最終兩種顏色把[1,5]覆蓋掉了,事實上中間還有一片(3,4)顏色未處理;我們增加節點后的效果是塗掉[1,3],[5,7],那么數顏色數量的時候就能答案就修正了。

//Mayor's posters
#include<iostream>
#include<vector>
#include<algorithm>
#define MID int mid=(p->l + p->r)>>1
using namespace std;
struct Post{ // 海報
   int l,r;
} pst[10100];
int nodenum,cnt,ans;
int t,n,hs[10000010];
vector<int>vt;

struct Node{
   int l,r;
   bool full; // 區間[l,r]是否被完全覆蓋
   Node *ls, *rs;
}tr[1000000];

void buildTree(Node *p, int l, int r){ //建樹
   p->l=l;
   p->r=r;
   p->full=0; //初始化節點
   if(l==r)return; //如果是葉子,直接返回
   nodenum++;
   p->ls=tr+nodenum; //建立左右子樹
   nodenum++;
   p->rs=tr+nodenum;
   MID;
   buildTree(p->ls,l,mid); //左區間建樹
   buildTree(p->rs,mid+1,r); //右區間建樹
}
bool check(Node *p,int l,int r){ //判斷該區間是否有覆蓋
   if (p->full) return false; //已經被覆蓋了,這份海報就看不到了
   if (p->l==l&&p->r==r){
       p->full=1; //覆蓋此區間
       return true;
  }
   bool res;
   MID;
   if(r<=mid) res=check(p->ls,l,r); //全在左區間,找左子樹
   else if(l>mid) res=check(p->rs,l,r);
   else{
       bool b1=check(p->ls,l,mid);
       bool b2=check(p->rs,mid+1,r);
       res=b1||b2; //其中一點沒覆蓋即可
  }
   if (p->ls->full&&p->rs->full){
  p->full=1;//如果左右區間都被覆蓋,那么這個區間也被覆蓋了
}
   return res;
}

signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
   cin>>t;
   while(t--){
  cin>>n;
  nodenum=0,cnt=0,ans=0; //清空數據
       for(int i=0;i<n;i++) {
      cin>>pst[i].l>>pst[i].r;
      vt.push_back(pst[i].l); //把區間端點放入容器
      vt.push_back(pst[i].r);
      }
       sort(vt.begin(),vt.end());//排序
       vt.erase(unique(vt.begin(),vt.end()),vt.end()); //去重
       for(int i=0;i<vt.size();i++){
           hs[vt[i]]=cnt++; //記錄元素所在數的節點編號
           if(i<vt.size()-1){
               if(vt[i+1]-vt[i]>1) cnt++; //中間再插一個點
          }
      }
       buildTree(tr,0,cnt);//開始建樹
       for(int i=n-1;i>=0;i--){ //這里倒序,要從沒被覆蓋的開始數
           if(check(tr,hs[pst[i].l],hs[pst[i].r])){
          ans++; //可見海報增加
}
      }
       cout<<ans<<endl;
  }
   return 0;
}

 

區間第K大

由於篇幅問題這個問題另開一篇主席樹的博文講吧。

 

掃描線線段樹

掃描線算法可用於解決多個矩形圍成的周長和面積問題。

掃描線算法

用一根線對圖像從下往上進行掃描,掃描到邊的時候對答案進行計算。相當於給妙計分層,然后計算每一層的面積,最后匯總到答案當中。

矩陣面積並

給定多個矩形,邊一定平行於x或y軸。求所有矩形的面積之和。

$$
(1)每個矩形的上下邊加入掃描線並標記,下邊標記為1,上邊標記為-1\\ (2)讓掃描線從從低到高排序,而矩形的左右邊從小到大排序\\ (3)考慮數據范圍,我們進行離散化處理\\ (3)建立線段樹,那么區間可表示為區域的y1,y2,以及長度。\\ (4)遍歷所有相鄰左右邊,每次通過線段樹詢問出高度,然后面積相加。\\
$$

 

注意我們這里要維護的是線段長度而非點的值,所以我們要對線段樹進行修改,即左孩子的右值=右孩子的左值,這樣才能使維護不出現縫隙。

// luoguP5490 【模板】掃描線
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+7;

struct L{
ll x,y1,y2,flag;
L(ll X=0,ll Y1=0,ll Y2=0,ll T=0){
x=X,y1=Y1,y2=Y2,flag=T;
}
}line[N<<1];

struct node{
int l,r;
ll len;
ll lazy;
}t[N<<2];

int n,cnt;
ll seg[N];
map<ll,int>val;

ll len(int p){
//獲得區域p的高度,即兩掃描線相減
return seg[t[p].r+1]-seg[t[p].l];
}

void update(int p){
if(t[p].lazy) t[p].len=len(p); //如果該區間被標記過
else if(t[p].l==t[p].r) t[p].len=0; //區間長度為0
else t[p].len=t[p<<1].len+t[p<<1|1].len; //兩區間相加
}

void build(int p,int l,int r){
t[p].l=l; //建樹時只需記錄區間左右就可以了
t[p].r=r;
if(l==r) return;
int mid=l+r>>1;
build(p<<1,l,mid); //對左邊建樹
build(p<<1|1,mid+1,r); //對右邊建樹
}

void change(int p,int l,int r,int k){
if(l<=t[p].l&&t[p].r<=r){ //如果區間在所求上下邊中
t[p].lazy+=k; //打上標記
update(p); //更新區間長度
return;
}
int mid=t[p].l+t[p].r>>1;
if(l<=mid) change(p<<1,l,r,k); //向下更新
if(r>mid) change(p<<1|1,l,r,k);  //向上更小
update(p); //最后要更新整個區間
}

bool cmp(L a,L b){
return a.x<b.x; //按照x坐標排序
}

signed main(){
ios::sync_with_stdio(0); //讀入優化
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
ll x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
seg[++cnt]=y1; //加入掃描線
line[cnt]=L(x1,y1,y2,1); //加入豎線
seg[++cnt]=y2;
line[cnt]=L(x2,y1,y2,-1);
}
sort(line+1,line+cnt+1,cmp); //對豎線排序
sort(seg+1,seg+1+cnt); //掃描線從低到高排序
int m=unique(seg+1,seg+1+cnt)-(seg+1); //去重
for(int i=1;i<=m;i++) val[seg[i]]=i; //離散化
build(1,1,m); //建樹
ll ans=0; //記錄答案
for(int i=1;i<cnt;i++){ //對於cnt-1條豎邊
int x=val[line[i].y1],y=val[line[i].y2]-1; //區域的上下邊
change(1,x,y,line[i].flag); //查詢並更新區域的高
ans+=t[1].len*(line[i+1].x-line[i].x); //累加區域面積
}
printf("%lld",ans); //輸出答案
return 0;
}

 

動態開點

動態開點線段樹可以避免離散化。

如果權值線段樹的值域較大,離散化比較麻煩,可以用動態開點的技巧。

省略了建樹的步驟,而是在具體操作中加入結點。

 

參考資料

https://oi-wiki.org/geometry/scanning/

https://mirasire.xyz/2019/11/17/SMX/

https://wmathor.com/index.php/archives/1176/

https://www.bilibili.com/video/BV1FU4y1t79g


免責聲明!

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



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