一、貓樹的作用
學一個演算法當然得先了解它的用處,那麼貓樹的作用嘛...
簡單來講,線段樹能維護的資訊貓樹基本都能維護
比如什麼區間和、區間 gcd 、最大子段和 等 滿足結合律且支援快速合併的資訊
二、貓樹的演算法實現
什麼都別說,我知道你想先知道貓樹是怎麼實現的
我們就以區間和查詢為例,假設當前查詢的區間為 [ l , r ]
那麼如果我們在此之前預處理過某兩個區間的資訊,且這兩個區間可以合併成當前查詢區間,是不是就可以 O(1) 得到答案了呢?
但是問題就在於如何在一個較短的時間內預處理區間資訊,並且使得任意一個區間都能被分成兩份預處理過的區間
不扯了,進入正題
1.首先將 1~n 整個區間分成兩份 1~mid , mid+1~n
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:
還是畫圖好...下面是一個不斷向下迭代的區間
我們先將查詢區間的兩個端點表示在總區間上
我們發現這兩個點並不能被當前所在區間的中間點分到兩邊,於是我們將他們下移,那麼這兩個點就一起進入了右區間
我們發現還是他們還是不能被中間點分成兩份,繼續下移,一起進入左區間
可以被分成兩份了,那麼我們就成功地將該詢問區間分成了兩個已處理的區間
根本原因我已經在上面加粗了,沒錯,就是一起,如果兩個點無法被當前所處區間分到中間點的兩邊,那麼他們必然在該區間的左半部分或者右半部分,那麼就可以同時進入某一邊的區間了
於是乎得證了...
三、貓樹的複雜度分析
然後,演算法的複雜度總得分析的吧...
預處理複雜度
其實這個東西上面講過了,就是 O(nlog n) , 漏看的同學可以翻回去了
詢問複雜度
我們發現上面的預處理方式已經滿足了我們分割區間的要求,但是...
FAQ:按照上面的找尋分割點的方法,我們發現複雜度好像是 O(log n)的? (這不還是線段樹的複雜度?)
別急,上面只是證明分割的可行性,並不是找尋分割點的方法
其實不難看出,如果我們讓兩個點從葉子結點出發,不斷向上走知道相遇,那麼該區間的中間點就是它們的分割點。
emmm...兩個節點不斷向上走?這不是 LCALCA 嘛!那我們就用倍增或者樹剖來找LCA ?
然後我們會發現查詢複雜度神奇地變成了 O(log log n),已經比線段樹強了哈?
還不夠優秀?對,還可以繼續最佳化
之前我們有提到分割點在 LCALCA 上,那我們可以 O(1) 得到兩個節點的 LCA 麼?ST表?貌似是可以的哦,但其實不用這麼麻煩
我們觀察一下就可以發現(或者說根據線段樹的性質來說),兩個葉子結點的 LCA 的節點編號其實就是他們編號的最長公共字首(二進位制下)
Eg:
編號為 (10001)2 和 (10110)2 的兩個節點的 LCA 編號就是 (10)2
那麼怎麼快速求出兩個數的最長公共字首?
這裡要用到非常妙的一個辦法:
我們將兩個數異或之後可以發現他們的公共字首不見了,即最高位的位置後移了 logLCA.len , 其中 LCA.len 表示 LCA 節點在二進位制下的長度
那麼我們就可以預處理一下 log 陣列,然後在詢問的時候就可以快速求出兩個詢問節點的 LCA 所在的 層 了
等等,層?不用求出編號的麼?
那麼上面又說過的啊...我們將長度相同的區間放在一維陣列裡了啊,那麼我們又知道這兩個區間的左右邊界,中間點又是確定的,當然可以在該層中得到我們想要的資訊並快速合併起來了(這個的話還是得看程式碼理解的吧?)
綜上所述,我們可以在 O(1) 的時間複雜度內查詢區間
這複雜度比起線段樹都差一個 log 了,一般來講就是十幾倍的時間,然鵝自己造了資料測了測發現兩者執行時間僅為兩三倍,究其原因的話還是普通線段樹的 log 基本是跑不滿的(換句話說,我資料造太爛了...)
修改複雜度
修改?貓樹一般不拿來修改!
而且也有大佬向我提議說修改沒什麼用,但我覺得還是講講(限制過大,僅供娛樂)
舉個例子:有些題目比較毒瘤,可能會給你的操作中大多是查詢,少數是單點修改
那麼完蛋了,貓樹能支援修改麼?果斷棄坑
其實...貓樹可以支援吧...
我們在處理的時候用的是一個類似於字首和的做法,那麼字首和修改的複雜度是多少?(好吧一般來講帶修改就不用字首和了,這裡只是舉個例子), O(n) !
那麼我們看看一個數在長度為 n/2 、 n/4 、 n/8 .... 1 的區間內被做過字首和,那麼修改的時候也就是要修改這些區間,然後這些區間長度加起來...就是 n 吧?
然鵝具體的程式碼實現就不給出了,因為我懶 就在這裡給個思想,僅供娛樂
但是上面講的是單點修改,區間修改呢?
這個我真不會,而且也辦不到的...講道理改一次是 O(n log n) 的吧(相當於重建了),畢竟這也是性質決定的 (區間修改想都別想趕緊棄坑)
四、貓樹的程式碼實現
以處理區間最大子段和為例:
//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,len,a[M]; int 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]=a[mid], s[d][mid]=a[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]=a[mid+1], s[d][mid+1]=a[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; for(;len<n;len<<=1); for(int i=1;i<=n;++i) a[i]=read();; int l=len<<1; for(int i=2;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; }
碼量其實會少很多,可以看到最主要的碼量就在 buildbuild 裡面,但是 buildbuild 函式的思路還是很清晰的
五、貓樹的推薦例題
就是上面的板子
不帶修改好開森,這題要求最大字首 、 最大字尾,但是並不影響貓樹的發揮
用了貓樹之後直接 0 ms0ms
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); int l=len<<1; for(int i=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]=a[mid]; p[d][mid]=s[d][mid]=a[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]=a[mid+1]; p[d][mid+1]=s[d][mid+1]=a[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=query_mid(l2,r1); if(l1<l2) ans=Max(ans,query_suf(l1,l2)+query_pre(l2,r2)-a[l2]); 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); int l1,r1,l2,r2; for(int m=read();m;--m){ l1=read(),r1=read(), l2=read(),r2=read(), print(query(l1,r1,l2,r2)); } } return Ot(),0; }
其他的能拿來當純模板的基本找不到(可見限制還是蠻大的,畢竟帶修改的不行),不過一些要拿線段樹來最佳化的題目還是可以用上的...吧?(比如線段樹最佳化 dp ...好像也不行呀,一般線段樹最佳化 dp 不都是帶修改的嘛...)