首先對於線段樹,其實與其他各種樹都是一樣的,都有著樹形的結構。接下來讓我們考慮一些問題:
~~給出乙個陣列,要求滿足下面的操作:
~~①給定三個值x,y,z,要求吧【x,y】區間的每個數都加上z。
~~②給定兩個值x,y,要求輸出【x,y】區間的最大值(or最小值or和)。
如果我們用暴力做這些問題,那麼對於操作①和②,每次的複雜度為區間的長度。這種方法在資料比較小的時候可以直接用,在資料比較大的情況下就很難滿足時間限制了。這個時候我們就需要用到線段樹。
線段樹,最主要的點就在於它可以滿足對於乙個區間的詢問以及操作,達到o(log)的時間複雜度,能夠更好的滿足資料實時更新的題目要求。
接下來我們就來構建一棵線段樹。首先我們需要考慮線段樹的每個定點上面儲存的值是什麼。由於我們查詢的時候是直接查詢線段樹定點的值,所以我們線段樹的定點所儲存的值就是我們題目中所要求的值(區間最大值or區間最小值or區間和)。然後每次查詢只要從根節點開始查詢,如果當前查詢到的區間在所詢問的區間之內,那麼就返回當前區間的值,否則就繼續向下查詢。
接下來我們分別來介紹線段樹的每個操作(蒟蒻不怎麼會用指標~~見諒):
假定我們有乙個7個數的陣列:1 5 6 9 2 3 4
那麼下面這幅圖就是該陣列的線段樹(區間最小值)表示:
~~①建樹(最基本的~)
首先,由於乙個節點記錄的是乙個區間的最小值,而每個節點的值又會受到下面節點的影響,所以我們可以嘗試著用遞迴的方式來寫。蒟蒻用的是陣列記錄(不怎麼會指標),以零為起點,所以兩個孩子節點就是pos*2+1和pos*2+2。我們定義如下的結構體:
struct node
no[maxn*4];//這裡是很多新手會犯的錯誤之一,因為線段樹是二叉樹,但在最後一層,有很多的子節點是遞迴所要用到的,但是其本身卻沒有存在的意義,所以線段樹的陣列大小一般要開大到原來的2~4倍左右
當遞迴到區間的長度只有1的時候,那麼這個節點的值就是陣列中對應的值,然後回溯回去,比較得出每個非葉子節點的值。
void build(int
pos,int l,int r)
else
}
~~②查詢某個區間的給定值(最大值or最小值or和)
int find(int
pos,int nowl,int nowr,int l,int r)
if(nowl>=l&&nowr<=r)
int mid=(nowl+nowr)>>1;
return min(find(pos
*2+1,nowl,mid,l,r),find(pos
*2+2,mid+1,nowr,l,r));
}
到現在來看,如果線段樹只有這些,那麼還算是比較簡單的了,但是線段樹核心的優化就在於其修改的操作,大大降低了線段樹的總複雜度。
~~③對某一區間的值進行修改
到這一步,我們就要引進乙個新的引數:addmark。把這個引數作為延時標記,具體的作用在下面進行重點講解。
~(1)定義的結構體進行改變:
struct node
no[maxn];
~(2)建樹的時候順便進行初始化,其餘沒變化:
void build(int
pos,int l,int r)
else
}
~(3)新加入乙個操作:push_down,目的在於把延時標記addmark向節點下方傳遞。
void push_down(int now)
}
~(4)查詢的同時進行push_down操作,實時更新陣列資料。(本蒟蒻認為線段樹最為優秀也是最核心的乙個點,最後會進行專門的講解)
int find(int
pos,int nowl,int nowr,int l,int r)
if(nowl>=l&&nowr<=r)
push_down(pos);//查詢時向下傳遞addmark(手動高亮)
int mid=(nowl+nowr)/2;
return min(find(pos
*2+1,nowl,mid,l,r),find(pos
*2+2,mid+1,nowr,l,r));
}
~(5)改變區間的值,同時也向下傳遞markdown(運用遞迴)
void change(int
pos,int nowl,int nowr,int l,int r,int addval)
push_down(pos);//向下傳遞addmark
int mid=(nowl+nowr)/2;
change(pos
*2+1,nowl,mid,l,r,addval);
change(pos
*2+2,mid+1,nowr,l,r,addval);//遞迴進行區間改變值
no[pos].val=min(no[pos
*2+1].val,no[pos
*2+2].val);//回溯後改變當前節點的值
}
放出完整的**:
區間最小值:
struct node
no[maxn];
int a[maxn];
void build(int
pos,int l,int r)
else
}void push_down(int now)
}int find(int
pos,int nowl,int nowr,int l,int r)
if(nowl>=l&&nowr<=r)
push_down(pos);
int mid=(nowl+nowr)/2;
return min(find(pos
*2+1,nowl,mid,l,r),find(pos
*2+2,mid+1,nowr,l,r));
}void change(int
pos,int nowl,int nowr,int l,int r,int addval)
push_down(pos);
int mid=(nowl+nowr)/2;
change(pos
*2+1,nowl,mid,l,r,addval);
change(pos
*2+2,mid+1,nowr,l,r,addval);
no[pos].val=min(no[pos
*2+1].val,no[pos
*2+2].val);
}
區間之和:
typedef long long ll;
struct node
no[maxn*4];
ll a[maxn];
void build(int
pos,int l,int r)
else
}void push_down(int now,int l,int r)
}ll find(int
pos,int nowl,int nowr,int l,int r)
if(nowl>=l&&nowr<=r)
push_down(pos,nowl,nowr);
int mid=(nowl+nowr)/2;
return find(pos
*2+1,nowl,mid,l,r)+find(pos
*2+2,mid+1,nowr,l,r);
}void change(int
pos,int nowl,int nowr,int l,int r,ll addval)
push_down(pos,nowl,nowr);
int mid=(nowl+nowr)/2;
change(pos
*2+1,nowl,mid,l,r,addval);
change(pos
*2+2,mid+1,nowr,l,r,addval);
no[pos].val=no[pos
*2+1].val+no[pos
*2+2].val;
}
最後對push_down這個操作再進行一波解釋。實際上,線段樹在執行了區間改變量值之後,僅僅只有執行區間所對應節點的值發生了改變。舉個例子,就用我們上面所用的陣列,如果計算的是區間的和的話,那麼應該是下面這棵線段樹:
如果我們進行區間改變值得操作,把1~4的每個點加上4。那麼實際上在change操作完成之後,只有區間1~4的這個節點的值變為了47(21+4*4),而這個節點以下的節點的值都沒有進行改變。直到我們進行查詢操作涉及到了1~4這個區間的時候,我們才會將區間1~4的這個節點的addmark向下進行傳遞。所以本蒟蒻認為線段樹最大的優化便是在這個點上,減少了很多不會對查詢有影響的操作,使得演算法更加優秀。
不過在比賽的時候其實不怎麼推薦用線段樹去解題,真的是能不用就不。第一, 線段樹的**量已經有點偏大了,比賽的時候時間本身就比較少。第二,線段樹打完之後仍有許多細節需要調節,也會花費大量的時間。第三,線段樹的陣列要開到原來的2~4倍,所需的空間會比較大。
czl的知識點整理5 單調佇列
既然在模板庫中發了單調佇列的板子,那麼就順便把單調佇列講一講吧。我不會說是在noip前一天攢一攢rp的 單調佇列和單調棧的方法差不多,而且可以算是不會線段樹的oier的福音了。所以單調棧也不會的同學也來學一學吧,反正操作也是差不多的。回歸正題 單調佇列的用途是維護乙個區間的最值的 是不是很像線段樹啊...
czl的知識點整理2 高斯消元
xjoi 1822 civilization 高斯消元其實在小學初中解多元一次方程的時候已經接觸過了。其實,高斯消元就是建立在方程中加減消元和乘除消元之上的。只不過,高斯消元法把這兩種方法應用於矩陣之中,使得高斯消元的複雜度達到o n 相比於真正的去解方程可是要快的多了,想一想你手解100000元一...
Photoshop知識點整理(4)
向量繪圖工具組向量工具 快捷鍵u,切換工具shift u 1.形狀模式 在繪製過程中會自動新建圖層,預設自動填充前景色。2.顏色填充 純色填充 漸變填充 圖案填充。3.圖形描邊 純色填充 漸變填充 圖案填充 描邊大小 描邊選項。4.圖形大小 屬性欄處可以精確調整大小或ctrl t。5.圖形繪製 按住...