一、引例
有M個數排成一列,做N次操作,每次操作包括:
(1)詢問指定區間的最大值、最小值
(2)將指定區間的每個數加上一個值
如果按照最樸素的做法,一個個的遍歷,時間複雜度:O(MN)。
那麼如何解決一個區間求和(最大值,最小值)的問題呢?那麼就要用到線段樹啦。
二、定義
線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
主要用來解決區間查詢、區間修改,使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,基本保證每次操作的時間複雜度爲O(logN)。
三、實際應用
a.單點更新
1.定義每個結點的信息
線段樹是建立在線段的基礎上,每個結點都代表了一條線段[a,b]。長度爲1的線段稱爲元線段。非元線段都有兩個子結點,左結點代表的線段爲[a,(a + b) / 2],右結點代表的線段爲[((a + b) / 2)+1,b]。
struct node
{
int left,right,sum;//左端點,右端點,和
} tree[maxn<<2];
2.更新
void maintain(int root)//更新根節點爲左右子結點的和
{
int lnode=root<<1;
int rnode=root<<1+1;
tree[root].sum=tree[lnode].sum+tree[rnode].sum;
}
3.遞歸建樹
遇到葉子節點直接賦值,否則遞歸遍歷左右建樹,最後回溯即可。
void build(int root,int begin,int end)//樹的結點編號 左端點下標 右端點下標
{
tree[root].left=begin;
tree[root].right=end;
if(begin==end)//葉子節點
{
scanf("%d",&adj[begin]);
tree[root].sum=adj[begin];//單元素 直接賦值
return ;
}
int mid=(begin+end)>>1;
build(root<<1,begin,mid);//更新左子樹
build(root<<1+1,mid+1,end);//更新右子樹
maintain(root);//存儲左右子樹的和
}
4.單點更新
將一條線段[a,b] 插入到代表線段[l,r]的結點p中,如果p不是元線段,那麼令mid=(l+r)/2。如果b<mid,那麼將線段[a,b] 也插入到p的左兒子結點中,如果a>mid,那麼將線段[a,b] 也插入到p的右兒子結點中。
void update(int root,int pos,int num)
//根結點編號 欲修改值的下標 期待的值
{
if(tree[root].left==tree[root].right&&tree[root].left==pos)
{//若修改的值在這個節點的左右區間之間那麼就直接更改此區間的sum值就可
tree[root].sum+=num;
return ;
}
int mid=(tree[root].left+tree[root].right)>>1;
if(pos<=mid)
update(root<<1,pos,num);
else
update(root<<1+1,pos,num);
maintain(root);//每次都要更新根節點
}
5.求和操作
int query(int root,int begin,int end)//求和
{
int ans=0;
if(begin==tree[root].left&&end==tree[root].right)
{
return tree[root].sum;
}
int mid=(tree[root].left+tree[root].right)>>1;
if(end<=mid)
ans+=query(root<<1,begin,end);
else if(begin>=mid+1)
ans+=query(root<<1+1,begin,end);
else
{
ans+=query(root<<1,begin,mid);
ans+=query(root<<1+1,mid+1,end);
}
return ans;
}
b.區間更新(成段更新)
比如 從[1,10]每個結點的值都+1,普通單點更新就會超時。
*區間更新:
指更新某個區間內的葉子節點的值,因爲涉及到的葉子節點不止一個,而葉子節點會影響其相應的非葉父節點,那麼回溯需要更新的非葉子節點也會有很多,如果一次性更新完,操作的時間複雜度肯定不是O(lgn),例如當我們要更新區間[0,3]內的葉子節點時,需要更新出了葉子節點3,9外的所有其他節點。爲此引入了線段樹中的延遲標記概念,這也是線段樹的精華所在。
*延遲標記:
因爲更新的數很多,所以我每一步的更新不接着算出來,等到最後需要的時候再去取消標記算出來。
比如現在需要對[a,b]區間值進行加c操作,那麼就從根節點[1,n]開始調用update函數進行操作,如果剛好執行到一個子節點,它的節點標記爲rt,這時tree[rt].l == a && tree[rt].r == b 這時我們可以一步更新此時rt節點的sum[rt]的值,sum[rt] += c * (tree[rt].r - tree[rt].l + 1),注意關鍵的時刻來了,如果此時按照常規的線段樹的update操作,這時候還應該更新rt子節點的sum[]值,而Lazy思想恰恰是暫時不更新rt子節點的sum[]值,到此就return,直到下次需要用到rt子節點的值的時候纔去更新,這樣避免許多可能無用的操作,從而節省時間 。
用lazy標記,等到當前區間比我需要的目標區間大的時候,我必須用到下面的值了,必須往下修改了,這時候,我們就把之前堆積起來的懶惰標記pushdown了,於是就有了一個神奇的pushdown操作。
其他的建樹什麼的和單點更新一樣,只是多了lazy標記和pushdown。
void pushdown(LL root) //向下傳遞lazy標記
{
if (tree[root].lazy)
{
tree[root<<1].lazy+=tree[root].lazy;
tree[root<<1+1].lazy+=tree[root].lazy;
tree[root<<1].val+=tree[root<<1].len*tree[root].lazy;
tree[root<<1+1].val+=tree[root<<1+1].len*tree[root].lazy;
tree[root].lazy=0;
}
}
c.區間合併
不過還沒做過這方面的題QAQ
ps:之前看過,今天再看像重新學了一遍(>_<)
今天距離省賽過去已經一個星期了,該調整回來了。
還是不夠強,繼續修煉吧QAQ。