線段樹

adsd45666發表於2024-05-25

線段樹

線段樹,一種拿來解決 \(RMQ\) 與區間和問題的高效的資料結構,對於這兩種問題,線段樹均能在 \(O(mlog_{2}n)\) 的時間複雜度內解決。在這兩個基礎的應用場景的基礎上,線段樹還發展出了各種豐富的應用。

總的來說,線段樹是一個應用廣泛,功能強大的資料結構,本章將介紹其概念及基本操作。

本章將包括:

目錄
  • 線段樹
    • 一 概念
      • (1.原理
      • (2.分析
    • 二 實現
      • (1.建樹
      • 關於區間問題:
      • (2.區間修改
      • (3.區間查詢
    • 三 例題

一 概念

概括的說,線段樹可以理解為“分治法+二叉樹結構+ \(Lazy-tag\) 技術”。

(1.原理

線段樹是分治法與樹的有機結合,可將線段樹上的每個節點視為“線段”(或“區間”),區間是由分治得到的,下圖為一棵表示區間 \([1,10]\) 的線段樹:

(2.分析

由圖可以看出其有幾點基本特徵:

​ 1.用分治法自頂向下建立,每次分治左右子樹各一半。

​ 2.每個節點都表示一個區間 \([L,R]\) 葉節點 \(L==R\) ,非葉結點 \(L<R\)

​ 3.除最後一層其它層都是滿的。

對於每個區間 \([L,R]\) 區間都有:

​ 若 \(L \ne R\) ,即 \([L,R]\) 不為葉子結點;則可將其分為 \([L,M]\)\([M + 1,R]\) 兩部分,其中 \(M=(L+R)/2\) ;

線段樹節點(即線段)表示的值,可以為線段上的區間和,也可定義為區間最值或其他要求值,這也是線段樹為什麼靈活多變的原因

由上述條件可知,線段樹適合解決的問題的特徵是:大區間的解可以由小區間的解合併而來。

二 實現

線段樹的程式碼實現。

(1.建樹

分治建樹,順帶維護線段;

int ls(int p){return p<<1;}    //左兒,2*n
int rs(int p){return p<<1|1;}  //右兒,2*n+1
int a[maxn],tree[maxn];        //a陣列儲存數列元素,tree樹組表示一個線段區間的值

void push_up(int p){           //維護線段
    return tree[p]=tree[ls(p)]+tree[rs(p)];
    //若題目為求最小值,則此處為 tree[p]=min(tree[ls(p)],tree[rs(p)]);
}

void build_tree(ll p,ll pl,ll pr){//p為節點編號,其指向區間[pl,pr]
    if(pl==pr){
        tree[p]=a[pl];          //葉子結點,賦值
        return;
    }
    ll mid=(pl+pr)>>1;          //分治,折半
    build_tree(ls(p),pl,mid);   //左子樹
    build_tree(rs(p),mid+1,pr); //右子樹
    push_up(p);                 //從下往上維護線段樹陣列
}

關於區間問題:

對於區間問題,我們採用與分治一樣的手段,(即:大區間的解由小區間的解合併而來)。具體是怎樣操作呢?模版如下

//在遞迴函式中
void mo(int l,int r,int p,int pl,int pr){
//要進行__操作的區間[l,r],現在查詢到p線段[pl,pr];
	if(l<=pl&&r>=pr){      //[pl,pr]包含於[l,r]或[pl,pr]等於[l,r]
   		//todo 即進行操作
    	return;
    }
}

但如果你對分治掌握的並不好(像我一樣),理解起來可能有一點難度,用下圖來解釋一下(掌握的可以跳過)

上圖即為 l<=pl&&r>=pr 的情況。當然,情況不唯一,我們可以簡單的理解為區間 \([L,R]\) 由三個小區間 \([L,pl]\)\([pl,pr]\)\([pr,R]\) 組成,由是,對於區間 \([L,R]\) 的操作便可由三個小區間的解合併而來。

(2.區間修改

首先,我們需要引入 \(Lazy-tag\) 的概念:技如其名,懶標記,指在進行區間修改時,不一次執行到底,而是打下標記,當用到時,再進行修改。(不用就不修改,為我們貪心到了不少時間複雜度)

int tag[maxn];                                 //tag陣列用來儲存懶標記
void add_tag(int p,int pl,int pr,int d){       //新增標記d
    tag[p]+=d;
    tree[p]+=d*(pr-pl);
}

void push_tag(int p,int pl,int pr){           //維護懶陣列標記  
    if(tag[p]){                               //之前留下的懶標記,為了不被覆蓋,向下傳給子樹
        int mid=(pl+pr)>>1;
        add_tag(ls(p),pl,mid);
        add_tag(rs(p),mid+1,pr);
        tag[p]=0;                             //標記被傳走
    }
}

void up_date(int l,int r,int p,int pl,int pr,int d){
    if(l<=pl&&r>=pr){
        add_tag(p,pl,pr,d);                     //打上懶標記,畢竟我是懶人
        return;
    }
    push_down(p,pl,pr);                        //如不能覆蓋,把tag傳給子樹
    ll mid=(pl+pr)>>1;
    if(l<=mid) updata(l,r,ls(p),pl,mid,d  );    //遞迴左子樹
    if(r>mid)  updata(l,r,rs(p),mid+1,pr,d);    //遞迴右子樹
    push_up(p);                                 //維護線段樹
}

程式碼如上所示,懶標記不止能記錄加法,如乘,除,乘方,開方,異或等。都可以以懶標記的形式儲存,但當存在多個不同的懶標記時,注意懶標記的處理順序,順序不當可能會導致精度出現差異。

當然,對於單點修改問題,區間修改也可以兼任,只需查詢時讓 \(l=r\) 即可。

(3.區間查詢

int query(int l,int r,int p,int pl,int pr){
    if(l<=pl&&r>=pr){
        return tree[p];
    }
    push_down(p,pl,pr);
    int res=0,mid=(pl+pr)>>1;
    if(l>pl) res+=query(l,r,ls(p),pl,mid);
    if(p<pr) res+=query(l,r,rs(p),mid+1,pr);
    return res;
}

此為區間求和的的程式碼,挨個加起來,沒什麼好說的。

三 例題

  1. 3372 【模板】線段樹

最經典的班子題,區間加+區間求和

  1. P3870 [TJOI2009] 開關

對於線段樹的另一種應用方式,好好利用異或,此題會簡單不少

  1. P2357守墓人

花哨了一點,仔細一看,還是區間加+區間求和的版子

相關文章