<更新提示>
<第一次更新> 更新了基礎部分
<第二次更新>更新了\(lazytag\)標記的講解
<正文>
線段樹 Segment Tree
今天來講一下經典的線段樹。
線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間划分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
簡單的說,線段樹是一種基於分治思想的數據結構,用來維護序列的區間特殊值,相對於樹狀數組,線段樹可以做到更加通用,解決更多的區間問題。
性質
- 1.線段樹的每一個節點都代表了一個區間
- 2.線段樹是一棵二叉樹,具有唯一的根節點,其中,根節點代表的是整個區間\([1,n]\)
- 3.線段樹的每一個葉節點代表的是長度為\(1\)的元區間\([x,x]\)
- 4.對於每一個節點\([l,r]\),它的左兒子被定義為\([l,mid]\),右兒子被定義為\([mid+1,r]\)
如圖,這就是一棵維護了區間\([1,10]\)的線段樹。
我們還可以發現,線段樹層數為\(log_2n\)層,除去最后一層,線段樹是一棵完全二叉樹。
建樹 (build)
我們來考慮一下如何儲存並建立一棵線段樹。
由於線段樹是二叉樹,所以我們可以直接用數組存儲結點的編號,即對於節點\(x\)儲存在\(a[p]\)處,我們令\(x\)的左兒子儲存在\(a[p*2]\)處,右兒子儲存在\(a[p*2+1]\)處,這樣就可以快速地找到節點之間的父子關系。
理想狀態下,\(n\)個葉節點的滿二叉樹有\((\sum_{i=0}^{2^i=n}2^i)=2n-1\)個節點,但由於最后一層至多還可能有\(2n\)個節點,所以數組空間要開到\(4n\)大小。
我們先來看一個維護區間最大值的例子。
對於線段樹的每一個節點,我們可以額外的設置一個變量\(Max\)代表該節點所代表區間中的最大值,顯然有:\(Max(p)=\max(Max(p*2),Max(p*2+1))\),那么我們可以用如下方法建樹。
\(Code:\)
struct SegmentTree
{
int p,l,r,Max;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define p(x) tree[x].p
#define Max(x) tree[x].Max
}tree[N*4];
inline void build(int p,int l,int r)//對於節點p,代表的區間為[l,r]
{
l(p)=l,r(p)=r;//左右邊界賦值
if(l==r){Max(p)=0;return;}//如果為葉節點,直接賦值為權值
int mid=(l+r)/2;
//遞歸構建子樹
build(p*2,l,mid);
build(p*2+1,mid+1,r);
Max(p)=max(Max(p*2),Max(p*2+1));//回溯更新最大值
}
修改 (modify)
線段樹支持節點的動態修改。
對於如 "將節點\(x\)修改權值為\(v\)" 的指令,線段樹可以以自下向上的方式修改。具體地,可以從根節點作為入口進入,遞歸向下找到需要修改的節點,再在回溯過程中更新沿路祖先節點的最值信息。時間復雜度\(O(log_2n)\)。
\(Code:\)
inline void modify(int p,int x,int v)
{
if(l(p)==r(p))//如果已經找到葉節點,更新權值
{
Max(p)=v;
return;
}
int mid=(l(p)+r(p))/2;
if(x<=mid)modify(p*2,x,v);//如果在左子樹中,則遞歸左子樹尋找
if(x>mid)modify(p*2+1,x,v);//如果在右子樹中,則遞歸右子樹尋找
Max(p)=max(Max(p*2),Max(p*2+1)); //回溯更新
}
查詢 (query)
線段樹還需要能夠解決區間最值查詢問題。
對於如 "查詢區間\([l,r]\)的最大值" 的指令,線段樹可以遞歸查找得到最大值。具體地,從根節點開始,遞歸執行以下過程:
- 1.若\([l,r]\)完全覆蓋了當前結點所代表的區間,返回當前結點區間中的最大值作為備選答案
- 2.若左子節點與\([l,r]\)有重合部分,遞歸訪問左子節點
- 3.若右子節點與\([l,r]\)有重合部分,遞歸訪問右子節點
可以證明,區間查詢的時間復雜度至多為\(O(2log_2n)\)。
\(Code:\)
inline int query(int p,int l,int r)
{
if(l<=l(p)&&r>=r(p))return Max(p);//如果完全包含這個區間,返回這個區間的最大值作為備選答案
int mid=(l(p)+r(p))/2;
int res=-INF;
//遞歸查詢有重合部分的左右區間
if(l<=mid)res=max(res,query(p*2,l,r));
if(r>mid)res=max(res,query(p*2+1,l,r));
return res;
}
至此,線段樹的基本模型已經構成,我們通過一道模板題展示一下代碼。
Description
給定一個包含n個數的序列,初值全為0,現對這個序列有兩種操作:
操作1:把 給定 第k1 個數改為k2;
操作2:查詢 從第k1個數到第k2個數得最大值。(k1<=k2<=n)
所有的數都 <=100000
Input Format
第一行給定一個整數n,表示有n個操作。
以下接着n行,每行三個整數,表示一個操作。
第一個樹表示操作序號,第二個數為k1,第三個數為k2
Output Format
若干行,查詢一次,輸出一次。
Sample Input
3
1 2 2
1 3 3
2 2 3
Sample Output
3
\(Code:\)
#include<bits/stdc++.h>
using namespace std;
const int N=100000+200,INF=0x3f3f3f3f;
int n;
struct SegmentTree
{
int p,l,r,Max;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define p(x) tree[x].p
#define Max(x) tree[x].Max
}tree[N*4];
inline void build(int p,int l,int r)
{
l(p)=l,r(p)=r;
if(l==r){Max(p)=0;return;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
Max(p)=max(Max(p*2),Max(p*2+1));
}
inline void modify(int p,int x,int v)
{
if(l(p)==r(p))
{
Max(p)=v;
return;
}
int mid=(l(p)+r(p))/2;
if(x<=mid)modify(p*2,x,v);
if(x>mid)modify(p*2+1,x,v);
Max(p)=max(Max(p*2),Max(p*2+1));
}
inline int query(int p,int l,int r)
{
if(l<=l(p)&&r>=r(p))return Max(p);
int mid=(l(p)+r(p))/2;
int res=-INF;
if(l<=mid)res=max(res,query(p*2,l,r));
if(r>mid)res=max(res,query(p*2+1,l,r));
return res;
}
inline void input(void)
{
scanf("%d",&n);
build(1,1,n);
}
inline void solve(void)
{
for(int i=1;i<=n;i++)
{
int index,k1,k2;
scanf("%d%d%d",&index,&k1,&k2);
if(index==1)modify(1,k1,k2);
else printf("%d\n",query(1,k1,k2));
}
}
int main(void)
{
input();
solve();
return 0;
}
延遲標記 (lazytag)
在實現了簡單的線段樹后,我們考慮一下拓展。
我們以上實現的線段樹是支持區間查詢和單點修改的,如果需要區間修改呢?
如果用之前的線段樹直接做的話,每一次修改的時間復雜度是\(O(log_2n)\),那么區間修改的時間復雜度將會達到至多\(O(nlog_2n)\),這是我們無法承受的。
我們可以考慮一下這種情況:對於一次區間修改指令\([l,r,delta]\)(將\([l,r]\)內的所有元素加\(delta\)),如果在之后的區間詢問中完全沒有調用到區間\([l,r]\),那么這次\(O(nlog_2n)\)的修改就是完全無用的。
這樣,我們對於每一個線段樹中的節點引入一個變量\(lazytag\)延遲標記,\(lazytag(x)\)代表\(x\)被已經某一次區間操作修改,但是\(x\)的子節點暫時還未修改,其修改的變化量為\(lazytag(x)\)。然后,我們對於每一個區間修改操作,只對一個點做更新,並修改其\(lazytag\)值。需要查詢時,我們再下傳\(lazytag\)標記,順帶更新每一個沿路節點的關鍵值,就可以保證查詢可以得到正確答案。
那么,每一次區間修改操作就只需要對\(log_2n\)個節點做修改,時間復雜度就優化到了\(O(log_2n)\),對於子節點的更新,只需要在查詢時順帶更新即可。
\(Code:\)
struct SegmentTree
{
int p,l,r,Max,lazytag;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define p(x) tree[x].p
#define Max(x) tree[x].Max
#define lazytag(x) tree[x].lazytag
}tree[N*4];
inline void spread(int p)
{
if(lazytag(p))//將有標記節點的子節點更新,並下傳標記
{
Max(p*2)+=lazytag(p);
Max(p*2+1)+=lazytag(p);
lazytag(p*2)+=lazytag(p);
lazytag(p*2+1)+=lazytag(p);
lazytag(p)=0;
}
}
inline void modify(int p,int l,int r,int d)
{
if(l<=l(p)&&r>=r(p))//包含修改區間,進行標記
{
Max(p)+=d;
lazytag(p)+=d;
return;
}
spread(p);//下傳標記
int mid=(l(p)+r(p))/2;
if(l<=mid)modify(p*2,l,r,d);
if(r>mid)modify(p*2+1,l,r,d);
Max(p)=max(Max(p*2),Max(p*2+1));
}
inline int query(int p,int l,int r)
{
if(l<=l(p)&&r>=r(p))return Max(p);
spread(p); //下傳標記
int mid=(l(p)+r(p))/2;
int res=-INF;
if(l<=mid)res=max(res,query(p*2,l,r));
if(r>mid)res=max(res,query(p*2+1,l,r));
return res;
}
通過一道例題展示一下區間修改線段樹的代碼。
Description
給定一個包含n個數的序列,初值全為0,現對這個序列有兩種操作:
操作1:將第k1 個數 到 第k2 個數加1;
操作2:查詢 從第k1個數到第k2個數得最大值。(k1<=k2<=n)
所有的數都 <=100000
Input Format
第一行給定一個整數n,表示有n個操作。
以下接着n行,每行三個整數,表示一個操作。
Output Format
若干行,查詢一次,輸出一次。
Sample Input
3
1 2 2
1 3 3
2 2 3
Sample Output
1
\(Code:\)
#include<bits/stdc++.h>
using namespace std;
const int N=100000+200,INF=0x3f3f3f3f;
int n;
struct SegmentTree
{
int p,l,r,Max,lazytag;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define p(x) tree[x].p
#define Max(x) tree[x].Max
#define lazytag(x) tree[x].lazytag
}tree[N*4];
inline void build(int p,int l,int r)
{
l(p)=l,r(p)=r;
if(l==r){Max(p)=0;return;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
Max(p)=max(Max(p*2),Max(p*2+1));
}
inline void spread(int p)
{
if(lazytag(p))
{
Max(p*2)+=lazytag(p);
Max(p*2+1)+=lazytag(p);
lazytag(p*2)+=lazytag(p);
lazytag(p*2+1)+=lazytag(p);
lazytag(p)=0;
}
}
inline void modify(int p,int l,int r,int d)
{
if(l<=l(p)&&r>=r(p))
{
Max(p)+=d;
lazytag(p)+=d;
return;
}
spread(p);
int mid=(l(p)+r(p))/2;
if(l<=mid)modify(p*2,l,r,d);
if(r>mid)modify(p*2+1,l,r,d);
Max(p)=max(Max(p*2),Max(p*2+1));
}
inline int query(int p,int l,int r)
{
if(l<=l(p)&&r>=r(p))return Max(p);
spread(p);
int mid=(l(p)+r(p))/2;
int res=-INF;
if(l<=mid)res=max(res,query(p*2,l,r));
if(r>mid)res=max(res,query(p*2+1,l,r));
return res;
}
inline void input(void)
{
scanf("%d",&n);
build(1,1,n);
}
inline void solve(void)
{
for(int i=1;i<=n;i++)
{
int index,k1,k2;
scanf("%d%d%d",&index,&k1,&k2);
if(index==1) modify(1,k1,k2,1);
else printf("%d\n",query(1,k1,k2));
}
}
int main(void)
{
input();
solve();
return 0;
}
<后記>