从树状数组到线段树

在已知了树状数组的使用方法,那么便可以用它来解决一些实际问题了,比如说下面一道经典题:敌兵布阵 :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进行比较来确定目标结点在左子树中还是右子树中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,183评论 0 25
  • 1.树的定义 树是n(n>=0)个结点的有限集.n=0时称为空树.在任意一颗非空树种:(1)有且仅有一个特定的称为...
    e40c669177be阅读 2,796评论 1 14
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,743评论 0 19
  • 数据结构与算法--从平衡二叉树(AVL)到红黑树 上节学习了二叉查找树。算法的性能取决于树的形状,而树的形状取决于...
    sunhaiyu阅读 7,638评论 4 32
  • 树和二叉树 1、树的定义 树(Tree)是由一个 或 多个结点 组成的有限集合T,且满足: ①有且仅有一个称为根的...
    利伊奥克儿阅读 1,351评论 0 1