「隊列進出圖上的方向php
線段樹很早就會寫了,但一直沒有總結,因此偶爾重寫又會懵逼,因此還是要總結一下。html
引言node
在生活和競賽中,咱們老是會趕上一些問題,好比說使人厭惡的統計成績,老師會想詢問幾我的中成績最低的是誰......ios
因而問題出現了。c++
e.g.1(暴力膜不可取)算法
已知班上有50個學生,學號分別爲1-50,老師想問學號爲a-b之間的最低分是多少數組
好比 2 5 3 4 1中 2-4 之間的最小值爲 3數據結構
顯然數據很是少,咱們能夠針對每一個詢問,掃一遍,得到最小值。函數
複雜度呢?假設有m個詢問,a-b的區間最大爲npost
因此複雜度爲Θ(mn)。
e.g.2(st表)
那麼新問題來了,一場多市聯考之後,你的老師拿到了一份有100000人成績的表格(並無任何科學依據,純屬胡謅,若有雷同,不勝榮幸),老師如今想問你a-b之間的最低分是多少,並在一秒內出解。
說句實話老師你爲何必定要在1秒內出解啊
反正就當老師趕時間吧(攤手)
由於是靜態查詢,咱們能夠用st表來作,這就不詳細說了。
複雜度爲Θ(詢問次數),但預處理是Θ(nlogn)的。
代碼以下:
#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; int n,m,dp[100010][17]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&dp[i][0]); } for(int i=1;i<=17;i++) { for(int j=1;j<=n;j++) { if(j+(1<<(i-1))-1<=n) { dp[j][i]=min(dp[j][i-1],dp[j+(1<<(i-1))][i-1]); } } } int l,r; for(int i=1;i<=m;i++) { scanf("%d%d",&l,&r); int x=(int)(log((double)(r-l+1))/log(2.0)); printf("%d\n",min(dp[l][x],dp[r-(1<<x)+1][x])); } }
e.g.3(線段樹點修改)
成績還在複查,有時候會偶爾發現有些同窗的成績算錯了,而後要更新,因而老師又會想要詢問a~b之間的最低分,仍是100000個學生.......
說句實話老師你難道不能等到複查完再查分嗎
這道題和e.g.2有什麼區別嗎?
這道題是強制在線的,由於一個同窗的成績改變之後會致使整個區間的值改變。
這樣st表就失效了。
咱們會發現st表在查詢上很優,但構造的話就.......
那有沒有什麼好的辦法呢?
雖然st表有點小問題,但它的思想能夠借鑑——兩個小區間中最小值較小的那個是這個大區間的最小值。
那麼不妨想想
若是一個數值修改了,st表的哪些部分須要改動呢?
若是這些改動的部分並很少,咱們能夠只改這些部分,就不用重構st表了!
不用想天然是全部覆蓋這個點的區間。
這些區間有多少呢?長度爲一的一個,爲二的兩個,爲四的四個.......加起來彷佛太多了!與其如此,我還不如重構st表呢!
好的,那麼現在的問題就變成了,該怎麼讓這些區間變得少一些。
繼續開始yy,咱們的一個點在一個二的冪次的長度上只出現一次,那麼樣這一個點要修改值的時候,咱們只用修改包含它長度爲2,4,8.....的點就能夠了。
那麼這是什麼呢?一棵二叉樹。
好的吧 上面的彷佛是錯的,由於若是有不是2的冪次,樹就建不起來了。
那麼索性逆其道而行,咱們靠二分長度來建樹。
數值樹:
區間樹:
那麼此時建出來的樹中若是要修改一個點的怎麼辦呢
很簡單,修改它全部的影響的父節點就好了
嘛,固然不是每一個父節點都要改,仍是要按題意來的啊~
好比這個:
那麼怎麼查詢呢?
好比說我要查詢上圖2-4的最小值
可是會發現2-4並無完整對應的區間,若是有的話天然是能直接出解
那怎麼辦呢?
咱們能夠經過二分肯定長度的中間值,而後判斷咱們的查詢的l是否小於mid,若是是,那麼這個節點的左子樹中有一部分解會影響到總解,咱們須要繼續搜這顆子樹的子樹,最終若是對於一個子樹它的l-r被a-b所包含,那麼就不用再搜了。
好比查詢區間2-4
如圖則是查詢訪問的次序。
那麼它的複雜度是多少呢?
顯然建樹是Θ(nlogn)修改是Θ(logn)
那麼查詢呢?
能夠想象,對於一個區間,他必定包含一個長度小於他的最長的2的冪次的長度,好比說長度爲十的區間中必定包含一個長度爲八的區間
這個區間若是恰好對應一個相應的節點,那麼它就至關於用了一的費用,查了八個解
但比較尬的是有時候這個八是被錯開的,但再不濟也能分紅兩個四,至關於用了二的費用查了八個解
那麼對於這個區間每次最多須要用二的費用除掉最大的二的冪次的解
咱們知道1+2+4+8+......+2^n=2^(n+1)-1
而在2^(n+1)-1的範圍內全部的數字都能用最多n個2的冪次之和表示
因此複雜度爲Θ(logn)
e.g.3.1線段樹點修改如何實現?
彷佛能夠理解線段樹的思想了,但其實仍是一臉懵逼(其實我估計通常人看不懂……)
那麼就詳細的拆開來說講吧。
建樹(build)
好的,那麼應該怎麼建樹呢?
咱們須要快速的查詢二叉樹的每一個點的父節點和子節點,同是內存還要儘量省,咱們能夠對於一個編號爲n的點將它的父節點記爲[n/2](取整),左兒子記爲2*n,右兒子記爲2*n+1
而後根節點爲1,表示1-n區間中所要求的值(按題意來定)
這樣子不會有衝突,由於一個深度n的樹最多有2^n-1個節點,而深度爲n+1的標號最小的子樹爲2^n。
而後建樹從葉子節點開始,逐漸傳到父節點,因此咱們還須要一個函數(push_up),來計算兩個子節點到父節點的轉移。
固然實際建樹是dfs的,反正理解一發就好了!
主要須要兩個函數:push_up,build
一、push_up
二、build
點更新(update)
天然是先更新那個點,而後更新他的全部父節點。
update
查詢(query)
按照以前的解釋已經說的很清楚了
query
這樣子點修改的線段樹就基本寫出來了
那麼來一道例題
hdu1754 i hate it
有n個數,m個操做,操做分爲兩種種類:
一、Q l r 詢問l-r之間最大值
二、U x v 將x位置的值換成當前值與v值中較大的一個
輸入樣例#1:
5 6
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5
輸出樣例#1:
5
6
5
9
代碼:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define lson root<<1 #define rson root<<1|1 int tree[800010],n,m,a,b,x,v; char c; void push_up(int root) { tree[root]=max(tree[lson],tree[rson]); } void build(int l,int r,int root) { if(l==r) { scanf("%d",&tree[root]); return ; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void update(int l,int r,int x,int v,int root) { if(l==r) { tree[root]=max(tree[root],v); return; } int mid=(l+r)>>1; if(x<=mid) { update(l,mid,x,v,lson); } else { update(mid+1,r,x,v,rson); } push_up(root); } int query(int a,int b,int l,int r,int root) { int ans=0; if(l>=a&&b>=r) { return tree[root]; } int mid=(l+r)>>1; if(a<=mid) { ans=max(ans,query(a,b,l,mid,lson)); } if(mid<b) { ans=max(ans,query(a,b,mid+1,r,rson)); } return ans; } int main() { while(scanf("%d%d",&n,&m)!=EOF) { build(1,n,1); for(int i=1; i<=m; i++) { scanf("\n%c",&c); if(c=='Q') { scanf("%d%d",&a,&b); printf("%d\n",query(a,b,1,n,1)); } if(c=='U') { scanf("%d%d",&x,&v); update(1,n,x,v,1); } } } return 0; }
固然也能夠用結構體來儲存樹的結構,好比區間的左和右端點,這樣子能夠少傳遞幾個參數,不過實際上速度並不會快不少,甚至會慢……不過這有什麼問題嗎,這玩意可能會更加好寫2333
代碼:
#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define lson root<<1 #define rson root<<1|1 using namespace std; struct node { int l,r,m; } tr[800080]; int n,m; void push_up(int root) { tr[root].m=max(tr[lson].m,tr[rson].m); } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; scanf("%d",&tr[root].m); return ; } tr[root].l=l; tr[root].r=r; int mid=(l+r)>>1; build(lson,l,mid); build(rson,mid+1,r); push_up(root); } void update(int root,int pos,int val) { if(tr[root].l==pos&&tr[root].r==pos) { tr[root].m=max(tr[root].m,val); return ; } int mid=(tr[root].l+tr[root].r)>>1; if(pos<=mid) { update(lson,pos,val); } else { update(rson,pos,val); } push_up(root); } int query(int root,int l,int r) { if(l==tr[root].l&&r==tr[root].r) { return tr[root].m; } int mid=(tr[root].l+tr[root].r)>>1; if(l>mid) { return query(rson,l,r); } else { if(mid>=r) { return query(lson,l,r); } else { return max(query(lson,l,mid),query(rson,mid+1,r)); } } } int main() { while(scanf("%d%d",&n,&m)!=EOF) { build(1,1,n); while(m--) { char kd; scanf("\n%c",&kd); if(kd=='Q') { int l,r; scanf("%d%d",&l,&r); printf("%d\n",query(1,l,r)); } if(kd=='U') { int pos,val; scanf("%d%d",&pos,&val); update(1,pos,val); } } } }
上面那題爲單點修改求區間最大值模板
那麼咱們再來一道單點修改求區間和模板題
給出n個點以及一些操做,操做分爲四種
一、Add x v 給x的位置加v
二、Sub x v 給x的位置減v
三、Query l r 求l-r的區間和
四、End 結束詢問
Sample Input
1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End
Sample Output
Case 1:
6
33
59
代碼:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define lson root<<1 #define rson root<<1|1 int tree[800010],n,m,a,b,x,v; char c[10]; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build(int l,int r,int root) { if(l==r) { scanf("%d",&tree[root]); return ; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void update(int l,int r,int x,int v,int root) { if(l==r) { tree[root]+=v; return; } int mid=(l+r)>>1; if(x<=mid) { update(l,mid,x,v,lson); } else { update(mid+1,r,x,v,rson); } push_up(root); } int query(int a,int b,int l,int r,int root) { int ans=0; if(l>=a&&b>=r) { return tree[root]; } int mid=(l+r)>>1; if(a<=mid) { ans+=query(a,b,l,mid,lson); } if(mid<b) { ans+=query(a,b,mid+1,r,rson); } return ans; } int main() { int t,ttt=0; scanf("%d",&t); while(t--) { ttt++; printf("Case %d:\n",ttt); memset(tree,0,sizeof(tree)); scanf("%d",&n); build(1,n,1); while(1) { cin>>c; if(c[0]=='Q') { scanf("%d%d",&a,&b); printf("%d\n",query(a,b,1,n,1)); } if(c[0]=='A') { scanf("%d%d",&x,&v); update(1,n,x,v,1); } if(c[0]=='S') { scanf("%d%d",&x,&v); update(1,n,x,-v,1); } if(c[0]=='E') { break; } } } return 0; }
天然也能夠寫結構體板的,但請容我偷個懶哈~(doge)
不不不不,偷懶實際上是爲了寫下面這道好題。
能夠說若是下面這道題能寫出來,就說明你對線段樹的結構有必定的瞭解了。
洛谷U23283(原題爲codeforces 914D,這款遊戲就不要玩了,太傷身體)
題目大意
給出一段序列,兩個操做
操做1 給出l,r,x
求區間l-r的gcd,若是至多能改掉區間內的一個數(不影響原序列),使gcd是x的倍數,那麼輸出YES,不然輸出NO
操做2 給出pos,x
將序列中pos位置上的數字改成x
首先GCD是具備傳遞性的,因此可使用線段樹進行維護,但比較麻煩的就是能夠改掉一個數。
能夠試想一下,對於每一個區間的查詢,咱們最終獲得的是若干完整返回塊的gcd,若是其中有兩個塊gcd都不是x的倍數,那麼確定GG
若是隻有一個塊不是呢?這個塊中也可能有多個數不是x的倍數,還須要再檢驗一下。
線段樹的思路就是一個節點的數值由他的兩個兒子節點轉移而來,若是他的兩個兒子節點的gcd都不是x的倍數,那麼仍是GG
若是隻有一個不是,咱們就繼續查看那個點的左右兒子gcd是否都不是x的倍數,直到長度爲一的節點便可,此時必定知足條件
每次能夠去掉一半的長度,至關於二分。
算上gcd帶的log的狀況下,複雜度爲Θ(n*logn*logn)。
代碼以下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define lson root<<1 #define rson root<<1|1 using namespace std; int gcd(int a,int b) { if(b>a) { swap(a,b); } if(b) { return gcd(b,a%b); } else { return a; } } int nowson,x,cnt,n,m; struct node { int l,r,g; }tr[2000020]; void push(int root) { tr[root].g=gcd(tr[rson].g,tr[lson].g); } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; scanf("%d",&tr[root].g); return ; } int mid=(l+r)>>1; tr[root].l=l; tr[root].r=r; build(lson,l,mid); build(rson,mid+1,r); push(root); } void update(int root,int pos,int v) { if(tr[root].l==pos&&tr[root].r==pos) { tr[root].g=v; return ; } int mid=(tr[root].l+tr[root].r)>>1; if(mid>=pos) { update(lson,pos,v); } else { update(rson,pos,v); } push(root); } int query(int root,int l,int r) { if(tr[root].l==l&&tr[root].r==r) { if(tr[root].g%x!=0) { cnt--; nowson=root; } return tr[root].g; } int mid=(tr[root].l+tr[root].r)>>1; if(l>mid) { return query(rson,l,r); } else { if(r<=mid) { return query(lson,l,r); } else { return gcd(query(lson,l,mid),query(rson,mid+1,r)); } } } int check(int root) { if(tr[root].l==tr[root].r) { return 1; } if(tr[lson].g%x!=0&&tr[rson].g%x!=0) { return 0; } if(tr[lson].g%x==0) { return check(rson); } else { return check(lson); } } int main() { int n,m; scanf("%d",&n); build(1,1,n); scanf("%d",&m); for(int i=1;i<=m;i++) { int kd,l,r; scanf("%d",&kd); if(kd==1) { cnt=1; scanf("%d%d%d",&l,&r,&x); int tmp=query(1,l,r); if(x==tmp) { puts("YES"); } else { if((!cnt)&&check(nowson)) { puts("YES"); } else { puts("NO"); } } } else { scanf("%d%d",&l,&r); update(1,l,r); } } }
e.g.4(線段樹區間修改)
複查時出現了評卷大錯誤,連續一個考場某道題的分都沒有加,如今須要給加上,而後老師把聰明的你推薦給了統分人(做了吧233)要詢問區間和以方便計算平均數。
好的吧,反正改一個也是改,改一段也是改。
來看一道例題
如題,已知一個數列,你須要進行下面兩種操做:
1.將某區間每個數加上x(區間加)
2.求出某區間每個數的和(區間求和)
最簡單的思路天然是給每一個點都加上值而後push_up
代碼:
#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define lson root<<1 #define rson root<<1|1 long long tree[400040]; int n,m; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build(int l,int r,int root) { if(l==r) { scanf("%lld",&tree[root]); return; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void add(int l,int r,int root,int ls,int rs,int v) { if(l==r) { tree[root]+=v; return; } int mid=(l+r)>>1; if(ls<=mid) { add(l,mid,lson,ls,rs,v); } if(rs>mid) { add(mid+1,r,rson,ls,rs,v); } push_up(root); } long long query(int l,int r,int ls,int rs,int root) { if(l>=ls&&r<=rs) { return tree[root]; } int mid=(l+r)>>1; long long ans=0; if(ls<=mid) { ans+=query(l,mid,ls,rs,lson); } if(rs>mid) { ans+=query(mid+1,r,ls,rs,rson); } return ans; } int main() { int n,m; scanf("%d%d",&n,&m); build(1,n,1); for(int i=1;i<=m;i++) { int kd; int l,r,v; scanf("%d",&kd); if(kd==1) { scanf("%d%d%d",&l,&r,&v); add(1,n,1,l,r,v); } if(kd==2) { scanf("%d%d",&l,&r); int ans=query(1,n,l,r,1); printf("%lld\n",ans); } } }
而後就光榮的TLE了,咱們考慮再優化一發
其實徹底不用每一個點push_up
由於咱們徹底能夠用l-r區間中包含的須要修改的點的個數來算出該區間須要加上的值,即爲修改點個數乘以每一個點修改的值。
這樣子能夠省掉很多時間,由於將加法變成乘法後本來複雜度爲Θ(長度)的加就變成了Θ(1)的乘
代碼:
#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define lson root<<1 #define rson root<<1|1 long long tree[400040]; int n,m; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build(int l,int r,int root) { if(l==r) { scanf("%lld",&tree[root]); return; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void add(int l,int r,int root,int ls,int rs,int v) { if(l==r) { tree[root]+=v; return; } int mid=(l+r)>>1; if(ls<=mid) { add(l,mid,lson,ls,rs,v); } if(rs>mid) { add(mid+1,r,rson,ls,rs,v); } push_up(root); } long long query(int l,int r,int ls,int rs,int root) { if(l>=ls&&r<=rs) { return tree[root]; } int mid=(l+r)>>1; long long ans=0; if(ls<=mid) { ans+=query(l,mid,ls,rs,lson); } if(rs>mid) { ans+=query(mid+1,r,ls,rs,rson); } return ans; } int main() { int n,m; scanf("%d%d",&n,&m); build(1,n,1); for(int i=1;i<=m;i++) { int kd; int l,r,v; scanf("%d",&kd); if(kd==1) { scanf("%d%d%d",&l,&r,&v); add(1,n,1,l,r,v); } if(kd==2) { scanf("%d%d",&l,&r); int ans=query(1,n,l,r,1); printf("%lld\n",ans); } } }
可是由於每個修改的葉節點的父節點及祖先節點都須要遍歷,複雜度彷佛還有點可怕(但比起以前快了近兩倍)
因此又一次光榮的TLE了
可是此次計算中的乘思想是比較有啓發的。
那該怎麼繼續優化呢?
ちょっとまって!咱們以前的查詢爲何是Θ(logN)的呢?
咱們能夠發現,其實查詢的時候咱們遵循能返回大塊就返回大塊的思路,不會再下去查詢小塊
而咱們卻在每次更新時都會去將全部的大塊和小塊一併更新,
那麼能不能只更新大塊呢?
顯然不行
由於咱們仍然可能會須要查詢一些更小的區間的值。
可是咱們能夠退而求其次,等到須要查找更小的區間的時候再去更新
因而就能夠給每個點打上一個標記,等咱們訪問了他的父節點再將他的父節點附上這個值,並將這個值向下推給他,這就是lazy-tag的思想。
須要一個新的函數:用於將懶惰標記下傳。
push_down
固然原來的其餘函數也要有所改動,具體看代碼:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define lson root<<1 #define rson root<<1|1 using namespace std; struct node { long long l,r,sum,lazy; }tree[400040]; int n,m,kd; void push_up(int root) { tree[root].sum=tree[lson].sum+tree[rson].sum; } void push_down(int root) { int mid=(tree[root].l+tree[root].r)>>1; tree[lson].sum+=tree[root].lazy*(mid-tree[root].l+1); tree[rson].sum+=tree[root].lazy*(tree[root].r-mid); tree[lson].lazy+=tree[root].lazy; tree[rson].lazy+=tree[root].lazy; tree[root].lazy=0; } void build(int l,int r,int root) { if(l==r) { tree[root].l=l; tree[root].r=r; scanf("%lld",&tree[root].sum); return; } tree[root].l=l; tree[root].r=r; int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void add(int l,int r,int root,int x) { if(l==tree[root].l&&r==tree[root].r) { tree[root].lazy+=x; tree[root].sum+=x*(tree[root].r-tree[root].l+1); return; } int mid=(tree[root].l+tree[root].r)>>1; if(tree[root].lazy) { push_down(root); } if(r<=mid) { add(l,r,lson,x); } else { if(l>mid) { add(l,r,rson,x); } else { add(l,mid,lson,x); add(mid+1,r,rson,x); } } push_up(root); } long long query(int l,int r,int root) { if(l==tree[root].l&&tree[root].r==r) { return tree[root].sum; } int mid=(tree[root].l+tree[root].r)>>1; if(tree[root].lazy) { push_down(root); } if(r<=mid) { return query(l,r,lson); } else { if(l>mid) { return query(l,r,rson); } } return query(l,mid,lson)+query(mid+1,r,rson); } int main() { scanf("%d%d",&n,&m); build(1,n,1); for(int i=1;i<=m;i++) { scanf("%d",&kd); if(kd==1) { int l,r,x; scanf("%d%d%d",&l,&r,&x); add(l,r,1,x); } else { int l,r; scanf("%d%d",&l,&r); printf("%lld\n",query(l,r,1)); } } }
這就是lazy標記比較淺薄的應用,咱們能夠再來看一道比較抽象的題目
題目大意:
在牆上按照輸入順序貼海報,求最後能看見幾張不一樣的海報
emmm,這道題要用離散化的思路,由於原來的區間實在是太大了!
這是也我選這道題的惟一緣由
至於倒貼海報什麼的我卻是不敢苟同,由於明顯有更加暴力的方法啊……
來來來,讓咱們直接染色2333
對於區間l[i]-r[i]進行染色,其實就是對該區間進行區間修改,將這個區間修改爲i
到時候用桶的思路去記錄整面牆上出現了幾種顏色便可。
對了,爲了防止某些左右端點都被遮住,只有中間露出來的小透明影響答案,能夠再離散化以前把l+1,r-1也扔進去。
我並不知道這玩意到底正不正確,可是居然A掉了
代碼以下,若是有大佬能叉掉還請在評論區指正,注意這道題的本質只是爲了向你們介紹線段樹在趕上極大區間而實際使用的區間卻沒有這麼多的時候可使用離散化的思想。
離散化聽着高大上但其實代碼也就下面這麼一點,不要慌張哦~
可是必定要學會啊,以後權值線段樹和主席樹都是要用的啊
#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define lson root<<1 #define rson root<<1|1 using namespace std; struct node { int l,r,val,lazy; } tr[200020]; struct poster { int l,r; }p[40040]; int a[40040],cnt,ans[40040],cnt1[40040]; void push_down(int root) { tr[lson].val=tr[root].lazy; tr[rson].val=tr[root].lazy; tr[lson].lazy=tr[root].lazy; tr[rson].lazy=tr[root].lazy; tr[root].lazy=0; } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; tr[root].val=0; return ; } tr[root].l=l; tr[root].r=r; int mid=(tr[root].l+tr[root].r)>>1; build(lson,l,mid); build(rson,mid+1,r); } void update(int root,int l,int r,int val) { if(l==tr[root].l&&r==tr[root].r) { tr[root].val=val; tr[root].lazy=val; return ; } if(tr[root].lazy) { push_down(root); } int mid=(tr[root].l+tr[root].r)>>1; if(l>mid) { update(rson,l,r,val); } else { if(r<=mid) { update(lson,l,r,val); } else { update(lson,l,mid,val); update(rson,mid+1,r,val); } } } int query(int root,int pos) { if(pos==tr[root].l&&pos==tr[root].r) { return tr[root].val; } if(tr[root].lazy) { push_down(root); } int mid=(tr[root].l+tr[root].r)>>1; if(mid>=pos) { return query(lson,pos); } else { return query(rson,pos); } } int main() { int t,n; scanf("%d",&t); while(t--) { cnt=0; memset(cnt1,0,sizeof(cnt1)); scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d %d",&p[i].l,&p[i].r); a[++cnt]=p[i].l; a[++cnt]=p[i].l+1; a[++cnt]=p[i].r; a[++cnt]=p[i].r-1; } sort(a+1,a+cnt+1); //由此開始離散化 cnt=unique(a+1,a+cnt+1)-a-1; for(int i=1;i<=n;i++) { p[i].l=lower_bound(a+1,a+cnt+1,p[i].l)-a; p[i].r=lower_bound(a+1,a+cnt+1,p[i].r)-a; //到這結束 } build(1,1,cnt); for(int i=1;i<=n;i++) { update(1,p[i].l,p[i].r,i); } for(int i=1;i<=cnt;i++) { ans[i]=query(1,i); } int num=0; for(int i=1;i<=cnt;i++) { if(cnt1[ans[i]]==0&&ans[i]!=0) { cnt1[ans[i]]=1; num++; } } printf("%d\n",num); } }
既然連離散化都講了,那不妨也來說講掃描線吧(什麼神邏輯)
e.g.亂入 掃描線
掃描線這玩意一聽就很是高級啊
它具體使用的範圍是計算幾何裏,求矩形的面積並
來看一道例題吧
題意:給出平面上n個矩形的左下角座標和右上角座標,求平面被全部矩形覆蓋的面積。
好吧,這玩意跟線段樹有什麼關係?
我感受明顯能夠瞎搞啊
你瞧n<=200啊
可是若是可啪的出題人把數據範圍形成了n<=100000呢(x,y的範圍也極大)?
瞎搞不行了,咱們考慮一下哪裏能優化
一個矩形的面積能夠表示爲長乘寬,那麼一個矩形能作出的貢獻就是從第一條邊出現一直到第二條邊出現,在這中間與y軸平行的線段的長度是不變的。
有點蒙,那麼來張圖吧
因此想到了什麼?
差分啊,差分啊,差分啊!
即便是幾個矩形疊來疊去,在一條邊出現到下一條邊出現之間,與y軸平行的線段的長度也仍是不變的。
因此對於y軸上的長度,只有在遇到新的線段的時候纔會變化,咱們能夠按照每條與y軸平行的邊來分割矩形。
面積就是與y軸平行的當前長度和*(x[i]-x[i-1])
好的,那跟線段樹有什麼關係呢?
由於與y軸平行的長度和能夠用線段樹維護啊!
在第一條線段進入的時候對於y軸平行的那條線加入一條長度爲ly[i]~ry[i]的線段
第二條線進入的時候刪除這條線段,線段樹維護長度和 ,這樣子能夠很是美妙的適應強制在線的很是大的數據。
若是理解了的話,這就變成了一道區間覆蓋問題,支持插入一條線段,刪除一條線段,求當前全部線段覆蓋的長度
此時的線段樹要稍微更改一下
從push_up到update都要改(煩躁ing)
push_up的改變應該很好看懂,若是這一段lazy大於0,說明區間被全覆蓋,長度爲區間長度,不然則是兩個子樹的長度之和
build建右子樹的時候mid+1改爲mid,建的樹同時記錄實數的nl-nr和僞離散化的l-r
update見代碼,不是很好解釋,反正不難。
當初學的時候大概膜了好幾份代碼,這份是最好看懂得,我把它魔改了一發,應該也比較接近平日裏本身的寫法。
代碼以下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define lson root<<1 #define rson root<<1|1 #define N 100010 using namespace std; struct line { double x,y1,y2; int val; } l[N]; bool cmp(line a,line b) { return a.x<b.x; } struct node { double nl,nr,len; int l,r,lazy; } tr[N<<2]; double a[N]; void push_up(int root) { if(tr[root].lazy>0) { tr[root].len=tr[root].nr-tr[root].nl; } else { if(tr[root].r-tr[root].l==1) { tr[root].len=0; } else { tr[root].len=tr[lson].len+tr[rson].len; } } } void build(int root,int l,int r) { if(l+1==r) { tr[root].l=l,tr[root].r=r; tr[root].nl=a[l],tr[root].nr=a[r]; tr[root].lazy=0,tr[root].len=0.0; return ; } tr[root].l=l,tr[root].r=r; tr[root].nl=a[l],tr[root].nr=a[r]; tr[root].lazy=0,tr[root].len=0.0; int mid=(l+r)>>1; build(lson,l,mid); build(rson,mid,r); } void update(int root,double l,double r,int val) { if(tr[root].nl==l&&tr[root].nr==r) { tr[root].lazy+=val; push_up(root); return ; } if(r<=tr[lson].nr) { update(lson,l,r,val); } else { if(l>=tr[rson].nl) { update(rson,l,r,val); } else { update(lson,l,tr[lson].nr,val); update(rson,tr[rson].nl,r,val); } } push_up(root); } int main() { int n,cnt,ttt=0; while(scanf("%d",&n)!=EOF&&n) { ttt++; cnt=0; double x1,x2,y1,y2; for(int i=1;i<=n;i++) { scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2); l[++cnt].x=x1;l[cnt].y1=y1;l[cnt].y2=y2;l[cnt].val=1; a[cnt]=y1; l[++cnt].x=x2;l[cnt].y1=y1;l[cnt].y2=y2;l[cnt].val=-1; a[cnt]=y2; } sort(l+1,l+cnt+1,cmp); sort(a+1,a+cnt+1); build(1,1,cnt); update(1,l[1].y1,l[1].y2,l[1].val); double ans=0.0; for(int i=2;i<=cnt;i++) { ans+=tr[1].len*(l[i].x-l[i-1].x); update(1,l[i].y1,l[i].y2,l[i].val); } printf("Test case #%d\n",ttt); printf("Total explored area: %.6lf\n",ans); puts(""); } }
大致單標記的操做都有一些介紹了,更難的東西也是有的,但無外乎就是這個思想。
固然,除了單標記,還有雙標記這種更加神奇的操做
e.g.5(線段樹區間修改——雙標記)
啊,你終於幫助老師寫出了一顆帶區間修改的線段樹,結果老師眉頭一皺,發現事情並不簡單。
原來,這些考場考得是不同的卷子,按照卷子的不一樣要乘上不一樣比例的權值。
簡單地說,就是既要實現區間加又要實現區間乘,最後可憐的你仍是要出求區間和2333
這個例題就足以引出雙標記了,其實若是理解了單標記,雙標記也就不是很難了
雙標記之因此有別於單標記,就是由於標記還有前後順序。
前後順序,這彷佛表明着一堆zz的分類討論,可是不要慌,其實它並不複雜。
咱們來考慮一下哪一種計算先比較優越,
首先,是咱們的一號選手,人類最古老的數學符號,數學殿堂中的長者——加♂法
算了,太中二了,反正咱們就隨便考慮一下先加後乘會咋樣吧,好比說如今原數爲a,加標記爲b,乘標記爲c
來來來,如今的值是(a+b)*c,好像沒有什麼問題呢~
呵呵,誰告訴你咱們實際修改的時候必定是加標記在前的呢?
若是先乘再加呢?
(a+(b/c))*c
emmm,看,個人精度,他飛起來啦qwq
好的,那麼先乘再加就必定有用了嗎?
sorry,先乘再加是真的能夠隨心所欲的!
爲何呢?
由於乘標記再先乘後加是這樣子的
a*c+b
先加後乘是這樣子的
a*c+b*c
有沒有發現,只須要在進行乘操做的時候把加標記乘上c就能夠了。
反過來,加法也能夠這麼作,可是當心你的精度2333
仍是照例來到例題
已知一個數列,你須要進行下面三種操做:
1.將某區間每個數乘上x
2.將某區間每個數加上x
3.求出某區間每個數的和
代碼寫的時候注意開longlong 感謝hpw大佬的代碼,對我頗有啓發
代碼以下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define ll long long #define lson root<<1 #define rson root<<1|1 #define N 400010 using namespace std; struct node { ll l,r,sum,add=0,mul=1; }tree[N]; int n,m,p; void push_up(int root) { tree[root].sum=(tree[lson].sum+tree[rson].sum)%p; } void push_down(int root) { int mid=(tree[root].l+tree[root].r)>>1; tree[lson].sum=(tree[lson].sum*tree[root].mul+tree[root].add*(mid-tree[root].l+1))%p; tree[rson].sum=(tree[rson].sum*tree[root].mul+tree[root].add*(tree[root].r-mid))%p; tree[lson].mul=(tree[lson].mul*tree[root].mul)%p; tree[rson].mul=(tree[rson].mul*tree[root].mul)%p; tree[lson].add=(tree[lson].add*tree[root].mul+tree[root].add)%p; tree[rson].add=(tree[rson].add*tree[root].mul+tree[root].add)%p; tree[root].add=0; tree[root].mul=1; return; } void build(int l,int r,int root) { if(l==r) { tree[root].l=l; tree[root].r=r; scanf("%lld",&tree[root].sum); return; } tree[root].l=l; tree[root].r=r; int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); return; } void add(int l,int r,int root,ll k) { if(l==tree[root].l&&r==tree[root].r) { tree[root].add=(tree[root].add+k)%p; tree[root].sum=(tree[root].sum+k*(tree[root].r-tree[root].l+1))%p; return ; } int mid=(tree[root].l+tree[root].r)>>1; push_down(root); if(r<=mid) { add(l,r,lson,k); } else { if(l>mid) { add(l,r,rson,k); } else { add(l,mid,lson,k); add(mid+1,r,rson,k); } } push_up(root); } void mul(int l,int r,int root,ll k) { if(l==tree[root].l&&r==tree[root].r) { tree[root].sum=(tree[root].sum*k)%p; tree[root].mul=(tree[root].mul*k)%p; tree[root].add=(tree[root].add*k)%p; return ; } int mid=(tree[root].l+tree[root].r)>>1; push_down(root); if (r<=mid) { mul(l,r,lson,k); } else { if (l>mid) { mul(l,r,rson,k); } else { mul (l,mid,lson,k); mul(mid+1,r,rson,k); } } push_up(root); } ll query (int l,int r,int root) { if(l==tree[root].l&&r==tree[root].r) { return tree[root].sum%p; } int mid=(tree[root].l+tree[root].r)>>1; push_down(root); if(r<=mid) { return query(l,r,lson); } else { if(l>mid) { return query(l,r,rson); } else { return (query(l,mid,lson)+query(mid+1,r,rson))%p; } } } int main () { scanf("%d%d%d",&n,&m,&p); build(1,n,1); for(int i=1; i<=m; ++i) { int oper; scanf("%d",&oper); if(oper==1) { int l,r; ll k; scanf("%d%d%lld",&l,&r,&k); mul(l,r,1,k); } else { if(oper==2) { int l,r; ll k; scanf("%d%d%lld",&l,&r,&k); add(l,r,1,k); } else { int l,r; scanf("%d%d",&l,&r); printf("%lld\n",query(l,r,1)%p); } } } return 0; }
e.g.6(zkw線段樹點修改)
好了如今毒瘤的老師(出題人)已經不知足於100000的數據了,他魔改了時限和數據範圍,幾乎卡到了nlogn的極限,而後仍是詢問區間最大值。
你以爲你的線段樹常數優越,隨手一交就又一次完成了任務,可是你的菜雞同窗xhk TLE了(話說這不是給個人任務嗎?爲何xhk會來摻和一腳啊,好氣啊!)
秉着同學的情誼,你開始教他zkw線段樹
zkw線段樹又被稱爲非遞歸版線段樹,常數比普通線段樹要小不少,這是由於zkw線段樹用的幾乎都是for循環,這省去了不少時間和空間。
由於c++的遞歸用的是棧機制,額外的空間複雜度與遞歸次數呈線性比例,同時,由於調用了大量函數,時間開銷也會變大。
之因此須要普通線段樹要用遞歸寫,是由於遞歸好理解。
但其實非遞歸的線段樹也不是很是難理解,很是難寫。相反,它很短很精悍。
因此zkw神犇提出了它,並說明了它的幾個優勢:
常數小,空間小,代碼短
那簡直是太優了!因此zkw線段樹應該怎麼實現呢?
讓咱們從新來看一看線段樹的構造吧!
按照咱們存圖的編號來講它會是這樣的:
由於咱們不用遞歸,因此建樹時須要用一個科學的方法找到葉子節點,那麼這種樹合適嗎?顯然是不合適的,由於葉子結點沒有連續性。上面可能還看不出來,這個就很明顯了。
其中點1-6分別對應八、九、五、十二、1三、7
徹底沒有任何簡單規律。
因此樹的結構要改一下,改爲什麼呢?
還記得最初那顆不科學的線段樹嗎?
這棵樹的葉子結點是連續的 ,可是它只能支持2的冪次的建樹。
爲何呢?由於底層的葉子節點數不夠多(一本正經的胡說八道)
那麼就給它足夠多的葉子結點啊(何不食肉糜式的回答233)
可是這真不難,你只須要先建一顆足夠大的滿二叉樹,而後把那一個數組掛上去就好了。
有多大呢?天然是底層的滿葉子結點個數大於數組的大小。
即滿葉子結點個數爲第一個大於n+1的2的冪次。
而後再把n所有掛上去。
是的,這看起來是比普通線段樹耗內存,但其實也沒超過四倍的內存,仍是能夠接受的(順便一提,他的空間複雜度仍是比通常線段樹優越)。
並且,咱們能夠直接得到這些要讀入的葉子結點的位置。
接着咱們倒着一層一層往上推(由於一個點的編號一定大於它的父節點,咱們能夠直接for i=bit;i>0;i--)
不過要注意,咱們不掛第一個葉子節點(爲何先本身想一想)
因而建樹就寫出來了!
那麼點修改呢?
是的,由於知道了葉子結點的位置,咱們能夠很輕鬆地直接修改葉子結點,而後從下往上推父節點,這代碼,清真!
可是查詢略微有一點難理解
先舉個栗子吧,若是我如今建了這麼一棵有6個節點的zkw線段樹,我要查詢1-6的最小值,那麼我須要的是哪幾塊?
沒錯,是圖中的9,5,6,14
誒,那麼每次先把l和r扔到底層而後一層一層往上跳,若是l是奇數就統計答案,r是偶數就統計答案,豈不美哉?
naive!
來來來,再看看上面這張圖
11的上面是5,,恭喜你,順便把2也算進去了
因此咱們如今須要一種查詢
可以完成logn的查詢區間最大值,並且不會錯誤的將不屬於該區間的塊記錄進來
感受很難搞吧,其實還真有一種能夠勝任此工做的查詢方法,原理和上面的差很少
咱們在底層將l指向l-1,r指向r+1,像這張圖同樣跳上來
你發現了什麼?
什麼也沒發現????
好吧,是在下輸了,我再來張圖,如今是查詢3-5
這彷佛是很是明顯了,若是l是偶數,就加上他的右兄弟(父親節點的右子樹),若是r是奇數,就加上他的左兄弟(父親節點的左子樹)
結束的條件是l==r-1
這能夠保證查詢到的區間必定是咱們想要的,由於這些區間的範圍確定在(l-1,r+1)以內,被l和r限制着,STO zkw神犇
好的,因此能夠很是輕鬆地發現,這種查詢查詢的是開區間。
這也就是咱們以前爲何不掛地一個葉子節點的緣由。
爲了方便查詢開區間(0,n+1)的值,很顯然咱們不會趕上n是上面15節點之類的狀況,具體爲何請回顧建樹的過程
好的,查詢代碼以下:
如今zkw線段樹全部須要用的代碼都寫完了,來個總代碼:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define lson root<<1 #define rson root<<1|1 int n,m,bit; char c; int tree[800080]; void push_up(int root) { tree[root]=max(tree[lson],tree[rson]); } void build() { for(bit=1; bit<=(n+1); bit<<=1); for(int root=bit+1; root<=bit+n; root++) { scanf("%d",&tree[root]); } for(int root=bit-1; root>=1; root--) { push_up(root); } } void update(int root,int v) { for(tree[root+=bit]=v,root>>=1; root>0; root>>=1) { push_up(root); } } int query(int l,int r) { int ans=0; for(l+=bit-1,r+=bit+1; l^r^1; l>>=1,r>>=1) { if(~l&1) { ans=max(ans,tree[l^1]); } if(r&1) { ans=max(ans,tree[r^1]); } } return ans; } int main() { while(scanf("%d%d",&n,&m)!=EOF) { build(); for(int i=1; i<=m; i++) { int a,b; scanf("\n%c%d%d",&c,&a,&b); if(c=='Q') { printf("%d\n",query(a,b)); } else { update(a,b); } } } }
zkw線段樹單點修改區間求和也是好寫的, 以hdu1166爲例
代碼以下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define lson root<<1 #define rson root<<1|1 int n,m,bit; char c[10]; int tree[800080]; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build() { for(bit=1; bit<=(n+1); bit<<=1); for(int root=bit+1; root<=bit+n; root++) { scanf("%d",&tree[root]); } for(int root=bit-1; root>=1; root--) { push_up(root); } } void update(int root,int v) { for(tree[root+=bit]+=v,root>>=1; root>0; root>>=1) { push_up(root); } } int query(int l,int r) { int ans=0; for(l+=bit-1,r+=bit+1; l^r^1; l>>=1,r>>=1) { if(~l&1) { ans+=tree[l^1]; } if(r&1) { ans+=tree[r^1]; } } return ans; } int main() { int t,ttt=0; scanf("%d",&t); while(t--) { ttt++; printf("Case %d:\n",ttt); memset(tree,0,sizeof(tree)); scanf("%d",&n); build(); for(; 1; ) { int a,b; scanf("\n%s",&c); if(c[0]=='E') { break; } scanf("%d %d",&a,&b); if(c[0]=='Q') { printf("%d\n",query(a,b)); } else { if(c[0]=='A') { update(a,b); } else { if(c[0]=='S') { update(a,-b); } } } } } }
至於區間修改,你會發現由於zkw是從下到上查詢的因此標記什麼的就gg了,可是zkw神犇提出了差分的作法,說句實話感受不明覺厲,因此區間修改仍是乖乖地寫普通線段樹吧~(之因此再也不寫下去的緣由是由於沒(zuo)有(zhe)必(hen)要(lan)了)
自帶大常數的xhk你仍是爆零吧~
至於zkw線段樹有多優越,看看下面這兩張圖就知道了~感謝xhk提供的大常數普通線段樹~
提及來普通線段樹,老師靈機一動又改了需求
e.g.7(權值線段樹求第k小,rank,前驅後繼)
嗯,老師終於不來找你改分數了,他來詢問你一個新的毒瘤問題
他會有六種操做
第一種:給你一個同窗的成績
第二種:讓你刪掉一個同窗的成績
第三種:詢問這些成績中第k小的是哪一個
第四種:詢問某同窗的得分x在這些成績中排第幾
第五種:找到比該同窗高的最低分數
第六種:找到比該同窗低的最高分數
總操做數爲100000
你嘿嘿一笑:「來,老師,splay、treap拿去不謝,不滿意我這還有紅黑樹。」
老師眉頭一皺:「我以爲你以前那個算法挺好的,你就用那個來實現一下吧。」
好的,你開始寫權值線段樹。
若是咱們再也不按照區間建樹而是改去按照每一個數出現的次數建樹會怎麼樣?
好比說1,1,2,4,5這個序列,線段樹建成這個樣子,維護的是區間和
那麼這有什麼用呢?
咱們來分析一下這幾個問題的具體意思
第一個第二個是插入和刪除,就不須要分析了,pass,下一個!
第三個是查詢第k小數
若是某個數以前(包括本身)有k個數以上,且這個數以前(不包括本身)的數的個數不到k個,那麼這個數就是第k小
第四個是求某數的排名
這就是這個數以前數的個數加一
第五第六個前驅和後繼就是這個數以前(以後)第一個出現的數,也就是個數大於等於一的最近的一個數,能夠二分區間啦~
因此咱們只要有一個可以logn計算有多少個數比他小的數據結構就能夠啦,是什麼呢?
權值線段樹啊
在上面那張圖裏,你能夠經過query 1~x來求出x以前有多少個數啦~
因此這些問題就都迎刃而解了,能夠實現每一個操做log^2 n之內。
如今來道例題
您須要寫一種數據結構(可參考題目標題),來維護一些數,其中須要提供如下操做:
額,看着很簡單?來,把數據範圍供上來!
emmm,n仍是能夠接受的,可是值域……太大了啊!
怕什麼?咱們有離散化。
kth和排名均可以logn搞,前驅後繼我瞎胡了一種log^2 n的作法,若是有大佬可以給出logn的作法還請不吝賜教~
代碼以下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define lson root<<1 #define rson root<<1|1 using namespace std; struct node { int l,r,sum; } tr[800080]; struct opt { int kd,x; } op[100010]; int cnt,a[100010]; void push_up(int root) { tr[root].sum=tr[lson].sum+tr[rson].sum; } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; tr[root].sum=0; return ; } tr[root].l=l; tr[root].r=r; int mid=(l+r)>>1; build(lson,l,mid); build(rson,mid+1,r); push_up(root); } void update(int root,int pos,int val) { if(tr[root].l==pos&&tr[root].r==pos) { tr[root].sum+=val; return ; } int mid=(tr[root].l+tr[root].r)>>1; if(mid>=pos) { update(lson,pos,val); } else { update(rson,pos,val); } push_up(root); } int query(int root,int l,int r) { if(l>r) { return 0; } if(tr[root].l==l&&tr[root].r==r) { return tr[root].sum; } int mid=(tr[root].l+tr[root].r)>>1; if(mid<l) { return query(rson,l,r); } else { if(r<=mid) { return query(lson,l,r); } else { return query(lson,l,mid)+query(rson,mid+1,r); } } } int kth(int root,int k) { if(tr[root].l==tr[root].r) { return tr[root].l; } if(tr[lson].sum>=k) { return kth(lson,k); } else { return kth(rson,k-tr[lson].sum); } } int rank(int x) { int pos=lower_bound(a+1,a+cnt+1,x)-a; return query(1,1,pos-1)+1; } int pre(int root,int pos) { int l=1,r=pos-1,mid; while(l<r) { mid=(l+r)>>1; if(query(1,mid+1,r)) { l=mid+1; } else { r=mid; } } return r; } int next(int root,int pos) { int l=pos+1,r=cnt,mid; while(l<r) { mid=(l+r)>>1; if(query(1,l,mid)) { r=mid; } else { l=mid+1; } } return l; } int main() { int n; scanf("%d",&n); for(int i=1; i<=n; i++) { scanf("%d%d",&op[i].kd,&op[i].x); if(op[i].kd!=2&&op[i].kd!=4) { a[++cnt]=op[i].x; } } sort(a+1,a+cnt+1); cnt=unique(a+1,a+cnt+1)-a-1; build(1,1,cnt); for(int i=1; i<=n; i++) { if(op[i].kd==1) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; update(1,pos,1); } if(op[i].kd==2) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; update(1,pos,-1); } if(op[i].kd==3) { printf("%d\n",rank(op[i].x)); } if(op[i].kd==4) { int pos=kth(1,op[i].x); printf("%d\n",a[pos]); } if(op[i].kd==5) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; printf("%d\n",a[pre(1,pos)]); } if(op[i].kd==6) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; printf("%d\n",a[next(1,pos)]); } } }
好的,終於到主席樹了(鬆氣)
e.g.8(主席樹求區間第k小)
毒瘤老師又一次提出了非分的要求,他但願查找全部考試成績中大家班的倒數第k名,即一段連續區間的第k小(爲何一個班在同一個考場考呢,這是一個值得深思的問題)
區間第k小啊,這不是很好辦呢。
反正求第k小的話,權值線段樹的思路是能夠借鑑的,而後咱們想一想若是對於區間l~r的第k大,咱們已經知道了1~l-1的權值線段樹,又知道了1~r的權值線段樹,那麼根據前綴和的思路,咱們能夠很輕鬆的求出l~r的權值線段樹,對於這棵樹進行求區間第k大便可
可是首先建n棵線段樹就已經不是咱們可以接受的了
複雜度實在過高,確定不能適應100000的數據範圍,同時,空間也是硬傷
那麼怎麼辦呢?
咱們分析一下,每次主席樹的操做,是否是全部的點都會變呢?
若是不會的話咱們天然能夠有多少個變就改多少個了。
很幸運,與update的複雜度同樣,每次插入新的數字只會改變logn個節點,那麼咱們新建這logn個節點就能夠了。(這玩意應該不用解釋爲何吧,由於update就只會遍歷logn次)
對於每一個節點記錄他的左子樹右子樹編號,這個編號再也不由root<<1,root<<1|1推得,而是由數組l和r記錄。
若是哪一個子樹再也不被影響,那麼就把他的原來的子樹接上來。
主席樹就是這個思路啦
查詢的時候就查插入第l-1個數到插入第r個數的差的樹的第k小就能夠了
具體的操做就看看代碼吧
照例來到例題:
題意就是求區間第k小
那麼就是最裸的主席樹。
代碼以下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define mid ((l+r)>>1) #define hi puts("hi"); using namespace std; #define N 200010 int n,m,q,cnt=0; int a[N],b[N],T[N]; int sum[N<<5],L[N<<5],R[N<<5]; int build(int l,int r) { int rt=++cnt; sum[rt]=0; if(l<r) { L[rt]=build(l,mid); R[rt]=build(mid+1,r); } return rt; } int update(int pre,int l,int r,int x) { int rt=++cnt; L[rt]=L[pre]; R[rt]=R[pre]; sum[rt]=sum[pre]+1; if(l<r) { if(x<=mid) { L[rt]=update(L[pre],l,mid,x); } else { R[rt]=update(R[pre],mid+1,r,x); } } return rt; } int query(int u,int v,int l,int r,int k) { if(l>=r) { return l; } int x=sum[L[v]]-sum[L[u]]; if(x>=k) { return query(L[u],L[v],l,mid,k); } else { return query(R[u],R[v],mid+1,r,k-x); } } int main() { scanf("%d%d",&n,&q); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); b[i]=a[i]; } sort(b+1,b+n+1); m=unique(b+1,b+1+n)-b-1; T[0]=build(1,m); for(int i=1;i<=n;i++) { int t=lower_bound(b+1,b+1+m,a[i])-b; T[i]=update(T[i-1],1,m,t); } while(q--) { int ll,rr,kk; scanf("%d%d%d",&ll,&rr,&kk); int t=query(T[ll-1],T[rr],1,m,kk); printf("%d\n",b[t]); } return 0; }
啊,寫了三天終於寫完了,原本當初的flag還有二維線段樹和樹狀數組的,結果是在寫不動了orz
平均一天3000字(不含製圖)真的快要遇上網文做家了,代碼寫到死啊,不過仍是頗有成就感的
嗯,就這樣了