『zkw線段樹及其簡單運用』


<更新提示>

<第一次更新>
閱讀本文前,請確保已經閱讀並理解了如下兩篇文章:
『線段樹 Segment Tree』
『線段樹簡單運用』


<正文>

引入

這是一種由\(THU-zkw\)大佬發明的數據結構,本質上是經典的線段樹區間划分思想,采用了自底向上的方式傳遞區間信息,避免的遞歸結構,其代碼相對經典線段樹更簡單,常數更小,易於實現。

統計的力量-源自這里

基礎非遞歸

接下來,我們將講解\(zkw\)線段樹的第一種實現形式,用於單點修改 區間查詢,我們以查詢區間最大值為例來講解。

建樹

普通線段樹需要建樹,\(zkw\)線段樹當然也需要建樹。

考慮線段樹的一個性質,其樹上的葉節點代表的往往都是形如\([x,x]\)的元區間,而且除最后一層外,線段樹是一顆滿二叉樹,所以我們要把這顆線段樹的數組大小先申請好了。

一棵滿二叉樹有\(x\)個節點時,它有\(\frac{x+1}{2}\)個葉子節點,而我們需要至少\(n\)個葉子節點的線段樹,即使\(\frac{x+1}{2}\geq n\),那么我們設\(x=1\),在\(\frac{x+1}{2}<n\)時不斷執行\(x*=2\),就能得到足夠大小的線段樹下標\(base\),由於線段樹的葉子節點可能分布在兩層,所以保險起見,我們還需再將\(x\)擴大一倍,即在\(x+1<n\)時不斷執行\(x*=2\)就可以了。

得到合適的下標位置后,將\(1-n\)下標位置的原數據直接存入線段樹的葉子節點即可。

其實,我們還需將下標再擴大兩個位置,即需要保證\(x>n\),才停止執行\(x*=2\)。其原因是這樣的:在執行區間查詢操作時,我們需要將查詢區間\([l,r]\)更改為\((l,r)\)(關於原因,我們之后再分析),才便於\(zkw\)線段樹的查詢,那么在詢問\([1,n]\)時,可能為調用到\([0,n+1]\)的原下標,所以還需再擴大兩個位置。

得到了合適的下標\(base\)並將\(1-n\)的數據存入對應位置后,當然我們還要對\(1\)\(base-1\)的線段樹位置進行區間更新,這個普通的更新就可以了。

\(Code:\)

inline void reset(void)
{
    memset( val , 0 , sizeof val );
    base = 1;
}
inline void build(int *s,int len)
{
    for (;base<=len;base<<=1);
    for (int i=base+1;i<=base+len;i++)
        val[i] = s[i-base];
    for (int i=base-1;i>=1;i--)
        val[i] = max( val[i<<1] , val[i<<1|1] );
}

單點修改

直接在葉節點上修改對應的值,然后更新其每一個父節點即可。

\(Code:\)

inline void modify(int pos,int x)
{
    pos += base;val[pos] = x;
    for (pos>>=1;pos;pos>>=1)
        val[pos] = max( val[pos<<1] , val[pos<<1|1] );
}

區間查詢

我們先來看一個最大值線段樹。

其中,葉節點下面的橙色代表數組上的原數值,淡藍色代表線段樹對應節點的區間最大值,棕色代表查詢區間的范圍,如圖,我們需要查詢區間\([3,7]\)的最大值。

enter image description here

顯然,我們只要查詢上圖帶五角星的幾個線段樹節點的關鍵值,就能得知最大值。

\(zkw\)線段樹上,我們考慮如下一種方式:

enter image description here

先將閉區間\([3,7]\)拓展為開區間\((2,8)\),我們設兩個指針\(l=2,r=8\)

enter image description here

然后讓\(l,r\)依次向上找父親節點,直到兩個節點\(l,r\)的父親節點相同,我們停止向上查找。此時,結束位置的兩個節點標記了粉色星,需要查詢的節點還是標記的紅色星。

不難發現規律:當指針\(l\)經過的節點是一個左兒子時,或者當指針\(r\)經過的節點是一個右兒子時,它的兄弟就是一個需要查詢的節點。

對於一個查詢,我們只需將閉區間轉換為開區間,就能通過向上找父親的遍歷得到區間的答案,這就是使用開區間,並要求數組大小至少大於原大小兩個位置的原因。

\(Code:\)

inline int query(int l,int r)
{
    int res = 0;
    for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
    {
        if ( ~ l & 1 )res = max( res , val[l^1] );
        if ( r & 1 )res = max( res , val[r^1] );
    }
    return res;
}

至此,我們就能用極短的代碼和很高的效率實現單點修改,區間查詢的線段樹了。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N=100200;
int val[N<<2],base,n,a[N];
inline void read(int &k)
{
    int x=0,w=0;char ch;
    while (!isdigit(ch))
        w |= ch=='-' , ch=getchar();
    while (isdigit(ch))
        x = x*10 + ch-48 , ch=getchar();
    k=(w?-x:x);return;
}
inline void reset(void)
{
    memset( val , 0 , sizeof val );
    base = 1;
}
inline void build(int *s,int len)
{
    for (;base<=len;base<<=1);
    for (int i=base+1;i<=base+len;i++)
        val[i] = s[i-base];
    for (int i=base-1;i>=1;i--)
        val[i] = max( val[i<<1] , val[i<<1|1] );
}
inline void modify(int pos,int x)
{
    pos += base;val[pos] = x;
    for (pos>>=1;pos;pos>>=1)
        val[pos] = max( val[pos<<1] , val[pos<<1|1] );
}
inline int query(int l,int r)
{
    int res = 0;
    for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
    {
        if ( ~ l & 1 )res = max( res , val[l^1] );
        if ( r & 1 )res = max( res , val[r^1] );
    }
    return res;
}
int main(void)
{
    read(n);
    reset();
    build(a,n);
    for (int i=1;i<=n;i++)
    {
        int op,k1,k2;
        read(op);read(k1);read(k2);
        if (op==1)modify(k1,k2);
        if (op==2)printf("%d\n",query(k1,k2));
    }
    return 0;
}

簡單標記

在此,我們要實現\(zkw\)線段樹的第二種基本形式,用於區間修改 區間求和

標記永久化

對於區間修改 區間求和\(zkw\)線段樹,最重要的思想就是標記永久化的思想。

對於區間修改,我們在普通線段樹上是通過\(lazytag\)的標記方式實現的,對於修改和查詢操作調用到時,再下傳標記。而在\(zkw\)線段樹中,顯然向下傳遞標記的方式是毫無用武之地了。那么,我們引入一種新的標記思想:標記永久化

對於一個節點,若修改操作對節點所代表的整個區間產生影響,顯然我們可以直接對該節點進行標記,而非逐層遞歸修改。那么,在自底向上的線段樹中,我們可以不下傳標記,而是在每一次查詢時,統計累加一路上所有標記對答案產生的影響,這種標記思想被稱為標記永久化

建樹

該版本\(zkw\)線段樹的建樹方式和第一種形式的\(zkw\)線段樹的建樹方式一致,不再重復說明。

\(Code:\)

inline void build(void)
{
    for (;base<=n;base<<=1);
    for (int i=base+1;i<=base+n;i++)
        val[i] = a[i-base];
    for (int i=base-1;i>=1;i--)
        val[i] =  val[i<<1] + val[i<<1|1] ;
}

區間修改

關於標記永久化,我們進行定義:\(add_i\)代表線段樹中\(i\)號節點的關鍵值已經進行修改,但是其所有子節點均有一個值為\(add_i\)的增量未進行處理

我們采用上一版本\(zkw\)線段樹區間查詢的方式,設置兩個開區間指針\(l,r\),並同時向上遍歷。同時,我們維護三個變量\(lcnt,rcnt,cnt\),分別代表左指針處理增量的節點個數,右指針處理增量的節點個數,兩個指針當前所在節點左包含的葉節點個數

然后利用上述變量和\(add\)標記的定義,沿路更新\(add\)標記和原線段樹即可,當然,對於\(l,r\)成為兄弟后,我們還須將\(add\)標記一直上推到根節點。

\(Code:\)

inline void modify(int l,int r,long long delta)
{
    long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
    for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
    {
        val[l] += delta*lcnt;
        val[r] += delta*rcnt;
        if ( ~ l & 1 )
            add[l^1] += delta , val[l^1] += delta*cnt , lcnt += cnt;
        if ( r & 1 )
            add[r^1] += delta , val[r^1] += delta*cnt , rcnt += cnt;
    }
    for (; l || r ; l>>=1 , r>>=1 )
        val[l] += delta*lcnt , val[r] += delta*rcnt;
}

區間求和

有了\(add\)標記,我們就很容易求得區間的和了。還是一樣的方式,將閉區間轉換為開區間,然后向上遍歷,同樣維護\(lcnt,rcnt,cnt\),然后利用\(add\)標記進行累加,再加上原來的區間和,就能得到答案。

\(Code:\)

inline long long query(int l,int r)
{
    long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
    long long res = 0;
    for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
    {
        if (add[l]) res += add[l]*lcnt;
        if (add[r]) res += add[r]*rcnt;
        if ( ~ l & 1 )
            res += val[l^1] , lcnt += cnt;
        if ( r & 1 )
            res += val[r^1] , rcnt += cnt;
    }
    for (; l || r ; l>>=1 , r>>=1 )
        res += add[l]*lcnt , res += add[r]*rcnt;
    return res; 
}

至此,我們已經實現了支持區間修改,區間求和的\(zkw\)線段樹了,對於更多需要維護求和性質的值,也可以使用標記永久化的思想,這需要讀者理解掌握。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N=100020;
long long n,q,a[N],val[N<<2],base,add[N<<2];
inline void reset(void)
{
    memset( val , 0 , sizeof val );
    memset( add , 0 , sizeof add );
    base = 1;
}
inline void build(void)
{
    for (;base<=n;base<<=1);
    for (int i=base+1;i<=base+n;i++)
        val[i] = a[i-base];
    for (int i=base-1;i>=1;i--)
        val[i] =  val[i<<1] + val[i<<1|1] ;
}
inline void modify(int l,int r,long long delta)
{
    long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
    for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
    {
        val[l] += delta*lcnt;
        val[r] += delta*rcnt;
        if ( ~ l & 1 )
            add[l^1] += delta , val[l^1] += delta*cnt , lcnt += cnt;
        if ( r & 1 )
            add[r^1] += delta , val[r^1] += delta*cnt , rcnt += cnt;
    }
    for (; l || r ; l>>=1 , r>>=1 )
        val[l] += delta*lcnt , val[r] += delta*rcnt;
}
inline long long query(int l,int r)
{
    long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
    long long res = 0;
    for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
    {
        if (add[l]) res += add[l]*lcnt;
        if (add[r]) res += add[r]*rcnt;
        if ( ~ l & 1 )
            res += val[l^1] , lcnt += cnt;
        if ( r & 1 )
            res += val[r^1] , rcnt += cnt;
    }
    for (; l || r ; l>>=1 , r>>=1 )
        res += add[l]*lcnt , res += add[r]*rcnt;
    return res; 
}
inline void solve(void)
{
    scanf("%lld%lld",&n,&q);
    for (int i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    reset();
    build();
    for (int i=1;i<=q;i++)
    {
        char op;
        scanf("\n%c",&op);
        if (op=='C')
        {
            int l,r;long long delta;
            scanf("%d%d%lld",&l,&r,&delta);
            modify(l,r,delta);
        }
        if (op=='Q')
        {
            int l,r;
            scanf("%d%d",&l,&r);
            printf("%lld\n",query(l,r));
        }
    }
}
int main(void)
{
    freopen("b.in","r",stdin);
    freopen("b.out","w",stdout);
    solve();
    return 0;
}

差分思想和區間最值

接下來,我們將要嘗試實現使用區間查詢的另一種形式,區間最值的查詢。

用上述兩個模板稍微結合,更改一下難道不就可以實現區間修改,區間最值的\(zkw\)線段樹了嗎?答案是否定的。在區間修改的限制下,如果還用標記永久化的思想,由於標記的大小和位置未知,那么區間最值的查詢就會出問題。

差分思想

現在,我們線段樹上的節點將不再存對應區間的關鍵值了。我們需要用\(zkw\)線段樹來維護原關鍵值的差分值,若原來的\(val_i\)代表節點\(i\)所代表區間的最大值,則現在我們需要維護的\(val'_i=val_i-val_{i/2}\),特殊地,\(val_1\)仍代表整個區間的最大值。

可能讀者已經發現一點性質了:從任意葉節點\(y\)開始,一直向上找父親,並累加對應點的權值,就得到了原節點的權值。

其實,我們還可以用這樣的方式理解:\(val_i\)代表\(i\)節點所在區間的最大值比其父親節點所在區間最大值大多少(可能負數)。

建樹

還是可以利用和之前一樣的方式建樹,特殊地,在存完一個節點的值以后要利用\(val_i\)的定義來計算得到差分的值。

\(Code:\)

inline void build(void)
{
    for (;base<=n;base<<=1);
    for (int i=base+1;i<=base+n;i++)
        val[i] = a[i-base];
    for (int i=base;i>=1;i--)
        val[i] = max( val[i<<1] , val[i<<1|1] ) ,
        val[i<<1] -= val[i] , val[i<<1|1] -= val[i];
}

區間修改

有了差分線段樹以后,我們發現區間修改就可以直接在樹上操作了。還是利用開區間的方式,向上查找父親並更新線段樹,對於沿路訪問到的每一個節點,由於可能其子樹中包含修改過的節點,就要利用差分定義上傳一下差值給父親,就還能維護之前所提到的性質,而不用再去操作子節點。

同樣地,對於\(l,r\)指針成為兄弟后,還需將差值上推到根節點。

\(Code:\)

inline void modify(int l,int r,int delta)
{
    int temp;
    for ( l=l+base-1 , r=r+base+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
    {
        if ( ~ l & 1 ) val[l^1] += delta;
        if ( r & 1 ) val[r^1] += delta;
        temp = max( val[l] , val[l^1] );
        val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
        temp = max( val[r] , val[r^1] );
        val[r] -= temp , val[r^1] -= temp , val[r>>1] += temp;
    }
    for (; l > 1 ; l>>=1 )
        temp = max( val[l] , val[l^1] ) ,
        val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
}

區間最值

維護了這樣一顆差分線段樹,我們就可以用一種簡單的方式來查詢區間最值了。

這次,我們維護\(l,r\)為閉區間的左右指針,在向上找父親遍歷的過程中,對左右指針遍歷到節點的區間差分值取一下最大值,再一直向上累加,累加到根節點,就是區間最大值,這和單點向上累加的道理是一樣的。

\(Code:\)

inline int query(int l,int r)
{
    int lres = 0 ,rres = 0;
    l += base , r += base;
    if ( l ^ r )
    {
        for (; l ^ r ^ 1 ; l>>=1 , r>>=1 )
        {
            lres += val[l] , rres += val[r];
            if ( ~ l & 1 ) lres = max( lres , val[l^1] );
            if ( r & 1 ) rres = max( rres , val[r^1] );
        }
    }
    int res = max( lres + val[l] , rres + val[r] );
    while ( l > 1 ) res += val[l>>=1];
    return res;
}

這樣,\(zkw\)線段樹的三類基礎模板就已經得到實現了,有關更多的拓展,需要我們靈活運用。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N=100200;
int val[N<<2],n,a[N],base;
inline void read(int &k)
{
    int x=0,w=0;char ch;
    while (!isdigit(ch))
        w |= ch=='-' , ch=getchar();
    while (isdigit(ch))
        x = x*10 + ch-48 , ch=getchar();
    k=(w?-x:x);return;
}
inline void reset(void)
{
    memset( val , 0 , sizeof val );
    base = 1;
}
inline void build(void)
{
    for (;base<=n;base<<=1);
    for (int i=base+1;i<=base+n;i++)
        val[i] = a[i-base];
    for (int i=base;i>=1;i--)
        val[i] = max( val[i<<1] , val[i<<1|1] ) ,
        val[i<<1] -= val[i] , val[i<<1|1] -= val[i];
}
inline void modify(int l,int r,int delta)
{
    int temp;
    for ( l=l+base-1 , r=r+base+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
    {
        if ( ~ l & 1 ) val[l^1] += delta;
        if ( r & 1 ) val[r^1] += delta;
        temp = max( val[l] , val[l^1] );
        val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
        temp = max( val[r] , val[r^1] );
        val[r] -= temp , val[r^1] -= temp , val[r>>1] += temp;
    }
    for (; l > 1 ; l>>=1 )
        temp = max( val[l] , val[l^1] ) ,
        val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
}
inline int query(int l,int r)
{
    int lres = 0 ,rres = 0;
    l += base , r += base;
    if ( l ^ r )
    {
        for (; l ^ r ^ 1 ; l>>=1 , r>>=1 )
        {
            lres += val[l] , rres += val[r];
            if ( ~ l & 1 ) lres = max( lres , val[l^1] );
            if ( r & 1 ) rres = max( rres , val[r^1] );
        }
    }
    int res = max( lres + val[l] , rres + val[r] );
    while ( l > 1 ) res += val[l>>=1];
    return res;
}
inline void solve(void)
{
    scanf("%d",&n);
    reset();
    build();
    for (int i=1;i<=n;i++)
    {
        int op,k1,k2;
        read(op),read(k1),read(k2);
        if (op==1)modify(k1,k2,1);
        if (op==2)printf("%d\n",query(k1,k2));
    }
}
int main(void)
{
    solve();
    return 0;
}

<后記>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM