1. 程式人生 > >線段樹的原理及實現

線段樹的原理及實現

前言

有時我們需要對陣列中[i, j]區間中的所有值進行操作,這樣的操作對於普通的樹來說是十分麻煩的,所以我們引入了新的一種樹——線段樹。

線段樹

線段樹(segment tree),顧名思義, 是用來存放給定區間(segment, or interval)內對應資訊的一種資料結構。即線段樹的每一個節點代表一段區間,而節點的值代表區間的一個性質,他可以是區間的和、區間的最大公約數。。。

使用線段樹時要求其具有區間可加性,即一個大區間的值能夠由它分開的兩個小區間的值得到。

舉個栗子:總區間數字和 = 左半區間數字和 + 右半區間數字和

舉個反慄:由左半區間的眾數和右半區間的眾數求不出 總區間的眾數

來張圖便於大家理解

基本實現

儲存實現:

線段樹是以二叉樹的形式來表示的,所以我們只需要一個數組來表示,i節點的左右節點分別是i * 2 和i * 2 + 1。由上圖我們也可以看出,線段樹不一定是完全二叉樹,考慮到極端情況,若區間的長度為N,則線段樹的陣列長度開到4 * N絕對是夠用的。

程式碼以區間和的線段樹為例進行講解。

pushUp操作:

pushUp表示由下往上跟新節點的值,當我們改變了區間中某一個值或某些值時,我們需要沿著線段樹向上更新改變值的節點與根節點間的所有值,可以看出這部操作與線段樹的高度有關,複雜度為O(logn)。

void pushUp(int id){
    sum[id] = sum[id * 2] + sum[id * 2 + 1];
}

構造:

線段樹的構造採用遞迴方式,先找到葉節點在樹中的位置,然後一步步遞迴構建起整個樹。

const int N = 1000000;
long long int sum[N], lazyT[N];
long long int a[N];

void buildTree(int l, int r, int id){
    if(l == r){
        sum[id] = a[l];
        return;
    }
    int mid = (l + r) / 2;
    
    buildTree(l, mid, id * 2);
    buildTree(mid + 1, r, id * 2 + 1);
    
    pushUp(id);
}

點修改:

改變區件中的某一個值,即在樹中找到對應的葉節點,更新其值,然後遞迴改變該節點到根節點路徑上所有節點的值。

//change a node(a[idChange] += num)
void update(int idChange, int num, int l, int r, int id){
    if(l == r){
        sum[id] += num;
        return;
    }
    
    int mid = (l + r) / 2;
    if(idChange <= mid){
        update(idChange, num, l, mid, id * 2);
    }
    else{
        update(idChange, num, mid + 1, r, id * 2 + 1);
    }
    pushUp(id);
}

區間修改:

在區間修改的時候我們引入一個新的概念——懶惰標記

懶惰標記:表示本節點的統計資訊已經根據標記更新過了,但是本節點的子節點還沒有更新。

為什麼叫懶惰標記呢,哈哈,因為程式猿實在太懶了,構建個樹都要偷工減料。且待我慢慢給你講解這懶惰標記的作用

比如還是這棵樹,我們要把1-13區間中的所有值都+1,按照正常思維,你肯定會想到從根節點開始一路向下找找到每一個葉節點+1,然後遞迴改變所有節點的值,最終遞歸回去,這樣查詢時就會得到正確的結果。

但程式猿的思維是不一樣的(懶的要命),說我就想得到1-13的值,你給我把整個樹都改了,太麻煩了,我太懶了,懶的去改整個樹。不就是+1嘛,我給根節點打個+1標記,就像套個BUFF一樣,這樣根節點1-13的值num = num + 13 * 1,這樣不是省了很多事嗎。

這時有人問了,你這是查詢1-13,你可以這樣偷懶,那如果我要查詢3-10呢?程式猿嘿嘿一笑,那我就下推懶惰標記唄,BUFF從1-13轉移到1-7和8-13上,這樣1-7 num = num + 7 * 1, 8-13num = num + 6 * 1, 再看好像還不是要找的曲間,繼續下推,直到找的找到3-4,5-7,8-10三個區間,給這三個套上BUFF,將這三個區間的值改變後加起來便是要查詢的值了,這樣還是省了好多操作有沒有。

當然如果你要找某一個節點的值,那懶惰標記必然是推到底了,也就查詢操作的時間最多還是O(logn)的。

懶惰標記分為相對標記和絕對標記

相對標記:與懶惰標記的標記順序無關,比如說+1,這樣的懶惰標記可以疊加,比如說你先添加了標記+1,後又新增一個+2標記,那麼這個標記可以直接變為+3

絕對標記:與懶惰標記的標記順序有關,比如說將節點的值變為a,這樣的標記是不可以疊加的,而且與順序有很大的關係。比如先添加個標記“變a”,後添加個標記“變b”,在標記的時候就要注意變a還是變b,哪個先變,哪個後變,這些都很重要。

來看看操作的c++實現:

pushDown:懶惰標記下推

update:區間修改

void pushDown(int id, int leftTreeNum, int rightTreeNum){
    if(lazyT[id]){
        lazyT[id * 2] += lazyT[id];
        lazyT[id * 2 + 1] += lazyT[id];
        
        sum[id * 2] += lazyT[id] * leftTreeNum;
        sum[id * 2 + 1] += lazyT[id] * rightTreeNum;
        
        lazyT[id] = 0;
    }
}

//change a [](a[idChangeLeft, a[idChangeRight]] += num)
void update(int idChangeLeft, int idChangeRight, int num, int l, int r, int id){
    if(idChangeLeft <= l && r <= idChangeRight){
        sum[id] += num * (r - l + 1);
        lazyT[id] += num;
        return;
    }
    
    int mid = (l + r) / 2;
    pushDown(id, mid - l + 1, r - mid);
    
    if(idChangeLeft <= mid){
        update(idChangeLeft, idChangeRight, num, l, mid, id * 2);
        
    }
    if(idChangeRight > mid){
        update(idChangeLeft, idChangeRight, num, mid + 1, r, id * 2 + 1);
    }
    pushUp(id);
}

查詢操作:

在查詢過程中有一個很重要的操作便是懶惰標記的下推,理解了懶惰標記的下推,查詢操作就很好理解了。

long long int query(int queryLeft, int queryRight, int l, int r, int id){
    if(queryLeft <= l && r <= queryRight){
        return sum[id];
    }
    int mid = (l + r) / 2;
    pushDown(id, mid - l + 1, r - mid);
    long long int ans = 0;
    if(queryLeft <= mid){
        ans += query(queryLeft, queryRight, l, mid, id * 2);
    }
    if(queryRight > mid){
        ans += query(queryLeft, queryRight, mid + 1, r, id * 2 + 1);
    }
    return ans;
}

完整程式碼:

#include <iostream>

using namespace std;

const int N = 1000000;
long long int sum[N], lazyT[N];
long long int a[N];

void pushUp(int id){
    sum[id] = sum[id * 2] + sum[id * 2 + 1];
}

void pushDown(int id, int leftTreeNum, int rightTreeNum){
    if(lazyT[id]){
        lazyT[id * 2] += lazyT[id];
        lazyT[id * 2 + 1] += lazyT[id];
        
        sum[id * 2] += lazyT[id] * leftTreeNum;
        sum[id * 2 + 1] += lazyT[id] * rightTreeNum;
        
        lazyT[id] = 0;
    }
}

void buildTree(int l, int r, int id){
    if(l == r){
        sum[id] = a[l];
        return;
    }
    int mid = (l + r) / 2;
    
    buildTree(l, mid, id * 2);
    buildTree(mid + 1, r, id * 2 + 1);
    
    pushUp(id);
}

//change a node(a[idChange] += num)
void update(int idChange, int num, int l, int r, int id){
    if(l == r){
        sum[id] += num;
        return;
    }
    
    int mid = (l + r) / 2;
    if(idChange <= mid){
        update(idChange, num, l, mid, id * 2);
    }
    else{
        update(idChange, num, mid + 1, r, id * 2 + 1);
    }
    pushUp(id);
}

//change a [](a[idChangeLeft, a[idChangeRight]] += num)
void update(int idChangeLeft, int idChangeRight, int num, int l, int r, int id){
    if(idChangeLeft <= l && r <= idChangeRight){
        sum[id] += num * (r - l + 1);
        lazyT[id] += num;
        return;
    }
    
    int mid = (l + r) / 2;
    pushDown(id, mid - l + 1, r - mid);
    
    if(idChangeLeft <= mid){
        update(idChangeLeft, idChangeRight, num, l, mid, id * 2);
        
    }
    if(idChangeRight > mid){
        update(idChangeLeft, idChangeRight, num, mid + 1, r, id * 2 + 1);
    }
    pushUp(id);
}

long long int query(int queryLeft, int queryRight, int l, int r, int id){
    if(queryLeft <= l && r <= queryRight){
        return sum[id];
    }
    int mid = (l + r) / 2;
    pushDown(id, mid - l + 1, r - mid);
    long long int ans = 0;
    if(queryLeft <= mid){
        ans += query(queryLeft, queryRight, l, mid, id * 2);
    }
    if(queryRight > mid){
        ans += query(queryLeft, queryRight, mid + 1, r, id * 2 + 1);
    }
    return ans;
}

總結

線段樹作為一種由點資訊擴充套件到線資訊的資料結構,在多方面都有應用,它的查詢和修改操作的時間複雜度都為O(n),有著很好的效能。