從樹狀數組到線段樹

在已知了樹狀數組的使用方法,那么便可以用它來解決一些實際問題了,比如說下面一道經典題:敵兵布陣 :HDU:1166。
題目大致意圖為:敵方有排好的數個陣營,每個陣營都有一些士兵,且不斷有人進出,在某個時刻快速統計陣營號在某個范圍內的陣營內人數和。
為了快速統計前n項和且盡量減少統計時間,避免所有的數據遍歷,很容易想到樹狀數組的應用,不但可以以很少的時間更新數組,而且計算和時減少遍歷求和時間,假設要求的陣營排號范圍為(a,b),那么只需要sum(b)-sum(a-1),即可,我們可以按照此思路很容易地寫出相關的代碼:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <string.h>
#define lowbit(x)  (x&(-x))
#define MMAX 500010
using namespace std;

int m;
int C[MMAX];

int sum(int i)
{
    int ans=0;
    while (i>0)
    {
        ans+=C[i];
        i-=lowbit(i);
    }
    return ans;
}

void add(int num,int i)
{
    while (i<=m)
    {
        C[i]+=num;
        i+=lowbit(i);
    }
}


void main()
{
    int i,j,k,n,x,y;
    char ch[7];
    scanf("%d",&n);
    for (j=0;j<n;j++)
    {
        memset (C,0,sizeof (C));
        scanf("%d",&m);
        for (i=0;i<m;i++)
        {
            scanf("%d",&k);
            add(k,i+1);
        }
        printf("Case %d:\n",j+1);
        getchar();
        while(scanf("%s",ch)&&strcmp(ch,"End")!=0)
        {
            scanf("%d%d",&x,&y);
            if (strcmp(ch,"Add")==0)
            {
                add(y,x);
            }
            else if (strcmp(ch,"Query")==0)
            {
                printf("%d\n",sum(y)-sum(x-1));
            }
            else if (strcmp(ch,"Sub")==0)
            {
                add(-y,x);
            }
            getchar();
        }
    }
}

該程序在指定的時間范圍內,是可以順利AC的,那么,我們不妨繼續尋找更加簡便的方法來更快的減少時間。
我們知道,樹狀數組的結果是前N項的和,那么,正如該題,如果我們要求的是一段范圍內的和該怎么辦呢?假設范圍是(a,b),那么1到a-1的求和我們計算了兩遍,而需要使用的卻不是這段的和,我們便不由得去想,可否想一個辦法讓數組直接存儲的就是一定范圍內的和,這時候需要涉及到的就是線段樹。

顧名思義,線段樹即在樹的每一個節點都儲存一個線段,若把線段視為一個范圍,那么每一個結點都會是下標為一段范圍的數組和,而葉子節點存放一個元素,如圖

ABCD代表的葉子節點只存儲一個元素的和,而EF存儲的是兩個孩子結點的和E=A+B=1+2,F=C+D=3+4,而G結點存儲的也為其兩個孩子的和即G=E+F=A+B+C+D=1+2+3+4。
那么,如果想要求(3,4)的和,只需要調出F結點輸出即可,如果有一個葉子節點內的數據變動,要做的便是向上迭代,將其雙親結點變動相應的參數,直到樹的根結點。
但是如何準確地使用線段樹呢?首先需要做的就是深刻理解線段樹的原理和性質:

線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
對于線段樹中的每一個非葉子節點[a,b],它的左兒子表示的區間為[a,(a+b)/2],右兒子表示的區間為[(a+b)/2+1,b]。因此線段樹是平衡二叉樹,最后的子節點數目為N,即整個線段區間的長度。
使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,時間復雜度為O(logN)。而未優化的空間復雜度為2N,因此有時需要離散化讓空間壓縮。

----來自百度百科
【以下以 求區間最大值為例】
先看聲明:

01.#include <stdio.h>  
02.#include <math.h>  
03.const int MAXNODE = 2097152;  
04.const int MAX = 1000003;  
05.struct NODE{  
06.    int value;        // 結點對應區間的權值  
07.    int left,right;   // 區間 [left,right]  
08.}node[MAXNODE];  
09.int father[MAX];     // 每個點(當區間長度為0時,對應一個點)對應的結構體數組下標  

【創建線段樹(初始化)】:

由于線段樹是用二叉樹結構儲存的,而且是近乎完全二叉樹的,所以在這里我使用了數組來代替鏈表上圖中區間上面的紅色數字表示了結構體數組中對應的下標。

在完全二叉樹中假如一個結點的序號(數組下標)為 I ,那么 (二叉樹基本關系)

I 的父親為 I/2,

I 的另一個兄弟為 I/2 * 2 或 I/2?2+1

I 的兩個孩子為 I*2 (左) I?2+1(右)

有了這樣的關系之后,我們便能很方便的寫出創建線段樹的代碼了。

01.void BuildTree(int i,int left,int right){ // 為區間[left,right]建立一個以i為祖先的線段樹,i為數組下標,我稱作結點序號  
02.    node[i].left = left;    // 寫入第i個結點中的 左區間  
03.    node[i].right = right;  // 寫入第i個結點中的 右區間  
04.    node[i].value = 0;      // 每個區間初始化為 0  
05.    if (left == right){ // 當區間長度為 0 時,結束遞歸  
06.        father[left] = i; // 能知道某個點對應的序號,為了更新的時候從下往上一直到頂  
07.        return;  
08.    }  
09.    // 該結點往 左孩子的方向 繼續建立線段樹,線段的劃分是二分思想,如果寫過二分查找的話這里很容易接受  
10.    // 這里將 區間[left,right] 一分為二了  
11.    BuildTree(i<<1, left, (int)floor( (right+left) / 2.0));  
12.    // 該結點往 右孩子的方向 繼續建立線段樹  
13.    BuildTree((i<<1) + 1, (int)floor( (right+left) / 2.0) + 1, right);  
14.}  

【單點更新線段樹】:

假設該線段樹的作用是找到N個節點的最大值,由于我事先用 father[ ] 數組保存過 每單個結點 對應的下標了,因此我只需要知道第幾個點,就能知道這個點在結構體中的位置(即下標)了,這樣的話,根據之前已知的基本關系,就只需要直接一路更新上去即可。

01.void UpdataTree(int ri){ // 從下往上更新(注:這個點本身已經在函數外更新過了)  
02.  
03.    if (ri == 1)return; // 向上已經找到了祖先(整個線段樹的祖先結點 對應的下標為1)  
04.    int fi = ri / 2;        // ri 的父結點  
05.    int a = node[fi<<1].value; // 該父結點的兩個孩子結點(左)  
06.    int b = node[(fi<<1)+1].value; // 右  (每個數字按位左移一個單位相當于乘2)
07.    node[fi].value = (a > b)?(a):(b);    // 更新這個父結點(從兩個孩子結點中挑個大的)  
08.    UpdataTree(ri/2);       // 遞歸更新,由父結點往上找  
09.}  
//直到更新完畢,根節點中存放的數則是數組中所有的數組的最大數。

【查詢區間最大值】:
將一段區間按照建立的線段樹從上往下一直拆開,直到存在有完全重合的區間停止。對照圖例建立的樹,假如查詢區間為 [2,5]


紅色的區間為完全重合的區間,因為在這個具體問題中我們只需要比較這 三個區間的值 找出 最大值 即可。

01.int Max = -1<<20;  
02.void Query(int i,int l,int r){ // i為區間的序號(對應的區間是最大范圍的那個區間,也是第一個圖最頂端的區間,一般初始是 1 啦)  
03.    if (node[i].left == l && node[i].right == r){ // 找到了一個完全重合的區間  
04.        Max = (Max < node[i].value)?node[i].value:(Max);  
05.        return ;  
06.    }  
07.    i = i << 1; // get the left child of the tree node  
08.    if (l <= node[i].right){ // 左區間有涉及  
09.        if (r <= node[i].right) // 全包含于左區間,則查詢區間形態不變  
10.            Query(i, l, r);  
11.        else // 半包含于左區間,則查詢區間拆分,左端點不變,右端點變為左孩子的右區間端點  
12.            Query(i, l, node[i].right);  
13.    }  
14.    i += 1; // right child of the tree  
15.    if (r >= node[i].left){ // 右區間有涉及  
16.        if (l >= node[i].left) // 全包含于右區間,則查詢區間形態不變  
17.            Query(i, l, r);  
18.        else // 半包含于左區間,則查詢區間拆分,與上同理  
19.            Query(i, node[i].left, r);  
20.    }  
21.}  

此段算法代碼并不好理解,但是,可以根據二叉樹的遍歷便可以得到大致思路:

首先,從最大的結點:根結點開始遍歷,如果目標數據域與根數據相同,那么直接輸出的就是跟數據域內的數據,如果不相同,那么一定是小于跟數據域的,可以先判斷其在左子樹和右子樹中是不是有目標數據域內的數。
首先,判斷是否在左子樹中:將i從代表跟子樹的下標1轉換為左子樹的下標i<<1;然后判斷,如果在左子樹中有數據,那么一定是從小的數據開始排,有if (node[i].right>=l),如果有,那么判斷是否整個數據域都在左子樹中:if (node[i].right>=r)如果是,那么就可以遞歸算法,傳參i(左子樹坐標),繼續從頭判斷是否目標數據域全部在該結點中……如果不是,那么則說明目標數據域即在左子樹中有元素,也在右子樹中有,先判斷左子樹中的元素具體位置,將r置為node[i].right,保證目標數據域都在i結點中,遞歸,再次調用數據,判斷是否目標數據域全部在該結點中……直到找到了左子樹中的部分的最大值,那么,右子樹中的該怎么辦呢?已知找到后,循環將跳出,那么,將i++,得到的下標就是當前子樹的兄弟,即右子樹了;已知右子樹可能有部分的目標數據域內的數據,也可能沒有,那么,只要加以判斷if (node[i].left<=r),之后的判斷與左子樹內的處理相似,如果判斷為否,退出循環,找到解,若為是,判斷是否全部都在該區域中,遞歸參數i,若為否,遞歸數據域的左區間將為node[i].left。
此代碼運用的環境為求最大值,可以減少比較和遍歷時間。

在本題當中,所求的解為一定數據域內的和,便需要找到完全符合的數據域,并將它們的值并入和的值即可。根據題意得出相應代碼:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <string.h>
#define MMAX 3000010
using namespace std;

struct
{
    int value;
    int right,left;
}node[MMAX];

int num[500010];
int sum;
int n;

void build (int i,int l,int r)
{
    if (l==r)
    {
        node[i].right=r;
        node[i].left=l;
        node[i].value=num[l];
        return;
    }
    node[i].left=l;
    node[i].right=r;
    build(i*2,l,(l+r)/2);
    build(i*2+1,(l+r)/2+1,r);
    node[i].value=node[i*2].value+node[i*2+1].value;
}


void add(int i,int l,int r)
{
    if (node[i].left==l&&node[i].right==r)
    {
        sum+=node[i].value;
        return;
    }
    i=i<<1;
    if (node[i].right>=l)
    {
        if (node[i].right>=r)
            add(i,l,r);
        else
            add(i,l,node[i].right);
    }
    i++;
    if (node[i].left<=r)
    {
        if (node[i].left<=l)
            add(i,l,r);
        else
            add(i,node[i].left,r);
    }
}

void update(int i,int shu)
{
    int a=1;
    while (a<=MMAX)
    {
        node[a].value+=shu;
        if (node[a].right==node[a].left)
            break;
        if (i<=(node[a].left+node[a].right)/2)
            a*=2;
        else
            a=2*a+1;
    }
}


void main()
{
    int i,j,k,m,x,y;
    char s[12];
    scanf("%d",&m);
    for (int cas=1;cas<=m;cas++)
    {
        scanf("%d",&n);
        memset(num,0,sizeof(num));
        for(i=1;i<=n;i++)
        {
            scanf("%d",&num[i]);
        }
        build(1,1,n);
        printf("Case %d:\n",cas);
        getchar();
        while(scanf("%s",s)&&s[0]!='E')
        {
            sum=0;
            scanf("%d%d",&x,&y);
            getchar();
            if (s[0]=='Q')
            {
                add(1,x,y);
                printf("%d\n",sum);
            }
            else if (s[0]=='A')
                update(x,y);
            else if (s[0]=='S')
                update(x,-y);
        }
    }
}

由上面的代碼論述可知,實現從根結點到達某個根結點的數據更新,不但可以按照比較左右數據域的大小比較,還可以根據實際情況將其與(node[i].left+node[i].right)/2進行比較來確定目標結點在左子樹中還是右子樹中。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • B樹的定義 一棵m階的B樹滿足下列條件: 樹中每個結點至多有m個孩子。 除根結點和葉子結點外,其它每個結點至少有m...
    文檔隨手記閱讀 13,345評論 0 25
  • 1.樹的定義 樹是n(n>=0)個結點的有限集.n=0時稱為空樹.在任意一顆非空樹種:(1)有且僅有一個特定的稱為...
    e40c669177be閱讀 2,894評論 1 14
  • 第一章 緒論 什么是數據結構? 數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。 第二章...
    SeanCheney閱讀 5,821評論 0 19
  • 數據結構與算法--從平衡二叉樹(AVL)到紅黑樹 上節學習了二叉查找樹。算法的性能取決于樹的形狀,而樹的形狀取決于...
    sunhaiyu閱讀 7,677評論 4 32
  • 樹和二叉樹 1、樹的定義 樹(Tree)是由一個 或 多個結點 組成的有限集合T,且滿足: ①有且僅有一個稱為根的...
    利伊奧克兒閱讀 1,397評論 0 1