「數(shù)據(jù)結(jié)構(gòu)進(jìn)階」例題之樹形數(shù)據(jù)結(jié)構(gòu)

0x40「數(shù)據(jù)結(jié)構(gòu)進(jìn)階」例題

幾點(diǎn)總結(jié)
1 數(shù)據(jù)結(jié)構(gòu)有兩個用處:組織特定類型的數(shù)據(jù)方便調(diào)用猎醇、高效的實(shí)現(xiàn)數(shù)據(jù)的增刪改查穴墅。換句話說秽之,這兩個用處指向了思考數(shù)據(jù)結(jié)構(gòu)問題時的邏輯過程成肘,即先想到樸素算法(可能是模擬亿遂,貪心浓若,復(fù)雜度較高的DP方程等等),然后我們往往時因?yàn)閿?shù)據(jù)范圍的限制而使用數(shù)據(jù)結(jié)構(gòu)蛇数。也就是說挪钓,數(shù)據(jù)結(jié)構(gòu)本質(zhì)上是一種對算法實(shí)現(xiàn)過程的優(yōu)化惦费。
2 數(shù)據(jù)結(jié)構(gòu)問題中耸彪,很多數(shù)據(jù)結(jié)構(gòu)維護(hù)的信息有相似處,這時需要根據(jù)情況選取特定的數(shù)據(jù)結(jié)構(gòu)丘逸,同時充分考慮代碼量浦徊,實(shí)現(xiàn)難度馏予,算法常數(shù)等因素盔性,最終找到最合適的數(shù)據(jù)結(jié)構(gòu)悉尾。
3 所有樹形數(shù)據(jù)結(jié)構(gòu)還有一個明顯的特征:擅于且僅擅于維護(hù)符合“區(qū)間加法”的信息务漩。具體地說,就是兩個小區(qū)間的最值再取最值可以得到一個大區(qū)間的最值它褪,兩個小區(qū)間的和再求和可以得到一個大區(qū)間的和饵骨。然而,若維護(hù)信息不滿足“區(qū)間加法”茫打,樹形數(shù)據(jù)結(jié)構(gòu)就會在時間復(fù)雜度上產(chǎn)生退化居触。例如妖混,已知兩個小區(qū)間的眾數(shù),難以在O(1)的時間內(nèi)完成合并兩個小區(qū)間得到大區(qū)間眾數(shù)的過程轮洋。

并查集

當(dāng)問題滿足關(guān)系具有傳遞性無向性時制市,并查集時維護(hù)關(guān)系的一個有用工具。(PS:若是關(guān)系具有傳遞性和有向性弊予,這時可以思考圖論建模有向圖強(qiáng)連通分量的tarjan算法以及k-sat問題
并查集的實(shí)現(xiàn)非常簡單祥楣,且屬于基礎(chǔ)內(nèi)容,這里不再贅述汉柒。不過值得一提的是误褪,并查集合并過程中優(yōu)化時間復(fù)雜度的思想非常重要,很多復(fù)雜的問題都會用到碾褂。例如兽间,我們可以通過并查集的路徑壓縮特性過濾很多無用信息。也可以將”按秩合并”推廣到啟發(fā)式合并(啟發(fā)式合并指:將大的結(jié)構(gòu)和小的結(jié)構(gòu)合并時正塌,將小的結(jié)構(gòu)向大的結(jié)構(gòu)合并嘀略,同時只增加小的結(jié)構(gòu)的查詢費(fèi)用)
同時使用路徑壓縮和按秩合并優(yōu)化的并查集的單次查詢/合并時間復(fù)雜度趨近反阿克曼函數(shù)(近似為常數(shù)),但我們遇到的問題中乓诽,往往只要其中的一種合并方式即可帜羊。
首先給出路徑壓縮優(yōu)化的并查集代碼,單次查詢/合并時間復(fù)雜度:O(logn)

int fa[SIZE];

for (int i = 0; i <= n; i++) fa[i] = i;

int get(int x) {
    if (x == fa[x]) return x;
    return fa[x] = get(fa[x]);
}

void merge(int x, int y) {
    fa[get(x)] = get(y);
}

例題

4101 銀河英雄傳說
顯然问裕,最后的戰(zhàn)艦排列是一條條“鏈”逮壁,即一種特殊形態(tài)的樹,我們可以用并查集維護(hù)粮宛。
本題中需要我們維護(hù)一些并查集森林中的特殊信息窥淆。具體地說,我們需要對于每個元素x巍杈,它到所在樹的樹根的距離d[x]和以它為根的子樹的size[x](注意:由于路徑壓縮的存在忧饭,我們得到的并查集森林中并不是一條條的“鏈”),這樣若x元素和y元素在同一列筷畦,那么他們之間的距離(不包括x和y)就是|d[y]-d[x]|-1
d[x]和size[x]的具體計(jì)算過程代碼如下

int get(int x) {
    if (x == fa[x]) return x;
    int root = get(fa[x]);  
    d[x] += d[fa[x]];       
    return fa[x] = root;    
}

void merge(int x, int y) {
    x = get(x), y = get(y);
    fa[x] = y, d[x] = size[y];
    size[y] += size[x];
}

如果我們面對的問題中需要維護(hù)明顯對立的兩/三/更多的集合词裤,同時“傳遞關(guān)系”可以相互導(dǎo)出時,那么我們會使用擴(kuò)展域并查集吼砂。
例如,若只有兩個集合“敵人”和“朋友”周偎,且如下關(guān)系可以相互導(dǎo)出:
1 A和B是朋友,B和C是朋友蛉艾,可推出A和C是朋友
2 A和B是敵人逢享,B和C是敵人弓柱,可推出A和C是朋友
那么我們可以建立一個擴(kuò)展域并查集來維護(hù)關(guān)系禀横。
具體地說酿箭,我們將一個節(jié)點(diǎn)x拆成兩個節(jié)點(diǎn)x_{1}x_{2}x_{1}表示x是朋友,x_{2}表示x是敵人,同理纵诞,我們將另一個節(jié)點(diǎn)y拆成y_{1}y_{2}
若題目輸入x和y是朋友,那么這意味著兩條信息:
1 x_{1}y_{1}是朋友
2 x_{2}y_{2}是朋友
因此合并(x_{1}掉蔬,y_{1})和(x_{2}壕翩,y_{2}
若題目輸入x和y是敵人,那么這意味著兩條信息:
1 x_{1}y_{2}是朋友
2 x_{2}y_{1}是朋友
因此合并(x_{1}珍策,y_{2})和(x_{2}y_{1}

例題

POJ1733 Parity Game
用sum表示S序列的前綴和,則輸入分兩種情況
1 S[l...r]有偶數(shù)個1,即sum[r]和sum[l-1]奇偶性相同
2 S[l...r]有奇數(shù)個1祟蚀,即sum[r]和sum[l-1]奇偶性不同
顯然鹏溯,奇數(shù)域偶數(shù)域天然對立匀借,也滿足剛才“朋友”和“敵人”集合的特點(diǎn):
1 A和B奇偶性相同,B和C奇偶性相同,可推出A和C奇偶性相同
2 A和B奇偶性不同,B和C奇偶性不同,可推出A和C奇偶性相同
因此按照上面的方法處理輸入即可紫皇。
PS:由于l和r的范圍可能很大萄窜,需要離散化處理
代碼如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
using namespace std;
typedef long long ll;
const int maxn=5000*4+5;
const int INF=0x3f3f3f3f;
int temp,n,f[maxn],q,x[maxn],y[maxn],a[maxn],cnt=0;
string s[maxn];
int find(int x){
    return f[x]==x?x:f[x]=find(f[x]);
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("Parity Game.in","r",stdin);
    cin>>temp;
    cin>>q;
    for(int i=1; i<=q; i++) {
        cin>>x[i]>>y[i]>>s[i];
        if(y[i]<x[i]) swap(x[i],y[i]);
        a[++cnt]=x[i];
        a[++cnt]=y[i];
    }
    sort(a+1,a+cnt+1);
    n=unique(a+1,a+cnt+1)-a-1;
    for(int i=1;i<=n*2;i++){
        f[i]=i;
    }
    for(int i=1; i<=q; i++) {
        int xx=lower_bound(a+1,a+n+1,x[i]-1)-a;
        int yy=lower_bound(a+1,a+n+1,y[i])-a;
        int fa=find(xx);
        int fa1=find(xx+n);
        int fb=find(yy);
        int fb1=find(yy+n);
        if(s[i]=="even") {
            if(fa==fb1){
                cout<<i-1<<endl;
                return 0;
            }
            f[fa]=fb;
            f[fa1]=fb1;
        }
        else{
            if(fa==fb){
                cout<<i-1<<endl;
                return 0;
            }
            f[fa1]=fb;
            f[fb1]=fa;
        }
    }
    cout<<q<<endl;
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

POJ1182 食物鏈
這里的兩個對立集合變成了三個穗泵,但原理與上面相同。
代碼如下

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int n,k,ans=0,fa[3*5*10002],q,x,y;
void init()
{
    for(int i=1;i<=3*n;i++) fa[i]=i;
}
int f(int x)
{
    if (x==fa[x]) return x;
    return fa[x]=f(fa[x]);
}
bool check1(int x,int y)
{
    if (f(x+n)==f(y))
        return false;
    if (f(x)==f(y+n))
        return false;
    return true;
}
bool check2(int x,int y)
{
    if (x==y) return false;
    if (f(x)==f(y))
        return false;
    if (f(x)==f(y+n))
        return false;
    return true;
}
int main()
{
    scanf("%d%d",&n,&k);
    init();
    for(int i=1;i<=k;i++)
    {
        scanf("%d%d%d",&q,&x,&y);
        if (x>n||y>n)
        {
            ans++;
            continue;
        }
        if (q==1)
        {
            if (check1(x,y))
            {
                fa[f(x)]=f(y);
                fa[f(x+n)]=f(y+n);
                fa[f(x+2*n)]=f(y+2*n);
            }
            else
                ans++;
        }
        if (q==2)
        {
            if (check2(x,y))
            {
                fa[f(x+n)]=f(y);
                fa[f(x)]=f(y+2*n);
                fa[f(x+2*n)]=f(y+n);
            }
            else
                ans++;
        }
    }
    printf("%d",ans);
    return 0;
}

樹狀數(shù)組

樹狀數(shù)組所有單次操作時間復(fù)雜度:O(nlogn)
由于在設(shè)計(jì)時消去了冗余節(jié)點(diǎn)尺棋,并且將節(jié)點(diǎn)編號和二進(jìn)制聯(lián)系起來,樹狀數(shù)組的空間復(fù)雜度闷叉、常數(shù)代碼量都比線段樹有了明顯的優(yōu)化握侧。然而,這些節(jié)點(diǎn)的消去嘿期,也讓樹狀數(shù)組難以維護(hù)求最值的操作品擎。
基本功能:單點(diǎn)增加,區(qū)間求和备徐,逆序?qū)?br> 代碼如下

void add(int x, int y) { //單點(diǎn)增加
    for (; x <= N; x += x & -x) c[x] += y;
}
int ask(int x) { //區(qū)間求和
    int ans = 0;
    for (; x; x -= x & -x) ans += c[x];
    return ans;
}
for (int i = n; i; i--) { //逆序?qū)Σ渌鬭[i]范圍較大缕坎,需要離散化
//然而離散化需要排序停局,所以還不如直接用歸并排序求逆序?qū)?    ans += ask(a[i]-1);
    add(a[i], 1);
}

PS:樹狀數(shù)組求逆序?qū)υ恚?strong>逆序掃描整個序列推汽,每次先查詢小于a[i]的數(shù)的個數(shù)累加進(jìn)入答案邪码,然后將a[i]插入樹狀數(shù)組斧拍。這就保證了每次查詢的范圍都是i之后的數(shù)字值小于a[i]不包括a[i]攒至。

例題

4201 樓蘭圖騰
記l[i]表示1...i-1中比a[i]大的數(shù)的個數(shù)
r[i]表示i+1...n中比a[i]大的數(shù)的個數(shù)
則“V”型圖騰的數(shù)量就是\sum_{i=1}^{n}l[i]*r[i]
另一種圖騰計(jì)算同理
代碼如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
using namespace std;
typedef long long ll;
const int maxn=200000+5;
const int INF=0x3f3f3f3f;
ll n,a[maxn],c[maxn],l1[maxn],l2[maxn],r1[maxn],r2[maxn],ans1=0,ans2=0;
void add(ll x){
    for(int i=x;i<=n;i+=i&-i){
        c[i]++;
    }
}
ll sum(ll x){
    ll sum=0;
    for(int i=x;i>0;i-=i&-i){
        sum+=c[i];
    }
    return sum;
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("樓蘭圖騰.in","r",stdin);
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        a[i]=n-a[i]+1;
    }
    memset(c,0,sizeof(c));
    for(int i=1;i<=n;i++){
        l1[i]=sum(a[i]);
        add(a[i]);
    }
    memset(c,0,sizeof(c));
    for(int i=n;i>=1;i--){
        r1[i]=sum(a[i]);
        add(a[i]);
    }
    for(int i=1;i<=n;i++){
//      cout<<l1[i]<<" "<<r1[i]<<endl;
        ans1+=l1[i]*r1[i];
    }
    cout<<ans1<<" ";
    for(int i=1;i<=n;i++){
        a[i]=n-a[i]+1;
    }
    memset(c,0,sizeof(c));
    for(int i=1;i<=n;i++){
        l2[i]=sum(a[i]);
        add(a[i]);
    }
    memset(c,0,sizeof(c));
    for(int i=n;i>=1;i--){
        r2[i]=sum(a[i]);
        add(a[i]);
    }
    for(int i=1;i<=n;i++){
//      cout<<l2[i]<<" "<<r2[i]<<endl;
        ans2+=l2[i]*r2[i];
    }
    cout<<ans2;
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

當(dāng)然荆几,通過一些技巧性的轉(zhuǎn)化抬纸,我們能將樹狀數(shù)組的適用范圍拓寬呜呐。
我們來看看如何通過前綴和和差分的轉(zhuǎn)化實(shí)現(xiàn)區(qū)間增加和單點(diǎn)查詢功能。
題意:長度為n的數(shù)列,q個操作。C l r d 表示把第l~r個數(shù)+d。Q x表示求第x個數(shù)的值撼短,n<1e5,q<1e5
我們知道,樹狀數(shù)組的基本操作可以實(shí)現(xiàn)單點(diǎn)修改吠撮,那么為了將區(qū)間修改轉(zhuǎn)化為單點(diǎn)修改尊惰,我們用樹狀數(shù)組維護(hù)序列的差分?jǐn)?shù)組,同時單點(diǎn)查詢也就變成了差分?jǐn)?shù)組上的前綴和操作。
我們繼續(xù)來考慮一個根據(jù)有一般性的問題
POJ3468 A Simple Problem with Integers
題目要求實(shí)現(xiàn)區(qū)間修改和區(qū)間求和
為了將區(qū)間修改轉(zhuǎn)化為單點(diǎn)修改弄屡,我們?nèi)杂脴錉顢?shù)組b維護(hù)序列變化的差分?jǐn)?shù)組题禀,具體地說,b數(shù)組初始全部為0膀捷,若輸入指令C l r d迈嘹,則令b[l]+d,b[r+1]-d全庸。
那么l~r上新增的量就是\sum_{i=l}^{r}\sum_{j=1}^{i}b[j]=\sum_{i=1}^{r}\sum_{j=1}^{i}b[j]-\sum_{i=1}^{l-1}\sum_{j=1}^{i}b[j]秀仲。
考慮如何求\sum_{i=1}^{x}\sum_{j=1}^{i}b[j]
將上式展開\sum_{i=1}^{x}\sum_{j=1}^{i}b[j]=(b[1])+(b[1]+b[2])+(b[1]+b[2]+b[3])+...+(b[1]+b[2]+...+b[x])\\=(x)*b[1]+(x-1)*b[2]+...+(x-(x-1))*b[x]\\=\sum_{i=1}^{x}(x-i+1)*b[i]
此時我們將三個變量x,i壶笼,j消去變量j神僵,變成了兩個變量。
由于求和式中同時包含x和i覆劈,不易計(jì)算保礼。而樹狀數(shù)組擅于計(jì)算\sum_{i=1}^{x}b[i],因此责语,我們將x和i分離炮障,即將上式變形如下
\sum_{i=1}^{x}(x-i+1)*b[i]=(x+1)\sum_{i=1}^{x}b[i]-\sum_{i=1}^{x}i*b[i]
因此我們建立兩個樹狀數(shù)組c_{0}c_{1}鹦筹,分別用于維護(hù)b[i]和i×b[i]铝阐。
具體地說,若輸入指令C l r d铐拐,則執(zhí)行以下四個操作
1 c_{0}的l位置+d
2 c_{0}的r+1位置-d
3 c_{1}的l位置+l×d
4 c_{1}的r+1位置-(r+1)×d
用sum數(shù)組維護(hù)原序列的前綴和徘键,對于指令Q l r,只需要輸出sum[r]+(r+1)*ask(c_{0},r)-ask(c_{1},r)-(sum[l-1]+(l)*ask(c_{0},l-1)-ask(c_{1},l-1))
代碼如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
using namespace std;
typedef long long ll;
const int maxn=100000+5;
const int INF=0x3f3f3f3f;
ll c[2][maxn],n,q,a[maxn],sum[maxn];
void add(int k,int x,ll v){
    for(int i=x;i<=n;i+=i&-i){
        c[k][i]+=v;
    }
}
ll ask(int k,int x){
    ll ans=0; 
    for(int i=x;i>0;i-=i&-i){
        ans+=c[k][i];
    }
    return ans;
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("A Simple Problem with Integers.in","r",stdin);
    cin>>n>>q;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        sum[i]=sum[i-1]+a[i];
    }
    char op;
    int l,r;
    while(q--){
        cin>>op>>l>>r;
        ll d;
        if(op=='C'){
            cin>>d;
            add(0,l,d);
            add(0,r+1,-d);
            add(1,l,(ll)l*d);
            add(1,r+1,-(ll)(r+1)*d);
        }
        else{
            cout<<sum[r]-sum[l-1]+(r+1)*ask(0,r)-ask(1,r)-(l*ask(0,l-1)-ask(1,l-1))<<endl;
        }
    }

    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

POJ2182 Lost Cows
因?yàn)樗心膛I砀卟煌沂?~n的排列遍蟋,所以逆序考慮每一頭奶牛吹害,若最后一頭奶牛前面有A_{n}頭奶牛比它矮,那么它的身高為H_{n}=A_{n}+1虚青。
考慮倒數(shù)第二頭奶牛它呀,若它前面有A_{n-1}頭奶牛比它矮,那么:
1 若A_{n-1}<A_{n}棒厘,則它的身高H_{n-1}=A_{n-1}+1
2 若A_{n-1}\geq A_{n}纵穿,則它的身高H_{n-1}=A_{n-1}+2
以此類推,若第k頭奶牛前面有A_{k}頭奶牛比它矮奢人,那么它的身高H_{k}就是1~n中第A_{k}+1小的谓媒,且沒有在\left\{H_{k+1},H_{k+2},...,H_{n} \right\}中出現(xiàn)過的數(shù)。
具體實(shí)現(xiàn)中何乎,我們建立一個01序列b句惯,初始全為1土辩,表示所有身高都沒有出現(xiàn)過。逆序掃描n頭奶牛抢野,對于每頭奶牛執(zhí)行如下操作:
1 查詢b中第A_{i}+1個1在哪里拷淘,其位置就是第i頭奶牛的身高H_{i}
2 將b[H_{i}]變成0(減去1)
方法一:樹狀數(shù)組+二分
每次二分答案,通過樹狀數(shù)組求前綴和尋找第A_{i}+1個1
時間復(fù)雜度:O(nlog^{2}n)
方法二:樹狀數(shù)組+倍增
因?yàn)闃錉顢?shù)組天然維護(hù)了區(qū)間長度為2的整數(shù)次冪的信息指孤,于是可以在O(nlogn)的時間內(nèi)求解
附上兩種方法的代碼

/*

*/
#define method_2
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
using namespace std;
typedef long long ll;
const int maxn=8000+5;
const int INF=0x3f3f3f3f;
ll n,c[maxn],a[maxn],sum=0,ans[maxn];
void add(ll x,ll val){
    for(int i=x;i<=n;i+=i&-i){
        c[i]+=val;
    }
}
ll ask(ll x){
    ll sum=0;
    for(int i=x;i>0;i-=i&-i){
        sum+=c[i];
    }
    return sum;
}
int main() {
    ios::sync_with_stdio(false);
    freopen("Lost Cows.in","r",stdin);
    memset(c,0,sizeof(c));
    cin>>n;
//  sum=(1+n)*n/2;
    a[1]=0;
    for(int i=1;i<=n;i++){
        add(i,1);
    }
    for(int i=2;i<=n;i++){
        cin>>a[i];
    }
    for(int i=n;i>=1;i--){
        int l=1,r=n;
        while(l<=r){
            int mid=(l+r)>>1;
            if(ask(mid)<a[i]+1) l=mid+1;
            else r=mid-1;
        }
        ans[i]=l;
        add(l,-1);
//      sum-=ans[i];
    }
    for(int i=1;i<=n;i++){
        cout<<ans[i]<<endl;
    }
    return 0;
}
#endif
#ifdef method_2
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
using namespace std;
typedef long long ll;
const int maxn=8000+5;
const int INF=0x3f3f3f3f;
ll n,c[maxn],a[maxn],sum=0,ans[maxn];
void add(ll x,ll val){
    for(int i=x;i<=n;i+=i&-i){
        c[i]+=val;
    }
}
ll ask(ll x){
    ll sum=0;
    for(int i=x;i>0;i-=i&-i){
        sum+=c[i];
    }
    return sum;
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("Lost Cows.in","r",stdin);
    memset(c,0,sizeof(c));
    cin>>n;
//  sum=(1+n)*n/2;
    a[1]=0;
    for(int i=1;i<=n;i++){
        add(i,1);
    }
    for(int i=2;i<=n;i++){
        cin>>a[i];
    }
    for(int i=n;i>=1;i--){
        ll ans1=0,sum=0;
        for(int j=log2(n);j>=0;j--){
            if(ans1+(1<<j)<=n&&sum+c[ans1+(1<<j)]<a[i]+1) sum+=c[ans1+(1<<j)],ans1+=(1<<j);
        }
        ans[i]=ans1+1;
        add(ans1+1,-1);
    }
    for(int i=1;i<=n;i++){
        cout<<ans[i]<<endl;
    }
    return 0;
}
#endif
#ifdef method_3
/*

*/

#endif

線段樹

線段樹的基本操作包括區(qū)間求和启涯,區(qū)間求最值(這里的最值是廣義的最值,不僅限于max和min)邓厕,單點(diǎn)修改逝嚎,單點(diǎn)查值。這些操作的時間復(fù)雜度均為O(nlogn)详恼。
類型定義

struct SegmentTree {
    int l, r;
    int dat;
} t[SIZE * 4]; 

建樹

void build(int p, int l, int r) {
    t[p].l = l, t[p].r = r;     
               if (l == r) { t[p].dat = a[l]; return; } 
               int mid = (l + r) / 2; 
    build(p*2, l, mid); 
    build(p*2+1, mid+1, r); 
    t[p].dat = max(t[p*2].dat, t[p*2+1].dat);
}

build(1, 1, n); 

單點(diǎn)修改

void change(int p, int x, int v) {
    if (t[p].l == t[p].r) { t[p].dat = v; return; } 
              int mid = (t[p].l + t[p].r) / 2;
    if (x <= mid) change(p*2, x, v); 
    else change(p*2+1, x, v); 
    t[p].dat = max(t[p*2].dat, t[p*2+1].dat);
}

change(1, x, v); 

區(qū)間求最值

int ask(int p, int l, int r) {
    if (l <= t[p].l && r >= t[p].r) return t[p].dat; 
    int mid = (t[p].l + t[p].r) / 2;
    int val = 0;
    if (l <= mid) val = max(val, ask(p*2, l, r)); 
    if (r > mid) val = max(val, ask(p*2+1, l, r)); 
    return val;
}

cout << ask(1, l, r) << endl; 

例題

4301 Can you answer on these queries III
考慮最大子段和的生成過程补君,在區(qū)間上維護(hù)六個信息:區(qū)間和sum,區(qū)間最大子段和dat昧互,緊貼左端點(diǎn)的最大子段和lmax挽铁,緊貼右端點(diǎn)的最大子段和rmax。
分三類討論敞掘,得到轉(zhuǎn)移如下
tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
tree[p].lmax=max(tree[p<<1].lmax,tree[p<<1].sum+tree[p<<1|1].lmax);
tree[p].rmax=max(tree[p<<1|1].rmax,tree[p<<1|1].sum+tree[p<<1].rmax);
tree[p].dat=max(tree[p<<1].dat,max(tree[p<<1|1].dat,tree[p<<1].rmax+tree[p<<1|1].lmax));
代碼如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
using namespace std;
typedef long long ll;
const int maxn=500000+5;
const int INF=0x3f3f3f3f;
int n,m,a[maxn];
struct node {
    int l,r,sum,lmax,rmax,dat;
    node(int tmp=0) {
        sum=lmax=rmax=dat=tmp;
    }
} tree[maxn<<2];
void build(int p,int l,int r) {
    tree[p].l=l,tree[p].r=r;
    if(l==r) {
        tree[p].sum=tree[p].dat=tree[p].lmax=tree[p].rmax=a[l];
        return;
    }
    int mid=(l+r)>>1;
    build(p<<1,l,mid);
    build(p<<1|1,mid+1,r);
    tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
    tree[p].lmax=max(tree[p<<1].lmax,tree[p<<1].sum+tree[p<<1|1].lmax);
    tree[p].rmax=max(tree[p<<1|1].rmax,tree[p<<1|1].sum+tree[p<<1].rmax);
//  tree[p].dat=max(tree[p<<1].dat,tree[p<<1|1].dat);
    tree[p].dat=max(tree[p<<1].dat,max(tree[p<<1|1].dat,tree[p<<1].rmax+tree[p<<1|1].lmax));
}
void change(int p,int x,int v) {
    if(tree[p].l==tree[p].r) {
        tree[p].sum=tree[p].dat=tree[p].lmax=tree[p].rmax=v;
        return;
    }
    int mid=(tree[p].l+tree[p].r)>>1;
    if(x<=mid) change(p<<1,x,v);
    else change(p<<1|1,x,v);
    tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
    tree[p].lmax=max(tree[p<<1].lmax,tree[p<<1].sum+tree[p<<1|1].lmax);
    tree[p].rmax=max(tree[p<<1|1].rmax,tree[p<<1|1].sum+tree[p<<1].rmax);
    tree[p].dat=max(tree[p<<1].dat,max(tree[p<<1|1].dat,tree[p<<1].rmax+tree[p<<1|1].lmax));
}
node ask(int p,int l,int r) {
    if(l<=tree[p].l&&r>=tree[p].r) {
        return tree[p];
    }
    int mid=(tree[p].l+tree[p].r)>>1;
//  int val=-INF;
    node a,b,c;
    if(r<=mid) return ask(p<<1,l,r);
    if(l>=mid+1) return ask(p<<1|1,l,r);
    else{
        a=ask(p<<1,l,r);
    b=ask(p<<1|1,l,r);
    c.sum=a.sum+b.sum;
    c.lmax=max(a.lmax,a.sum+b.lmax);
    c.rmax=max(b.rmax,b.sum+a.rmax);
    c.dat=max(a.dat,max(b.dat,a.rmax+b.lmax));
    return c;
    }
}
void print(int p,int l,int r) {
    if(l==r) cout<<p<<" "<<tree[p].lmax<<" "<<tree[p].rmax<<" "<<tree[p].sum<<" "<<tree[p].dat<<endl;
    int mid=(l+r)>>1;
    print(p<<1,l,mid);
    print(p<<1|1,mid+1,r);
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("Can you answer on these queries III.in","r",stdin);
    cin>>n>>m;
    for(int i=1; i<=n; i++) {
        cin>>a[i];
    }
    build(1,1,n);
    int x,y,z;
    while(m--)
    {
        cin>>x>>y>>z;
//      if(y>z) swap(y,z); //不能在這里swap 會影響命令2 
        if(x==1)cout<<ask(1,min(y,z),max(y,z)).dat<<endl;
        else change(1,y,z);
    }
    return 0;
}
#endif

4302 Interval GCD
因?yàn)間cd(x,y,z)=gcd(x,y-x,z-y)
所以用線段樹維護(hù)原序列的差分序列的gcd叽掘。
又因?yàn)榍髤^(qū)間gcd的時候,需要區(qū)間第一個數(shù)的原始值玖雁,所以額外用一個樹狀數(shù)組維護(hù)原值更扁。
代碼如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=5e5+5;
const int INF=0x3f3f3f3f;
ll n,m;
struct SegmentTree {
    int l,r;
    ll dat;
} t[maxn<<2];
ll a[maxn],b[maxn],c[maxn];
ll gcd(ll x,ll y) {
    return !y?x:gcd(y,x%y);
}
void build(int rt,int l,int r) {
    t[rt].l=l;
    t[rt].r=r;
    if(l==r) {
        t[rt].dat=b[l];
        return;
    }
    int mid=l+r>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
    t[rt].dat=gcd(t[rt<<1].dat,t[rt<<1|1].dat);
}
void change(int rt,int x,ll v) {
    if(t[rt].l==t[rt].r) {
        t[rt].dat+=v;
        return;
    }
    int mid=t[rt].l+t[rt].r>>1;
    if(mid>=x) change(rt<<1,x,v);
    else change(rt<<1|1,x,v);
    t[rt].dat=gcd(t[rt<<1].dat,t[rt<<1|1].dat);
}
ll ask(int rt,int l,int r) {
    if(t[rt].l>=l&&t[rt].r<=r) return abs(t[rt].dat);
    int mid=t[rt].l+t[rt].r>>1;
    ll val=0;
    if(l<=mid) val=gcd(val,ask(rt<<1,l,r));
    if(r>mid) val=gcd(val,ask(rt<<1|1,l,r));
    return abs(val);
}
void add(int x,ll v) {
    for(int i=x; i<=n; i+=i&-i) {
        c[i]+=v;
    }
}
ll sum(int x) {
    ll ans=0;
    for(int i=x; i>=1; i-=i&-i) {
        ans+=c[i];
    }
    return ans;
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("Interval GCD.in","r",stdin);
    cin>>n>>m;
    for(int i=1; i<=n; i++) {
        cin>>a[i];
    }
    b[1]=a[1];
    for(int i=2; i<=n; i++) {
        b[i]=a[i]-a[i-1];
    }
    build(1,1,n);
//  cout<<t[1].l<<" "<<t[1].r<<endl;
    char op;
    int l,r;
    ll d;
    while(m--) {
        /*
        for(int i=1;i<=n;i++){
            D(a[i]+sum(i));
        }
        E;
        */
        cin>>op;
        if(op=='Q') {
            cin>>l>>r;
            ll al=a[l]+sum(l);
            ll val=l<r?ask(1,l+1,r):0;
            cout<<gcd(ask(1,l+1,r),al)<<endl;
        } else {
            cin>>l>>r>>d;
            change(1,l,d);
            if(r<n) change(1,r+1,-d);
            add(l,d);
            if(r<n) add(r+1,-d);
        }
    }
    return 0;
}
#endif

通過“懶惰標(biāo)記/延遲標(biāo)記”的技巧,線段樹也能夠?qū)崿F(xiàn)O(nlogn)內(nèi)完成區(qū)間修改和區(qū)間查詢赫冬。

例題

4301 Can you answer on these queries III
關(guān)于延遲標(biāo)記這里不做解釋浓镜,代碼如下
類型定義

struct SegmentTree{
    int l,r;
    long long sum,add;
    #define l(x) tree[x].l
    #define r(x) tree[x].r
    #define sum(x) tree[x].sum
    #define add(x) tree[x].add
}tree[SIZE*4];

建樹

void build(int p,int l,int r)
{
    l(p)=l,r(p)=r;
    if(l==r) { sum(p)=a[l]; return; }
    int mid=(l+r)/2;
    build(p*2,l,mid);
    build(p*2+1,mid+1,r);
    sum(p)=sum(p*2)+sum(p*2+1);
}

傳遞標(biāo)記

void spread(int p) {
    if(add(p)) {
        sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1);
        sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1);
        add(p*2)+=add(p);
        add(p*2+1)+=add(p);
        add(p)=0;
    }
}

區(qū)間修改

void change(int p,int l,int r,int z) {
    if(l<=l(p)&&r>=r(p)) {
        sum(p)+=(long long)z*(r(p)-l(p)+1);
        add(p)+=z;
        return;
    }
    spread(p);
    int mid=(l(p)+r(p))/2;
    if(l<=mid) change(p*2,l,r,z);
    if(r>mid) change(p*2+1,l,r,z);
    sum(p)=sum(p*2)+sum(p*2+1);
}

區(qū)間求和

long long ask(int p,int l,int r) {
    if(l<=l(p)&&r>=r(p)) return sum(p);
    spread(p);
    int mid=(l(p)+r(p))/2;
    long long ans=0;
    if(l<=mid) ans+=ask(p*2,l,r);
    if(r>mid) ans+=ask(p*2+1,l,r);
    return ans;
}

另外,線段樹可以處理多維偏序問題的一個維度劲厌。因此膛薛,線段樹可以和排序結(jié)合求解二維偏序問題,可以和平衡樹結(jié)合構(gòu)成樹套樹求解三維/多維偏序問題补鼻。(類似的哄啄,樹狀數(shù)組結(jié)合排序可以求解某些不需要求最值的二維偏序問題,樹狀數(shù)組+排序+CDQ分治可以離線求解三維偏序問題)
這里講解線段樹維護(hù)掃描線+排序離散化求解二維偏序問題

例題

POJ1151 Atlantis
我們用一條豎直直線從左向右掃過整個坐標(biāo)系风范,那么直線上被并集圖形覆蓋的長度只會在每個矩形的左右邊界發(fā)生變化咨跌。
因此,我們只要知道掃描線在每段上被覆蓋的長度硼婿,乘以該段距離虑润,對所有這樣的值求和就是答案了。
具體實(shí)現(xiàn)中加酵,我們設(shè)每個矩形左上角為(x_{1},y_{1})拳喻,右下角為(x_{2},y_{2}),矩形的左邊界四元組(x_{1},y_{1},y_{2},1)猪腕,右邊界四元組(x_{2},y_{1},y_{2},-1)冗澈,c[i]表示掃描線上第i段被覆蓋的次數(shù)。
首先對y坐標(biāo)離散化陋葡,val(y)表示y被離散化后的整數(shù)值亚亲,raw[i]表示離散化后的i對應(yīng)到實(shí)際坐標(biāo)中的y值。
對于每個四元組(x,y_{1},y_{2},k)腐缤,把c數(shù)組中的c[val(y_{1})],c[val(y_{1}+1)],c[val(y_{1}+2)],...,c[val(y_{2}-1)]全部+k捌归,相當(dāng)于覆蓋了[y_{1},y_{2}]這個區(qū)間(因?yàn)閏數(shù)組的含義是區(qū)間,所以y_{1}y_{2}的區(qū)間上限是val(y_{2}-1)而不是val(y_{2}))岭粤。
若下一個四元組橫坐標(biāo)為x_{2}惜索,那么這段面積就是(x_{2}-x)*\sum_{c[i]>0}(raw[i+1]-raw[i]),對于每個四元素剃浇,樸素的在c數(shù)組上修改巾兆,時間復(fù)雜度為O(n^{2})(本題實(shí)現(xiàn)寬裕,這種方法已經(jīng)可以AC虎囚。
考慮用線段樹維護(hù)c數(shù)組角塑,把時間復(fù)雜度優(yōu)化到O(nlogn)
方法一:運(yùn)用延遲標(biāo)記實(shí)現(xiàn)區(qū)間修改和查詢
方法二:由于我們只關(guān)心線段樹根節(jié)點(diǎn)(整個掃描線)上被矩形覆蓋的長度,并且四元組總是成對出現(xiàn)淘讥,所以線段樹上的區(qū)間修改也是成對的圃伶,這種情況下,沒有必要向下傳遞延遲標(biāo)記蒲列。換句話說窒朋,統(tǒng)計(jì)時只考慮根節(jié)點(diǎn),所系不用向下傳遞標(biāo)記拭宁。
具體實(shí)現(xiàn)上腔剂,我們除了維護(hù)區(qū)間左右端點(diǎn)外宙攻,我們在線段樹的每個節(jié)點(diǎn)上維護(hù)兩個值:節(jié)點(diǎn)代表區(qū)間被覆蓋的長度len,節(jié)點(diǎn)被覆蓋次數(shù)cnt喉前,初始時兩者均為0。
對于四元組(x,y_{1},y_{2},k)视事,我們在[val()y_{1},val(y_{2})]上區(qū)間修改锈拨。該區(qū)間被線段樹劃分成O(logn)個節(jié)點(diǎn),我們將這些節(jié)點(diǎn)的cnt+=k。
對于線段中任意一個節(jié)點(diǎn)[l,r]留潦,若cnt>0十偶,則len=raw(r+1)-raw(l)(同樣,由于線段樹維護(hù)的是區(qū)間,所以這里是r+1猛频,而不是r)训柴。否則幻馁,該點(diǎn)的len為兩個子節(jié)點(diǎn)的len之和(需特判葉子節(jié)點(diǎn))膘滨。在一個節(jié)點(diǎn)的cnt被修改,以及線段樹從下向上回溯時稀拐,我們這樣更新len火邓,最后根節(jié)點(diǎn)的len就是整個掃描線上被覆蓋的長度。
代碼如下德撬,其中method_1是樸素做法铲咨,method_2是優(yōu)化做法中的方法二(spread函數(shù)和上面延遲標(biāo)記中的spread函數(shù)不同,這里的spread只含有向上合并的操作)蜓洪。

/*

*/
#define method_2
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=200+5;
const int INF=0x3f3f3f3f;
int n,num,c[maxn];
double raw[maxn];
struct node{
    double X,Y1,Y2;
    int f;
    bool operator<(const node &h)const{
        return X<h.X;
    }
}line[maxn];
int val(double y){
    return lower_bound(raw+1,raw+num+1,y)-raw;
}
int main() {
    ios::sync_with_stdio(false);
    //freopen("Atlantis.in","r",stdin);
    int kase=0;
    while(scanf("%d",&n)&&n){
        printf("Test case #%d\n",++kase);
        memset(c,0,sizeof(c));
        memset(raw,0,sizeof(raw));
        num=0;
        double X1,Y1,X2,Y2;
        for(int i=1;i<=n;i++){
            scanf("%lf%lf%lf%lf",&X1,&Y1,&X2,&Y2);
            line[2*i-1].X=X1;
            line[2*i-1].Y1=Y1;
            line[2*i-1].Y2=Y2;
            line[2*i-1].f=1;
            line[2*i].X=X2;
            line[2*i].Y1=Y1;
            line[2*i].Y2=Y2;
            line[2*i].f=-1;
            raw[++num]=Y1;
            raw[++num]=Y2;
        }
        sort(line+1,line+2*n+1);
        sort(raw+1,raw+num+1);
        num=unique(raw+1,raw+num+1)-raw-1;
        double ans=0.0;
        for(int i=1;i<=2*n-1;i++){
            double X=line[i].X;
            double X2=line[i+1].X; 
            double Y1=line[i].Y1;
            double Y2=line[i].Y2;
    //      printf("%lf %lf %lf %lf\n",X,X2,Y1,Y2);
    //      printf("%d %d\n",val(Y1),val(Y2));
            
            int f=line[i].f;
            for(int j=val(Y1);j<=val(Y2)-1;j++){
                c[j]+=f;
            }
            for(int j=1;j<=num-1;j++){
                if(c[j]>0){
            //      printf("%lf %lf\n",raw[j+1],raw[j]);
                    ans+=(X2-X)*(raw[j+1]-raw[j]);
                }
            }
        }
        printf("Total explored area: %.2lf\n\n",ans);
    }
    return 0;
}
#endif
#ifdef method_2
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=200+5;
const int INF=0x3f3f3f3f;
int n,num,c[maxn];
double raw[maxn];
struct node{
    double X,Y1,Y2;
    int f;
    bool operator<(const node &h)const{
        return X<h.X;
    }
}line[maxn];
struct SegmentTree{
    int l,r,cnt;
    double len;
}tr[maxn<<2];
int val(double y){
    return lower_bound(raw+1,raw+num+1,y)-raw;
}
void build(int rt,int l,int r){
    tr[rt].l=l,tr[rt].r=r;
    if(l==r){
        tr[rt].cnt=0;
        tr[rt].len=0;
        return;
    }
    int mid=l+r>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
    tr[rt].cnt=0;
    tr[rt].len=0;
}
void spread(int p){
    if(tr[p].cnt>0) tr[p].len=raw[tr[p].r+1]-raw[tr[p].l];
    else if(tr[p].l==tr[p].r) tr[p].len=0; //這句話一定要有 作為葉子節(jié)點(diǎn)的邊界 否則會合并到未知位置 
    else tr[p].len=tr[p<<1].len+tr[p<<1|1].len;
}
void update(int rt,int l,int r,int v){
    if(tr[rt].l>=l&&tr[rt].r<=r){
        tr[rt].cnt+=v;
        spread(rt);
        return;
    }
    int mid=tr[rt].l+tr[rt].r>>1;
    if(l<=mid) update(rt<<1,l,r,v);
    if(r>mid) update(rt<<1|1,l,r,v);
    spread(rt);
}
int main() {
    ios::sync_with_stdio(false);
    //freopen("Atlantis.in","r",stdin);
    int kase=0;
    while(scanf("%d",&n)&&n){
        printf("Test case #%d\n",++kase);
        memset(c,0,sizeof(c));
        memset(raw,0,sizeof(raw));
        num=0;
        double X1,Y1,X2,Y2;
        for(int i=1;i<=n;i++){
            scanf("%lf%lf%lf%lf",&X1,&Y1,&X2,&Y2);
            line[2*i-1].X=X1;
            line[2*i-1].Y1=Y1;
            line[2*i-1].Y2=Y2;
            line[2*i-1].f=1;
            line[2*i].X=X2;
            line[2*i].Y1=Y1;
            line[2*i].Y2=Y2;
            line[2*i].f=-1;
            raw[++num]=Y1;
            raw[++num]=Y2;
        }
        sort(line+1,line+2*n+1);
        sort(raw+1,raw+num+1);
        num=unique(raw+1,raw+num+1)-raw-1;
        build(1,1,num-1);
        double ans=0.0;
        for(int i=1;i<=2*n-1;i++){
            double X=line[i].X;
            double X2=line[i+1].X; 
            double Y1=line[i].Y1;
            double Y2=line[i].Y2;
            update(1,val(Y1),val(Y2)-1,line[i].f);
            ans+=(X2-X)*tr[1].len;
        }
        printf("Total explored area: %.2lf\n\n",ans);
    }
    return 0;
}
#endif
#ifdef method_3
/*

*/

#endif

POJ2482 Stars in Your Window
由于矩形長寬確定且不可旋轉(zhuǎn)纤勒,我們討論矩形右上角的位置。
逆向思考蝠咆,我們發(fā)現(xiàn)踊东,對于星星(x,y),能夠圈住它的矩形的右上角的坐標(biāo)范圍是(x,y)到(x+w,y+h)刚操,我們稱其為一個區(qū)域闸翅,區(qū)域的權(quán)值就是星星的亮度。將所有星星對應(yīng)的區(qū)域全部找出來后菊霜,問題轉(zhuǎn)化為若干個區(qū)域中坚冀,求在那個坐標(biāo)上區(qū)域重疊的權(quán)值和最大
求區(qū)域重疊的權(quán)值和最大鉴逞,換句話說记某,我們需要維護(hù)一個求二維區(qū)間最值的數(shù)據(jù)結(jié)構(gòu)。類比上一題的做法构捡,我們從左至右掃描液南,同時關(guān)于縱坐標(biāo)建立一棵支持區(qū)間修改和維護(hù)區(qū)間最值的線段樹,對于四元組(x,y_{1},y_{2},c)勾徽,在線段樹中滑凉,將對應(yīng)的區(qū)間[y_{1},y_{2}]+c即可,最后根節(jié)點(diǎn)的dat用于更新答案。
PS:由于本題認(rèn)為矩形邊界上的星星不算畅姊,我們將矩形做縮放咒钟,即每個星星限定的區(qū)域是(x,y)到(x+w-1,y+h-1)
代碼如下(這里用延遲標(biāo)記實(shí)現(xiàn)了區(qū)間修改)

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=20000+5;
const int INF=0x3f3f3f3f;
struct SegmentTree{
    ll l,r;
    ll dat,add;
}t[maxn<<2];
struct node{
    double X,Y1,Y2;
    ll f;
    bool operator<(const node &h)const{
        return X!=h.X?X<h.X:f<h.f;
    }
}a[maxn];
ll num,n,W,H,b[maxn];
void build(ll rt,ll l,ll r){
    t[rt].l=l,t[rt].r=r;
    t[rt].dat=t[rt].add=0;
    if(l==r){
        return;
    }
    ll mid=l+r>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
}
void spread(ll rt){
    if(t[rt].add){
        t[rt<<1].dat+=t[rt].add;
        t[rt<<1].add+=t[rt].add;
        t[rt<<1|1].dat+=t[rt].add;
        t[rt<<1|1].add+=t[rt].add;
        t[rt].add=0;
    }
}
void change(ll rt,ll l,ll r,ll v){
    if(t[rt].l>=l&&t[rt].r<=r){
        t[rt].add+=v;
        t[rt].dat+=v;
        return;
    }
    spread(rt);
    ll mid=(t[rt].l+t[rt].r)>>1;
    if(mid>=l) change(rt<<1,l,r,v);
    if(mid<r) change(rt<<1|1,l,r,v);
    t[rt].dat=max(t[rt<<1].dat,t[rt<<1|1].dat);
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("Stars in Your Window.in","r",stdin);
    while(cin>>n>>W>>H){
        ll x,y,z;
        num=0;
        for(int i=1;i<=n;i++){
            cin>>x>>y>>z;
            a[2*i-1].Y1=a[2*i].Y1=y;
            a[2*i-1].Y2=a[2*i].Y2=y+H-1;
            a[2*i-1].X=x;
            a[2*i].X=x+W;
            a[2*i-1].f=z;
            a[2*i].f=-z;
            b[++num]=y;
            b[++num]=y+H-1; 
        }
        sort(b+1,b+num+1);
        num=unique(b+1,b+num+1)-b-1;
        for(int i=1;i<=2*n;i++){
            a[i].Y1=lower_bound(b+1,b+num+1,a[i].Y1)-b;
            a[i].Y2=lower_bound(b+1,b+num+1,a[i].Y2)-b;
        }
        sort(a+1,a+2*n+1);
        build(1,1,num);
        ll ans=0;
        for(int i=1;i<=2*n;i++){
            change(1,a[i].Y1,a[i].Y2,a[i].f);
            ans=max(ans,t[1].dat);
        }
        cout<<ans<<endl;
    }
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

另外,有些計(jì)數(shù)問題中若未,線段樹可用于維護(hù)值域(權(quán)值線段樹)朱嘴,為了避免空間復(fù)雜度過高,權(quán)值線段樹會運(yùn)用動態(tài)開點(diǎn)的方式進(jìn)行粗合,這樣它沒有了完全二叉樹父子節(jié)點(diǎn)二倍編號的特性萍嬉,轉(zhuǎn)而用指針變量記錄左右兒子。(例題 6302 雨天的尾巴
類型定義

struct node1 {
    int lc,rc,dat,pos;
} tr[maxn*20*4]; //不動態(tài)開點(diǎn)的話 需要的空間就是maxn*maxn 因?yàn)閷τ诿總€點(diǎn)都要維護(hù)1e5種類型 這里的20=log2(maxn)
//也就是說動態(tài)開點(diǎn)的線段樹空間大概為maxn log(maxn)*4 
//每進(jìn)行一次插入隙疚,會添加log級的點(diǎn)帚湘,因此開nlogn級數(shù)組即可。

初始化(開始的時候甚淡,n棵權(quán)值線段樹每棵線段樹只有根節(jié)點(diǎn))

for(int i=1; i<=n; i++) {
    root[i]=++num;
}

插入節(jié)點(diǎn)(維護(hù)區(qū)間最值和區(qū)間最值取到的位置)

void insert(int p,int l,int r,int val,int delta) {
    if(l==r) {
        tr[p].dat+=delta;
        if(tr[p].dat==0) tr[p].pos=0;
        else tr[p].pos=l;
        return;
    }
    int mid=(l+r)>>1;
    if(val<=mid) {
        if(!tr[p].lc) tr[p].lc=++num;
        insert(tr[p].lc,l,mid,val,delta);
    } else {
        if(!tr[p].rc) tr[p].rc=++num;
        insert(tr[p].rc,mid+1,r,val,delta);
    }
    tr[p].dat=max(tr[tr[p].lc].dat,tr[tr[p].rc].dat);
    tr[p].pos=tr[tr[p].lc].dat>=tr[tr[p].rc].dat?tr[tr[p].lc].pos:tr[tr[p].rc].pos;
}

線段樹合并(對應(yīng)計(jì)數(shù)問題中的值域合并過程)

int merge(int p,int q,int l,int r) {
    if(!p) return q;
    if(!q) return p;
    if(l==r) {
        tr[p].dat+=tr[q].dat;
        if(tr[p].dat==0) tr[p].pos=0;
        else tr[p].pos=l;
        return p;
    }
    int mid=(l+r)>>1;
    tr[p].lc=merge(tr[p].lc,tr[q].lc,l,mid);
    tr[p].rc=merge(tr[p].rc,tr[q].rc,mid+1,r);
    tr[p].dat=max(tr[tr[p].lc].dat,tr[tr[p].rc].dat);
    tr[p].pos=tr[tr[p].lc].dat>=tr[tr[p].rc].dat?tr[tr[p].lc].pos:tr[tr[p].rc].pos;
    return p;
}

BST和平衡樹

BST的原理這里不做解釋,直接上代碼捅厂。
建樹

struct BST {
    int l, r; 
    int val; 
}a[SIZE]; 
int tot, root, INF = 1<<30;

int New(int val) {
    a[++tot].val = val;
    return tot;
}

void Build() {
    New(-INF), New(INF);
    root = 1, a[1].r = 2;
}

檢索

int Get(int p, int val) {
    if (p == 0) return 0; 
    if (val == a[p].val) return p; 
    return val < a[p].val ? Get(a[p].l, val) : Get(a[p].r, val);
}

插入

void Insert(int &p, int val) {
    if (p == 0) {
        p = New(val); 
                return;
    }
    if (val == a[p].val) return;
    if (val < a[p].val) Insert(a[p].l, val);
    else Insert(a[p].r, val);
}

求前驅(qū)/后繼 這里以后繼為例

int GetNext(int val) {
    int ans = 2; // a[2].val==INF
    int p = root;
    while (p) {
        if (val == a[p].val) {
        if (a[p].r > 0) { 
                p = a[p].r;
                while (a[p].l > 0) p = a[p].l;
                ans = p;
            }
            break;
        }
        if (a[p].val > val && a[p].val < a[ans].val) ans = p;
        p = val < a[p].val ? a[p].l : a[p].r;
    }
    return ans;
}

刪除

void Remove(int val) { //注意p是引用
    int &p = root; //由于是引用 所以需要保存root的副本
    while (p) {
        if (val == a[p].val) break;
        p = val < a[p].val ? a[p].l : a[p].r;
    }
    if (p == 0) return;
    if (a[p].l == 0) p = a[p].r; 
    else if (a[p].r == 0) p = a[p].l;   
    else { 
        int next = a[p].r;
        while (a[next].l > 0) next = a[next].l;
        Remove(a[next].val);
        a[next].l = a[p].l, a[next].r = a[p].r;
        p = next; 
    }
}

若BST維護(hù)的是隨機(jī)序列贯卦,那么它的時間復(fù)雜度為O(nlogn),但是當(dāng)序列有序時焙贷,它的復(fù)雜度會退化成O(n^{2})撵割。由于滿足BST性質(zhì)且中序遍歷相同的二叉樹很多,而保證相同而形狀改變的操作叫旋轉(zhuǎn)辙芍。
通過旋轉(zhuǎn)啡彬,我們能讓一個不平衡的二叉樹逐漸變平衡。

image

左旋

void zig(int &p) { //注意這里p是引用而q不是引用
    int q = a[p].l;
    a[p].l = a[q].r, a[q].r = p, p = q;
    Update(a[p].r), Update(p);
}

右旋

void zag(int &p) {
    int q = a[p].r;
    a[p].r = a[q].l, a[q].l = p, p = q;
    Update(a[p].l), Update(p);
}

為了找到旋轉(zhuǎn)的條件故硅,我們在維護(hù)節(jié)點(diǎn)權(quán)值的同時庶灿,為每個節(jié)點(diǎn)分配一個隨機(jī)生成的額外權(quán)值,時刻維護(hù)這個額外權(quán)值滿足大根堆性質(zhì)吃衅,若不滿足時進(jìn)行旋轉(zhuǎn)即可往踢。

例題

4601 普通平衡樹
平衡樹模板題,這里不再贅述過程徘层,詳見代碼
PS:值得注意的是:
1 可能有數(shù)值重復(fù)峻呕,這時我們給每個節(jié)點(diǎn)一個cnt變量表示這個節(jié)點(diǎn)數(shù)值出現(xiàn)的次數(shù),當(dāng)cnt=0時刪除節(jié)點(diǎn)即可趣效。
2 同時這里需要維護(hù)排名瘦癌。我們可以給每個節(jié)點(diǎn)維護(hù)一個size表示以該節(jié)點(diǎn)為根,其所有節(jié)點(diǎn)的cnt值之和跷敬,當(dāng)不存在重復(fù)數(shù)值時讯私,size即為子樹大小。使用遞歸更新size即可。

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=100000+5;
const int INF=0x3f3f3f3f;
int n;
struct node {
    int l,r;
    int dat,val;
    int cnt,size;
} a[maxn];
int tot=0,root;
int New(int val) {
    a[++tot].val=val;
    a[tot].dat=rand();
    a[tot].cnt=a[tot].size=1;
    return tot;
}
void Update(int p) {
    a[p].size=a[p].cnt+a[a[p].l].size+a[a[p].r].size;
}
void Build() {
    New(-INF*2);
    New(INF*2);
    root=1,a[1].r=2;
    Update(root);
}
int GetRankByVal(int p,int val) {
    if(p==0) return 0;
    if(a[p].val==val) return a[a[p].l].size+1;
    if(a[p].val<val) return GetRankByVal(a[p].r,val)+a[p].cnt+a[a[p].l].size;
    return GetRankByVal(a[p].l,val);
}
int GetValByRank(int p,int rank) {
    if(p==0) return INF;
    if(a[a[p].l].size>=rank) return GetValByRank(a[p].l,rank);
    if(a[a[p].l].size+a[p].cnt>=rank) return a[p].val;
    return GetValByRank(a[p].r,rank-(a[a[p].l].size+a[p].cnt));
}
void zig(int &p) {
    int q=a[p].l;
    a[p].l=a[q].r,a[q].r=p,p=q;
    Update(a[p].r);
    Update(p);
}
void zag(int &p) {
    int q=a[p].r;
    a[p].r=a[q].l,a[q].l=p,p=q;
    Update(a[p].l);
    Update(p);
}
void Insert(int &p,int val) {
    if(p==0) {
        p=New(val);
        return;
    }
    if(val==a[p].val) {
        a[p].cnt++,Update(p);
        return;
    }
    if(val<a[p].val) {
        Insert(a[p].l,val);
        if(a[p].dat<a[a[p].l].dat) zig(p);
    }
    if(val>a[p].val) {
        Insert(a[p].r,val);
        if(a[p].dat<a[a[p].r].dat) zag(p);
    }
    Update(p);
}
int GetPre(int val) {
    int ans=1;
    int p=root;
    while(p) {
        if(val==a[p].val) {
            if(a[p].l>0) {
                p=a[p].l;
                while(a[p].r>0) p=a[p].r;
                ans=p;
            }
            break;
        }
        if(a[p].val<val&&a[p].val>a[ans].val) ans=p;
        p=val<a[p].val?a[p].l:a[p].r;
    }
    return a[ans].val;
}
int GetNext(int val) {
    int ans=2;
    int p=root;
    while(p) {
        if(val==a[p].val) {
            if(a[p].r>0) {
                p=a[p].r;
                while(a[p].l>0) p=a[p].l;
                ans=p;
            }
            break;
        }
        if(a[p].val>val&&a[p].val<a[ans].val) ans=p;
        p=val<a[p].val?a[p].l:a[p].r;
    }
    return a[ans].val;
}
void Remove(int &p,int val) {
    if(p==0) return;
    if(val==a[p].val) {
        if(a[p].cnt>1) {
            a[p].cnt--;
            Update(p);
            return;
        }
        if(a[p].l||a[p].r) {
            if(a[p].r==0||a[a[p].l].dat>a[a[p].r].dat) { //zig之后  a[p].r是a[p].l的父節(jié)點(diǎn)
                zig(p),Remove(a[p].r,val); //注意:這樣傳遞不會改變root的值妄帘,除非對p做出賦值才會改變root
            } else {
                zag(p),Remove(a[p].l,val);
            }
            Update(p);
        } else p=0;
        return;
    }
    val<a[p].val?Remove(a[p].l,val):Remove(a[p].r,val);
    Update(p);
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("普通平衡樹.in","r",stdin);
    cin>>n;
    Build();
    int op,x;
    for(int i=1; i<=n; i++) {
        cin>>op>>x;
        if(op==1) {
            Insert(root,x);
        }
        if(op==2) {
            Remove(root,x);
        }
        if(op==3) {
            cout<<GetRankByVal(root,x)-1<<endl;
        }
        if(op==4) {
            cout<<GetValByRank(root,x+1)<<endl;
        }
        if(op==5) {
            cout<<GetPre(x)<<endl;
        }
        if(op==6) {
            cout<<GetNext(x)<<endl;
        }
    }
    return 0;
}
#endif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末楞黄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子抡驼,更是在濱河造成了極大的恐慌鬼廓,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件致盟,死亡現(xiàn)場離奇詭異碎税,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)馏锡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門雷蹂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人杯道,你說我怎么就攤上這事匪煌。” “怎么了党巾?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵萎庭,是天一觀的道長。 經(jīng)常有香客問我齿拂,道長驳规,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任署海,我火速辦了婚禮吗购,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘砸狞。我一直安慰自己捻勉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布趾代。 她就那樣靜靜地躺著贯底,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撒强。 梳的紋絲不亂的頭發(fā)上禽捆,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機(jī)與錄音飘哨,去河邊找鬼胚想。 笑死,一個胖子當(dāng)著我的面吹牛芽隆,可吹牛的內(nèi)容都是我干的浊服。 我是一名探鬼主播统屈,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牙躺!你這毒婦竟也來了愁憔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤孽拷,失蹤者是張志新(化名)和其女友劉穎吨掌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脓恕,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膜宋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了炼幔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秋茫。...
    茶點(diǎn)故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖乃秀,靈堂內(nèi)的尸體忽然破棺而出肛著,到底是詐尸還是另有隱情,我是刑警寧澤跺讯,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布策泣,位于F島的核電站,受9級特大地震影響抬吟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜统抬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一火本、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聪建,春花似錦钙畔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挥下,卻和暖如春揍魂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棚瘟。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工现斋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人偎蘸。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓庄蹋,卻偏偏與公主長得像瞬内,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子限书,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 專業(yè)考題類型管理運(yùn)行工作負(fù)責(zé)人一般作業(yè)考題內(nèi)容選項(xiàng)A選項(xiàng)B選項(xiàng)C選項(xiàng)D選項(xiàng)E選項(xiàng)F正確答案 變電單選GYSZ本規(guī)程...
    小白兔去釣魚閱讀 8,990評論 0 13
  • 曾經(jīng)虫蝶, 我接受孤獨(dú), 我接受一人的生活倦西, 你的出現(xiàn)能真, 讓我想去分享, 想你參與我的生活调限, 想打開我的防備舟陆, 想擁有...
    一世由閱讀 358評論 0 0
  • 心得: “我不能說一種充滿成就的緊張人生就一定比充滿享受的輕松人生更好,但是我敢說耻矮,堅(jiān)強(qiáng)比軟弱好秦躯,而拼搏讓人堅(jiān)強(qiáng)。...
    Leah瀟閱讀 204評論 0 0
  • 1.為口感 重品飲者茎活,最好喝淡點(diǎn),因?yàn)榈醋镣伲前撞枳畋菊娴牟栉对乩螅裕藭r最好淺泡白茶采桃。 2.為保健 重保健者懒熙,最...
    紫砂壺友閱讀 490評論 0 0