淺談二維線段樹的幾種不同的寫法

參考文獻

四叉樹
樹套樹
以及和zhoufangyuan巨佬的激烈討論

參考文獻

大家好我口糊大師又回來了。

給你一個nnn*n矩陣,然後讓你支持兩種操作,對子矩陣加值和對子矩陣查和。

暴力寫法

對於每一行開一個線段樹,然後跑,時間複雜度n2lognn^2logn

優點:

  1. 代碼較短
  2. 較爲靈活

缺點:

  1. 常數大
  2. 容易卡

二叉樹

我們對於平面如此處理,一層維護橫切,一層豎切。

在這裏插入圖片描述

當然,這個做法也是n2lognn^2logn的,卡法就是任意一行的全加值。

優點:

  1. 時間複雜度比較平均。

缺點:

  1. 時間複雜度還是這麼垃圾。

四叉樹

那麼不能兩兩分就四四分!

即把一個區域分成四份。

在這裏插入圖片描述
當然還有這兩種特殊情況。

很可惜,一樣的卡法,還是那個n2lognn^2logn的味道。

來自參考文獻的代碼

//神奇的碼風,等我哪一天碼了一個類似的代碼,就把這個換了吧,但是最近沒時間。
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<cmath>
#define ll long long
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define fur(i,x,y) for(i=x;i<=y;i++)
#define fdr(i,x,y) for(i=x;i>=y;i--)
#define Fur(i,x,y) for(ll i=x;i<=y;i++)
#define Fdr(x,y) for(ll i=x;i>=y;i--)
#define in2(x,y) in(x);in(y)
#define in3(x,y,z) in2(x,y);in(z)
#define in4(a,b,c,d) in2(a,b);in2(c,d)
#define clr(x,y) memset(x,y,sizeof(x))
#define cpy(x,y) memcpy(x,y,sizeof(x))
using namespace std;
/*---------------------------------------*/
namespace fib{char b[300000]= {},*f=b;}
#define gc ((*fib::f)?(*(fib ::f++)):(fgets(fib::b,sizeof(fib::b),stdin)?(fib::f=fib::b,*(fib::f++)):-1))
inline void in(ll &x){x=0;char c;bool f=0;while((c=gc)>'9'||c<'0')if(c=='-')f=!f;x=c-48;while((c=gc)<='9'&&c>='0')x=x*10+c-48;if(f)x=-x;}
namespace fob{char b[300000]= {},*f=b,*g=b+300000-2;}
#define pob (fwrite(fob::b,sizeof(char),fob::f-fob::b,stdout),fob::f=fob::b,0)
#define pc(x) (*(fob::f++)=(x),(fob::f==fob::g)?pob:0)
struct foce{~foce(){pob;fflush(stdout);}} _foce;
namespace ib{char b[100];}
inline void out(ll x){if(x==0){pc(48);return;}if(x<0){pc('-');x=-x;}char *s=ib::b;while(x) *(++s)=x%10,x/=10;while(s!=ib::b) pc((*(s--))+48);}
inline void outn(ll x){out(x);pc('\n');}//快速輸出
inline ll jdz(ll x){return x>0?x:-x;}//絕對值
/*------------------------------------------------------------------------------------------------*/

/*------------------------------------------------------------------------------------------------*/
#define N 2334
#define c1 (rt*4-2)
#define c2 (rt*4-1)
#define c3 (rt*4)
#define c4 (rt*4+1)
#define z1 x1,y1,mx,my
#define z2 x1,my+1,mx,y2
#define z3 mx+1,y1,x2,my
#define z4 mx+1,my+1,x2,y2
#define Z ll mx=(x1+x2)>>1,my=(y1+y2)>>1
#define U ll x1,ll y1,ll x2,ll y2,ll rt
#define pu s[rt]=s[c1]+s[c2]+s[c3]+s[c4]//push_up
ll s[N*N*4],laz[N*N*4],a[N][N],n,m,q,g[N*N*4];
bool b[N*N*4];
inline ll gs(ll x1,ll y1,ll x2,ll y2){return (jdz(x1-x2)+1)*(jdz(y1-y2)+1);}
inline void pd(ll rt,ll n1,ll n2,ll n3,ll n4){
    if(laz[rt]){ll &t=laz[rt];
        s[c1]+=n1*t;s[c2]+=n2*t;s[c3]+=n3*t;s[c4]+=n4*t;
        laz[c1]+=t;laz[c2]+=t;laz[c3]+=t;laz[c4]+=t;
        t=0;
    }
}
inline void build(U){
    g[rt]=gs(x1,y1,x2,y2);
    if(x1==x2&&y1==y2){s[rt]=a[x1][y1];return;}
    Z;
    build(z1,c1);if(y1!=y2)build(z2,c2);else b[c2]=1;
    if(x1!=x2)build(z3,c3);else b[c3]=1;if(x1!=x2&&y1!=y2)build(z4,c4);else b[c4]=1;
    pu;
}
inline void upd(ll X1,ll Y1,ll X2,ll Y2,ll v,U){
    if(b[rt])return;
    if(X1<=x1&&Y1<=y1&&x2<=X2&&y2<=Y2){s[rt]+=gs(x1,y1,x2,y2)*v;laz[rt]+=v;return;}
    Z;pd(rt,g[c1],g[c2],g[c3],g[c4]);
    if(X1<=mx&&Y1<=my)upd(X1,Y1,X2,Y2,v,z1,c1);
    if(X1<=mx&&Y2>my)upd(X1,Y1,X2,Y2,v,z2,c2);
    if(X2>mx&&Y1<=my)upd(X1,Y1,X2,Y2,v,z3,c3);
    if(X2>mx&&Y2>my)upd(X1,Y1,X2,Y2,v,z4,c4);
    pu;
}
inline ll qh(ll X1,ll Y1,ll X2,ll Y2,U){
    if(b[rt])return 0;
    if(X1<=x1&&Y1<=y1&&x2<=X2&&y2<=Y2)return s[rt];
    Z,ans=0;pd(rt,g[c1],g[c2],g[c3],g[c4]);
    if(X1<=mx&&Y1<=my)ans+=qh(X1,Y1,X2,Y2,z1,c1);
    if(X1<=mx&&Y2>my)ans+=qh(X1,Y1,X2,Y2,z2,c2);
    if(X2>mx&&Y1<=my)ans+=qh(X1,Y1,X2,Y2,z3,c3);
    if(X2>mx&&Y2>my)ans+=qh(X1,Y1,X2,Y2,z4,c4);
    return ans;
}
int main(){
    in3(n,m,q);
    Fur(i,1,n)Fur(j,1,m)in(a[i][j]);
    build(1,1,n,m,1);
    ll p,x1,y1,x2,y2,v;
    while(q--){
        in(p);in4(x1,y1,x2,y2);
        if(p==1)outn(qh(x1,y1,x2,y2,1,1,n,m,1));
        else{in(v);upd(x1,y1,x2,y2,v,1,1,n,m,1);}
    }
}

優點:

  1. 常數更小了。

缺點:

  1. 代碼更長了。
  2. 時間複雜度吃屎般的大。

當然,最後被吐槽說上面兩種都是KDT,其實個人也覺得挺像的。

樹套樹寫法1

這纔是重頭戲。

論如何樹套樹搞?

當然這個一般只能維護區間賦值和區間加值,或者偶爾某些神奇的維護也可以用這個。

當然一般也可以用二維樹狀數組來搞,以後再學吧。

我們需要理解一個永久化標記的東西。就是曾經我以爲自己是第一個YY出這個的東西,上網一查,啪啪打臉的那玩意。

當一個點賦上了永久化標記,這個標記不會下傳,而是在遞歸的時候,在記錄標記。

對於外層線段樹,維護的是行,對於內層線段樹,維護的是列,不過代碼是兩棵,一棵是找。

如果一個外層節點維護的是1,21,2行,那麼他裏面維護的就是[(1,l),(2,r)][(1,l),(2,r)]的矩陣的信息。

對於外層葉子結點的內層線段樹,僅僅維護這一行的和,但是非葉子結點的內層線段樹的節點就是非葉子節點的左右兒子節點的內層線段樹的相應節點之和。

那麼我們來看一個例子:

在這裏插入圖片描述

圖片解釋:對於每個橙色節點,他都維護了一棵綠色的線段樹,那麼黃色就是我們加值的點,設加kk
在這裏插入圖片描述

那麼因爲加值在第11行,而藍色框住的節點維護的行範圍包括但不是等於第11行,我們就統計他的影響,很明顯在第22列總共加了kk(如果是豎着的兩格就是2k2k,不過第二個藍色框框是全覆蓋了),那麼對於維護第11行的葉子結點,我們再修改統計和的內層線段樹的同時,我們也要對於永久化標記的線段樹,給第22列打上kk的永久化標記。

而對於下面的進階情況:
在這裏插入圖片描述

我們需要對於外層根節點的統計和的內層線段樹在[2,3]列加上3k3k的值,因爲是統計和,所以我們可以統計出修改對於上層節點的內層線段樹的影響。(最大值就不能直接統計)

當然,外層節點[3,4][3,4]行的[2,3][2,3]列的和也是加kk,對於維護[1,2][1,2]行的節點,我們需要在[2,3][2,3]列打上永久化標記kk,這時候你問了,爲什麼不是2k2k,完全覆蓋一列2k2k呀?因爲我們以後到了這個節點,我們可以查出在[1,2][1,2]行的任意一行的[2,3][2,3]列打的標記,然後根據詢問的行數,再乘以行數,不然標記會侷限於當查詢行數完全覆蓋才能查標記導致TLE或者WA,打完標記後退出。

內層loglog,外層logloglog2log^2時間

查詢就是對於沿路經過的外層節點,查詢列的標記,乘以查詢的行數,並在查詢行數覆蓋的節點直接查和。

log2log^2

來自參考文獻的代碼:

//這個代碼還算友善
#include<cstdio>
#define gc getchar
int gi(){int x=0,f=0;char c=gc();while(c<'0'||'9'<c){if(c=='-')f=!f;c=gc();}while('0'<=c&&c<='9'){x=x*10+c-48;c=gc();}return f?(-x):x;}
using namespace std;
#define N 2010
int D,S,q;
struct xds{//內層(標記永久化)
    #define Z int m=(l+r)>>1
    #define ls rt<<1
    #define rs rt<<1|1
    int s[N*4],tag[N*4]/*內層也用永久化標記。。。*/;
    void build(int l,int r,int rt){//內層建樹
        if(l==r){s[rt]=gi();return;}
        Z;build(l,m,ls);build(m+1,r,rs);
        s[rt]=s[ls]+s[rs];
    }
    void upd(int L,int R,int v,int l,int r,int rt){//內層修改
        s[rt]+=v*(R-L+1);
        if(L==l&&r==R){
            tag[rt]+=v;
            return;
        }
        Z;
        if(R<=m)upd(L,R,v,l,m,ls);
        else{
            if(L>m)upd(L,R,v,m+1,r,rs);
            else upd(L,m,v,l,m,ls),upd(m+1,R,v,m+1,r,rs);
        }
    }
    int qh(int L,int R,int l,int r,int rt,int ad){
        if(L==l&&r==R)return s[rt]+ad*(r-l+1);
        Z;ad+=tag[rt];
        if(R<=m)return qh(L,R,l,m,ls,ad);
        else{
            if(L>m)return qh(L,R,m+1,r,rs,ad);
            else return qh(L,m,l,m,ls,ad)+qh(m+1,R,m+1,r,rs,ad);
        }
    }
}s[N*4]/*維護和*/,tag[N*4]/*維護永久化標記*/;
void mg(xds& o,xds& lc,xds& rc,int l,int r,int rt){//外層節點更新(pushup)
    o.s[rt]=lc.s[rt]+rc.s[rt];
    if(l==r)return;
    Z;mg(o,lc,rc,l,m,ls);mg(o,lc,rc,m+1,r,rs);
}
void build(int l,int r,int rt){//外層建樹
    if(l==r){
        s[rt].build(1,S,1);
        return;
    }
    Z;build(l,m,ls);build(m+1,r,rs);
    mg(s[rt],s[ls],s[rs],1,S,1);
}
void upd(int x,int y,int xx,int yy,int v,int l,int r,int rt){//外層修改
    s[rt].upd(y,yy,v*(xx-x+1),1,S,1);//沿路改和
    if(x==l&&r==xx){
        tag[rt].upd(y,yy,v,1,S,1);//改標記
        return;
    }
    Z;
    if(xx<=m)upd(x,y,xx,yy,v,l,m,ls);
    else{
        if(x>m)upd(x,y,xx,yy,v,m+1,r,rs);
        else upd(x,y,m,yy,v,l,m,ls),upd(m+1,y,xx,yy,v,m+1,r,rs);
    }
}
int qh(int x,int y,int xx,int yy,int l,int r,int rt,int ad){//查詢(求和)
    if(x==l&&r==xx)return s[rt].qh(y,yy,1,S,1,0)+ad*(r-l+1);
    Z;ad+=tag[rt].qh(y,yy,1,S,1,0);//查標記
    if(xx<=m)return qh(x,y,xx,yy,l,m,ls,ad);
    else{
        if(x>m)return qh(x,y,xx,yy,m+1,r,rs,ad);
        else return qh(x,y,m,yy,l,m,ls,ad)+qh(m+1,y,xx,yy,m+1,r,rs,ad);
    }    
}
int main(){
    D=gi();S=gi();q=gi();
    build(1,D,1);
    int p,x,y,xx,yy;
    while(q--){
        p=gi();x=gi();y=gi();xx=gi();yy=gi();
        if(p==1)printf("%d\n",qh(x,y,xx,yy,1,D,1,0));
        else upd(x,y,xx,yy,gi(),1,D,1);
    }
}

優點:

  1. 時間複雜度優秀。
  2. 因爲使用永久化標記,常數較小。以及用主席樹支持可持久化

缺點:

  1. 不能支持最大值。
  2. 思想複雜。
  3. 空間變大

樹套樹寫法2

如何支持求最大值呢。

比如最近這道題目

即使這道題目隨便用四叉樹爆叉。

但是時間複雜度還是太慢了!!!!

於是我就去請教機房巨佬之神,膜拜1e1e∞後,zhoufangyuan巨佬爲之撼動,教了我一個做法,繼續爆切黑題。

就是一樣是對於行完全覆蓋的外層節點的內層線段樹,我們一樣可以打個永久化標記,然後維護最大值,但是不一樣的是,我們發現跑每個葉子節點內層線段樹所經過的節點都是一樣的,且是lognlogn個的,我們可以先建個線段樹,把列的範圍扔進去,然後處理出那麼會被訪問節點。

在這裏插入圖片描述
就是藍色框框中的點。(而且因爲每層最多兩個節點被訪問,所以數量級爲lognlogn

然後我們一如既往的跑一遍外層修改,然後也是把行完全覆蓋的外層節點的內層節點改一下,然後對於行未完全覆蓋的外層節點,就是把自己內層線段樹中的藍色框住的點的最大值,去合併左右兒子藍色框柱的點的最大值就是了,這樣就可以用log2log^2代替原本log2log^2就可以計算的部分影響,無傷大雅,果然遠哥一想就是一個神仙。

其實就是把部分影響的維護方法給了一下,但是卻可以多維護大量的信息。

都是口糊的了,哪有代碼。

優點:

  1. 維護信息多。
  2. 時間複雜度優秀。

缺點:

  1. 代碼打起來有點抽。
  2. 大部分缺點與寫法1類似。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章