談一類神奇的數據結構——貓樹

貓樹是一個有趣的數據結構,以前一直以爲這玩意兒應該很玄學,但學了以後發現仍是挺樸素也挺好打的數據結構html

→o→去我洛谷博客看唄?ios

1、貓樹的做用

學一個算法固然得先了解它的用處,那麼貓樹的做用嘛...git

簡單來說,線段樹能維護的信息貓樹基本都能維護算法

好比什麼區間和、區間 gcd 、最大子段和 等 知足結合律支持快速合併的信息數組

2、貓樹的算法實現

什麼都別說,我知道你想先知道貓樹是怎麼實現的數據結構

咱們就以區間和查詢爲例,假設當前查詢的區間爲 \([~l~,~r~]\)函數

那麼若是咱們在此以前預處理過某兩個區間的信息,且這兩個區間能夠合併成當前查詢區間,是否是就能夠 \(O(1)\) 獲得答案了呢?優化

可是問題就在於如何在一個較短的時間內預處理區間信息,而且使得任意一個區間都能被分紅兩份預處理過的區間ui

不扯了,進入正題

1.首先將 1~n 整個區間分紅兩份 1~mid , mid+1~nspa

2.而後對於這兩個區間,咱們先從中間點 mid 和 mid+1 出發,\(O(n)\) 地向兩邊遍歷區間中的每一個元素,同時維護要處理的信息

FAQ:怎麼維護?

這得看你要維護的信息,好比咱們舉例是區間和,那麼處理方式以下:

對於左邊的區間,i 倒序遍歷, \(f[i]=f[i+1]+a[i]\)

對於右邊的區間,i 正序遍歷, \(f[i]=f[i-1]+a[i]\)

3.等兩個區間都處理完以後,咱們再將兩個區間繼續分下去,重複迭代以上步驟直到區間左右邊界重合(即 \(l~=~r~\)

接着咱們考慮到這樣的迭代總共會有 \(log~ n\) 層,一個數都會在每一層中都被計算到一次,也就是說時間複雜度\(n~ log~ n\) 的,雖然比不上線段樹預處理的線性複雜度,但也已經可以讓人接受了

至於空間方面,咱們考慮向下迭代的長度相同的區間兩兩不相交,那麼他們其實能夠存在同一維數組裏面,也就是說咱們的空間複雜度也是 $n~ log~ n $ 的,在承受範圍以內

可是這裏還有一個問題:如何保證每一個區間都能被分紅兩份預處理過的區間?

其實咱們看到上面的處理方法使得

某個預處理過的區間 能夠將任意一個左右端點都在該區間內,且通過該區間中點的區間分紅兩份,而這兩份區間已經處理過了,那麼就能夠 \(O(1)\) 合併求解了

可能你已經玄學理解了,可是用圖仍是證實一下好了

Proof:

仍是畫圖好...下面是一個不斷向下迭代的區間

咱們先將查詢區間的兩個端點表示在總區間上

咱們發現這兩個點並不能被當前所在區間的中間點分到兩邊,因而咱們將他們下移,那麼這兩個點就一塊兒進入了右區間

咱們發現仍是他們仍是不能被中間點分紅兩份,繼續下移,一塊兒進入左區間

能夠被分紅兩份了,那麼咱們就成功地將該詢問區間分紅了兩個已處理的區間

根本緣由我已經在上面加粗了,沒錯,就是一塊兒,若是兩個點沒法被當前所處區間分到中間點的兩邊,那麼他們必然在該區間的左半部分或者右半部分,那麼就能夠同時進入某一邊的區間了

因而乎得證了...

3、貓樹的複雜度分析

而後,算法的複雜度總得分析的吧...

預處理複雜度

其實這個東西上面講過了,就是 \(O(n log~ n)\) , 漏看的同窗能夠翻回去了

詢問複雜度

咱們發現上面的預處理方式已經知足了咱們分割區間的要求,可是...

FAQ:按照上面的找尋分割點的方法,咱們發現複雜度好像是 \(O(log~ n)\)的? (這不仍是線段樹的複雜度?)

別急,上面只是證實分割的可行性,並非找尋分割點的方法

其實不難看出,若是咱們讓兩個點從葉子結點出發,不斷向上走知道相遇,那麼該區間的中間點就是它們的分割點。

emmm...兩個節點不斷向上走?這不是 \(LCA\) 嘛!那咱們就用倍增或者樹剖來找\(LCA\)

而後咱們會發現查詢複雜度神奇地變成了 \(O(log~log~n)\),已經比線段樹強了哈?

還不夠優秀?對,還能夠繼續優化

以前咱們有提到分割點在 \(LCA\) 上,那咱們能夠 \(O(1)\) 獲得兩個節點的 \(LCA\) 麼?ST表?貌似是能夠的哦,但其實不用這麼麻煩

咱們觀察一下就能夠發現(或者說根據線段樹的性質來講),兩個葉子結點的 \(LCA\) 的節點編號其實就是他們編號的最長公共前綴(二進制下)

Eg:

編號爲 $(10001)_2 $ 和 \((10110)_2\) 的兩個節點的 \(LCA\) 編號就是 \((10)_2\)

那麼怎麼快速求出兩個數的最長公共前綴?

這裏要用到很是妙的一個辦法:

咱們將兩個數異或以後能夠發現他們的公共前綴不見了,即最高位的位置後移了 \(\log LCA.len\) , 其中 \(LCA.len\) 表示 \(LCA\) 節點在二進制下的長度

那麼咱們就能夠預處理一下 log 數組,而後在詢問的時候就能夠快速求出兩個詢問節點的 \(LCA\) 所在的

等等,層?不用求出編號的麼?

那麼上面又說過的啊...咱們將長度相同的區間放在一維數組裏了啊,那麼咱們又知道這兩個區間的左右邊界,中間點又是肯定的,固然能夠在該層中獲得咱們想要的信息並快速合併起來了(這個的話仍是得看代碼理解的吧?)

綜上所述,咱們能夠在 O(1) 的時間複雜度內查詢區間

這複雜度比起線段樹都差一個 \(log\) 了,通常來說就是十幾倍的時間,然鵝本身造了數據測了測發現二者運行時間僅爲兩三倍,究其緣由的話仍是普通線段樹的 \(log\) 基本是跑不滿的(換句話說,我數據造太爛了...)

修改複雜度

修改?貓樹通常不拿來修改!

並且也有大佬向我提議說修改沒什麼用,但我以爲仍是講講(限制過大,僅供娛樂)

舉個例子:有些題目比較毒瘤,可能會給你的操做中大可能是查詢,少數是單點修改

那麼完蛋了,貓樹能支持修改麼?果斷棄坑

其實...貓樹能夠支持吧...

咱們在處理的時候用的是一個相似於前綴和的作法,那麼前綴和修改的複雜度是多少?(好吧通常來說帶修改就不用前綴和了,這裏只是舉個例子), \(O(n)\)

那麼咱們看看一個數在長度爲 n/2 、 n/4 、 n/8 .... 1 的區間內被作過前綴和,那麼修改的時候也就是要修改這些區間,而後這些區間長度加起來...就是 n 吧?

然鵝具體的代碼實現就不給出了,由於我懶 就在這裏給個思想,僅供娛樂

可是上面講的是單點修改,區間修改呢?

這個我真不會,並且也辦不到的...講道理改一次是 \(O(n~ log~ n)\) 的吧(至關於重建了),畢竟這也是性質決定的 (區間修改想都別想趕忙棄坑)

4、貓樹的代碼實現

以處理區間最大子段和爲例:

//by Judge
#include<cstdio>
#include<iostream>
#define ll long long
using namespace std;
const int M=2e5+3;
#ifndef Judge
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
#endif
char buf[1<<21],*p1=buf,*p2=buf;
inline int read(){ int x=0,f=1; char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
} char sr[1<<21],z[20];int C=-1,Z;
inline void Ot(){fwrite(sr,1,C+1,stdout),C=-1;}
inline void print(int x,char chr='\n'){
    if(C>1<<20)Ot();if(x<0)sr[++C]=45,x=-x;
    while(z[++Z]=x%10+48,x/=10);
    while(sr[++C]=z[Z],--Z);sr[++C]=chr;
} int n,m,a[M],len,lg[M<<2],pos[M],p[21][M],s[21][M];
// p 數組爲區間最大子段和, s 數組爲包含端點的最大子段和
inline int Max(int a,int b){return a>b?a:b;}
#define ls k<<1
#define rs k<<1|1
#define mid (l+r>>1)
#define lson ls,l,mid
#define rson rs,mid+1,r
void build(int k,int l,int r,int d){ //這裏的邊界是葉子結點
    //到達葉子後要記錄一下 位置 l 對應的葉子結點編號
    if(l==r) return pos[l]=k,void(); int prep,sm;
    // 處理左半部分
    p[d][mid]=s[d][mid]=prep=sm=a[mid],sm=Max(sm,0);
    for(int i=mid-1;i>=l;--i)
        prep+=a[i],sm+=a[i],s[d][i]=Max(s[d][i+1],prep),
        p[d][i]=Max(p[d][i+1],sm),sm=Max(sm,0);
    // 處理右半部分
    p[d][mid+1]=s[d][mid+1]=prep=sm=a[mid+1],sm=Max(sm,0);
    for(int i=mid+2;i<=r;++i)
        prep+=a[i],sm+=a[i],s[d][i]=Max(s[d][i-1],prep),
        p[d][i]=Max(p[d][i-1],sm),sm=Max(sm,0);
    build(lson,d+1),build(rson,d+1); //向下遞歸
}
inline int query(int l,int r){ if(l==r) return a[l];
    int d=lg[pos[l]]-lg[pos[l]^pos[r]];  //獲得 lca 所在層
    return Max(Max(p[d][l],p[d][r]),s[d][l]+s[d][r]);
}
int main(){ n=read(),len=2;
    while(len<n) len<<=1;
    for(int i=1;i<=n;++i) a[i]=read();
    for(int i=2,l=len<<1;i<=l;++i)
        lg[i]=lg[i>>1]+1;
    build(1,1,len,1);
    for(int m=read(),l,r;m;--m)
        l=read(),r=read(),
        print(query(l,r));
    return Ot(),0;
}

碼量其實會少不少,能夠看到最主要的碼量就在 \(build\) 裏面,可是 \(build\) 函數的思路仍是很清晰的

5、貓樹的推薦例題

GSS1

就是上面的板子

GSS5

不帶修改好開森,這題要求最大前綴 、 最大後綴,可是並不影響貓樹的發揮

用了貓樹以後直接 \(0 ms\)

FAQ:貌似不用也能夠啊...

可是貓樹碼量小吧...

FAQ:不見得啊....

...

下面是代碼

//by Judge
#include<cstdio>
#include<iostream>
#define ll long long
using namespace std;
const int M=2e4+3;
#ifndef Judge
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
#endif
inline void cmax(int& a,int b){if(a<b)a=b;}
inline void cmin(int& a,int b){if(a>b)a=b;}
char buf[1<<21],*p1=buf,*p2=buf;
inline int read(){ int x=0,f=1; char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
} char sr[1<<21],z[20];int C=-1,Z;
inline void Ot(){fwrite(sr,1,C+1,stdout),C=-1;}
inline void print(int x,char chr='\n'){
    if(C>1<<20)Ot();if(x<0)sr[++C]=45,x=-x;
    while(z[++Z]=x%10+48,x/=10);
    while(sr[++C]=z[Z],--Z);sr[++C]=chr;
} int n,m,a[M];
namespace cat_tree{ int len,lg[M<<1],pos[M];
    int p[16][M],s[16][M],f[16][M],g[16][M];
    #define ls k<<1
    #define rs k<<1|1
    #define mid (l+r>>1)
    #define lson ls,l,mid
    #define rson rs,mid+1,r
    inline int Max(int a,int b){return a>b?a:b;}
    inline void init(){ for(len=2;len<n;len<<=1);
        for(int i=1,l=len<<1;i<=l;++i) lg[i]=lg[i>>1]+1;
    }
    void build(int k,int l,int r,int d){
        if(l==r) return pos[l]=k,void(); int prep,sm;
        f[d][mid]=g[d][mid]=p[d][mid]=s[d][mid]=prep=sm=a[mid],sm=Max(sm,0);
        for(int i=mid-1;i>=l;--i)
            prep+=a[i],sm+=a[i],s[d][i]=prep,
            f[d][i]=Max(f[d][i+1],prep),g[d][i]=sm,
            p[d][i]=Max(p[d][i+1],sm),sm=Max(sm,0);
        f[d][mid+1]=g[d][mid+1]=p[d][mid+1]=s[d][mid+1]=prep=sm=a[mid+1],sm=Max(sm,0);
        for(int i=mid+2;i<=r;++i)
            prep+=a[i],sm+=a[i],s[d][i]=prep,
            f[d][i]=Max(f[d][i-1],prep),g[d][i]=sm,
            p[d][i]=Max(p[d][i-1],sm),sm=Max(sm,0);
        build(lson,d+1),build(rson,d+1);
    }
    inline int query_sum(int l,int r){
        if(l>r) return 0;if(l==r) return a[l];
        int d=lg[pos[l]]-lg[pos[l]^pos[r]];
        return s[d][l]+s[d][r];
    }
    inline int query_pre(int l,int r){
        if(l>r) return 0; if(l==r) return a[l];
        int d=lg[pos[l]]-lg[pos[l]^pos[r]];
        return Max(s[d][l]+f[d][r],g[d][l]);
    }
    inline int query_suf(int l,int r){
        if(l>r) return 0; if(l==r) return a[l];
        int d=lg[pos[l]]-lg[pos[l]^pos[r]];
        return Max(g[d][r],f[d][l]+s[d][r]);
    }
    inline int query_mid(int l,int r){
        if(l>r) return 0; if(l==r) return a[l];
        int d=lg[pos[l]]-lg[pos[l]^pos[r]];
        return Max(Max(p[d][l],p[d][r]),f[d][l]+f[d][r]);
    }
} using namespace cat_tree;
inline int query(int l1,int r1,int l2,int r2){ int ans;
    if(r1<l2) return query_sum(r1+1,l2-1)+query_suf(l1,r1)+query_pre(l2,r2);
    ans=l1<l2?Max(query_mid(l2,r1),query_suf(l1,l2)+query_pre(l2,r2)-a[l2]):query_mid(l2,r1);
    if(r2>r1) ans=Max(ans,query_suf(l1,r1)+query_pre(r1,r2)-a[r1]); return ans;
}
int main(){
    for(int T=read();T;--T){ n=read();
        for(int i=1;i<=n;++i) a[i]=read();
        init(),build(1,1,len,1);
        for(int m=read(),l1,r1,l2,r2;m;--m)
            l1=read(),r1=read(),
            l2=read(),r2=read(),
            print(query(l1,r1,l2,r2));
    } return Ot(),0;
}

其餘的能拿來當純模板的基本找不到(可見限制仍是蠻大的,畢竟帶修改的不行),不過一些要拿線段樹來優化的題目(好比線段樹優化 dp )仍是能夠用上的...吧?

參考資料:%%%zjp大佬

相關文章
相關標籤/搜索