1. 程式人生 > >線段樹(SegmentTree)學習筆記

線段樹(SegmentTree)學習筆記

身後 增加 來看 ask cpp struct amp log 技術分享

在對數組進行操作的時候,我們有時會需要獲取數組某個區間的信息,如該區間內的最值、區間和等。我們可以使用枚舉的方式去獲取這些信息,但是這樣做的平均時間復雜度期望為O(n),數據範圍一大,這樣的方式就基本穩穩的超時了。

於是,線段樹應運而生。線段樹是一種高級數據結構,基於二分思想,以O(logn)的時間進行區間查找與修改

技術分享圖片

線段樹常見的操作有:

1、建立一棵線段樹(build);
2、查詢某區間的信息(ask);
3、修改某個元素的值(replace);
4、給某個區間內的元素全部加上一個值(add)。

(ps:代碼中的p<<1、p<<1|1為位運算優化,效果等同於p*2,p*2+1。)

首先,我們來看看如何建立線段樹。從上圖來看,容易看出,線段樹是一顆二叉樹,每個節點都代表了一個區間,左孩子和右孩子都將父親分為了兩段。所以,我們可以采用二倍孩子法來儲存。這裏我采用了區間和線段樹作為講解模板。

template<typename T>//泛型,線段樹內的元素可以指定類型
struct Tree
{
    int l,r;//該節點所管區間的左右邊界
    T val;//這裏保存的是將要查詢的信息,如區間和、區間最值等。
}tree[SIZE<<2];

前面說了,線段樹是基於二分思想的,所以,我們采用遞歸+分治的方式進行建樹。

具體方式:

1、將區間邊界賦給該節點;
2、如果左右邊界相等,即區間內只有一個元素,則將數組內的值賦值給該葉節點,直接返回;
3、遞歸左孩子;
4、遞歸右孩子;
5、用左右孩子的信息來更新當前節點的信息。

代碼:

void Build(int p,int l,int r,T *a)//p指的是當前的區間節點
{
    tree[p].l=l,tree[p].r=r;//1
    if(l==r){tree[p].val=a[l];return;}//2
    int mid=(l+r)>>1;
    Build(p<<1,l,mid,a);//3
    Build(p<<1|1,mid+1,r,a);//4
    tree[p].val=tree[p<<1].val+tree[p<<1|1].val;//5
}

然後,就是查詢區間信息了。因為我們建樹的時候已經把各個節點的信息更新好了,所以現在可以直接查找了。

具體步驟:

1、如果當前節點管轄區間正好等於查找區間,直接返回該節點的信息值;
2、如果當前節點的左孩子正好完全包含查找區間,則遞歸到左孩子查找;
3、如果當前節點的右孩子正好完全包含查找區間,則遞歸到右孩子查找;
4、如果查找區間在左孩子中有一部分,在右孩子中有一部分,則左右區間都遞歸查找,再根據需要的信息進行操作。

代碼:

T Ask(int p,int l,int r)
{
    if(tree[p].l==tree[p].r){return tree[p].val;}
    int mid=(tree[p].l+tree[p].r)>>1;
    if(l<=mid&&r<=mid) return Ask(p<<1,l,r);
    else if(r>mid&&l>mid) return Ask(p<<1|1,l,r);
    else return Ask(p<<1,l,r)+Ask(p<<1|1,l,r);
}

然後,則是單點元素更改了。在理解完上面的內容後,單點更改就非常容易了。

具體步驟:

1、如果當前區間節點只有一個元素,且此元素為待修改元素,直接修改,返回;
2、如果節點在左區間,則遞歸左區間;
3、反之,遞歸右區間;
4、用左右節點信息更新當前節點信息。

代碼:

void Replace(int p,int index,T val)
{
    if(tree[p].l==tree[p].r){tree[p].val=val;return;}//1
    int mid=(tree[p].l+tree[p].r)>>1;
    if(index>mid) Replace(p<<1|1,index,val);//2
    else Replace(p<<1,index,val);//3
    tree[p].val=tree[p<<1].val+tree[p<<1|1].val;//4
}

接下來,則是重頭戲——區間整體增加(add)。

按照以上的方式,進行區間整體增加的方式只能是對區間內每個元素進行單點更改,共計length次修改,時間復雜度為O(nlogn)。對於某些題目,顯然是不能滿足速度需求的。那麽,有更快的算法嗎?

觀察其他的線段樹教學blog可以發現,在區間修改時,使用了一個叫lazytag,也就是懶標記的東西。為什麽叫做懶標記?因為它很懶

我們給每個節點額外增加一個成員變量tag,也就是上述的“懶標記”。

struct Tree
{
    int l,r;
    T val,tag;//懶標記
}tree[SIZE<<2];

懶標記為什麽叫做懶標記?因為它的作用是作為一個傳遞標記使用。在更改區間時,找到一個包含在待更改區間內的區間節點後,不繼續向下更新,而是更新完自身後,只把增加值給懶標記。等到以後要訪問下面的節點時,才將當前節點懶標記下傳給下面的子節點。

這麽一來,在區間修改操作中,我們的均攤復雜度就只有O(logn)了(常數比較大)。

值得註意的一點是,由於在調整區間值時,我們並沒有把底下的區間更改,只是掛在了上面的lazytag上,所以在單點更改和獲取信息時,記得要先將lazytag下放,再向下遞歸。

首先是下放的函數:
void Spread(int p) { if(tree[p].tag)//如果tag為0就沒必要下放了 { int tag=tree[p].tag; //一個區間管n個元素,所以val要加上tag*n tree[p<<1].val+=tag*(tree[p<<1].r-tree[p<<1].l+1);//更新左子樹 tree[p<<1|1].val+=tag*(tree[p<<1|1].r-tree[p<<1|1].l+1);//更新右子樹 tree[p<<1].tag+=tag,tree[p<<1|1].tag+=tag;//下放 tree[p].tag=0;//tag下傳完畢後就可以釋放了 } }
然後是區間修改:

void Add(int p,int l,int r,T val)
{
    if(l<=tree[p].l&&r>=tree[p].r)//如果包含了待修改區間,直接修改tag和val,不再向下下傳與遞歸。
    {
        tree[p].val+=val*(tree[p].r-tree[p].l+1);
        tree[p].tag+=val;
        return;
    }
    Spread(p);//下傳懶標記
    int mid=(tree[p].l+tree[p].r)>>1;
    if(l<=mid) Add(p<<1,l,mid,val);//左遞歸
    if(r>mid) Add(p<<1|1,mid+1,r,val);//右遞歸
    tree[p]val=tree[p<<1].val+tree[p<<1|1].val;//更新節點信息
}

ask和replace只要在左右向下遞歸前下傳tag就可以了。

線段樹(SegmentTree)學習筆記