线段树系列之——单点更新与区段总和

线段树

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

接下来的几篇文章我可能会多总结一些关于线段树的题目、扩展及其用法,帮助一些不太懂线段树的ACMer和未来忘记线段树的自己……

例题

题目链接

题意

中文题题意就不解释了。下面我们用糖果来进行相关说明,桌上有n堆糖果,给你每堆糖果的初始数量,然后进行一些操作,这些操作可以往指定的糖果堆里放糖果,也可以往指定的糖果堆里拿走糖果,也可以询问第a~b堆糖果的总数是多少。对于每次询问,输出这几堆糖果的总数

导学

可能读者以前没接触过线段树,我借这道题形容一下。如果你用数组来维护这一堆糖果,那么增加和减少可能比较方便,但是计算总和的时候每一次询问操作都需要重新循环,肯定就超时了

那么使用线段树的好处在哪里呢?比如10堆糖果,它把这线型的10堆糖果变成了树型。整一棵树的所有叶节点存储的是每一堆糖果的数量,而其叶节点的父节点则存储了该节点下所有子节点的糖果数的总和。依次往上,根节点存储的也就是所有糖果堆的总和

增加/减少糖果时,找到该糖果所在的叶节点的位置,更新,并且更新所有与该叶节点有关的“父节点”,在查询时,就不需要重新计算了,将指定父节点的位置相加即可。

解析

在这题中,首先建立一个空的线段树,即假设每堆糖果的数量为0

void build(int l,int r,int p){ //构造线段树
    tree[p].left=l;   //节点所表示的范围起点
    tree[p].right=r;  //节点所表示的范围终点
    tree[p].value=0;  //糖果的数量初始置0
    if(l==r) return;
    int mid=(l+r)/2;
    build(l,mid,p*2);  //依次建立子节点,直到处理到叶节点为止
    build(mid+1,r,p*2+1);
}
……
cin>>n;   //糖果的堆数
build(1,n,1);

然后得到每一堆糖果的具体数量。对于每一堆的数量,其实就相当于在原来置0的情况下,进行依次增加操作,往第i堆中增加value个糖果。

void add(int k,int p,int value){
    if(tree[p].left==k&&tree[p].right==k){   //如果找到了对应位置的叶子节点,就进行增加操作
        tree[p].value+=value;
        return;
    }
    int mid=(tree[p].left+tree[p].right)/2;  //从子节点中寻找
    if(k<=mid) add(k,p*2,value);
    else add(k,p*2+1,value);
    tree[p].value = tree[2*p].value+tree[2*p+1].value;  //子节点更新了,父节点也要更新
}
……
for(i=1;i<=n;i++){
    scanf("%d",&num);
    add(i,1,num);
}

然后,在上述操作的基础上,我们发现已经解决了题目要求的增加和减少的命令了,因为减少n等于增加-n。还有最后一个查询的操作,也就是查询a~b堆的总数

void search(int l,int r,int p){
    if(tree[p].left==l&&tree[p].right==r){  //如果这个节点的范围完全吻合所需要查找的范围
        sum+=tree[p].value;  //直接增加
        return;   //加完就跑
    }
    //如果这个节点的范围不满足怎么办?
    int mid = (tree[p].left+tree[p].right)/2;  //取当前范围的中点
    if(r<=mid) search(l,r,2*p);  //如果所求范围在中点左边,往左子树找
    else if(l>mid) search(l,r,2*p+1);  //如果所求范围在中点右边,往右子树找
    else{
        //如果mid在所有范围当中,也就是左子树也有,右子树也有,那就都找,但是需要处理一下范围,不能直接传递l和r
        search(l,mid,2*p);
        search(mid+1,r,2*p+1);
    }
}
……
sum=0;
search(num1,num2,1);
cout<<sum<<endl;

上面的查询操作也是这个代码的核心操作,和数组查询不同的是,数组查询每次都是在查询叶节点,而线段树几乎不会到叶节点,总是到某个父节点就停下了,所以大大减少了操作次数

AC代码

#include <iostream>
#include <string.h>
#include <stdio.h>
#define M 50005
using namespace std;
int sum;
struct node{
    int left;
    int right;
    int value;
}tree[4*M]; //大小为预定长度的4倍
void build(int l,int r,int p){ //构造线段树
    tree[p].left=l;   //节点所表示的范围起点
    tree[p].right=r;  //节点所表示的范围终点
    tree[p].value=0;  //糖果的数量初始置0
    if(l==r) return;
    int mid=(l+r)/2;
    build(l,mid,p*2);  //依次建立子节点,直到处理到叶节点位置
    build(mid+1,r,p*2+1);
}
void add(int k,int p,int value){
    if(tree[p].left==k&&tree[p].right==k){   //如果找到了对应位置的叶子节点,就进行增加操作
        tree[p].value+=value;
        return;
    }
    int mid=(tree[p].left+tree[p].right)/2;  //从子节点中寻找
    if(k<=mid) add(k,p*2,value);
    else add(k,p*2+1,value);
    tree[p].value = tree[2*p].value+tree[2*p+1].value;  //子节点更新了,父节点也要更新
}
void search(int l,int r,int p){
    if(tree[p].left==l&&tree[p].right==r){  //如果这个节点的范围完全吻合所需要查找的范围
        sum+=tree[p].value;  //直接增加
        return;   //加完就跑
    }
    //如果这个节点的范围不满足怎么办?
    int mid = (tree[p].left+tree[p].right)/2;  //取当前范围的中点
    if(r<=mid) search(l,r,2*p);  //如果所求范围在中点左边,往左子树找
    else if(l>mid) search(l,r,2*p+1);  //如果所求范围在终点右边,往右子树找
    else{   //如果mid在所有范围当中,也就是左子树也有,右子树也有,那就都找,但是需要处理一下范围,不能直接传递l和r
        search(l,mid,2*p);
        search(mid+1,r,2*p+1);
    }
}
int main(){
    char str[10];
    int T,n,i,k,num,num1,num2;
    cin>>T;
    for(k=1;k<=T;k++){
        cin>>n;   //堆数
        build(1,n,1);
        for(i=1;i<=n;i++){
            scanf("%d",&num);
            add(i,1,num);
        }
        cout<<"Case "<<k<<":"<<endl;
        while(scanf("%s",str)&&strcmp(str,"End")!=0){
            scanf("%d%d",&num1,&num2);
            if(strcmp(str,"Add")==0)
                add(num1,1,num2);
            else if(strcmp(str,"Sub")==0)
                add(num1,1,-num2);
            else{
                sum=0;
                search(num1,num2,1);
                cout<<sum<<endl;
            }
        }
    }
    return 0;
}


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