树状数组 线段树

树状数组

树状数组的基本用途是维护序列的前缀和,相比前缀和数组,树状数组优势在于高效率的单点修改,单点增加(前缀和数组单点修改效率比较低)

因为树状数组的思想,原理还是很好理解的,就直接讲基本算法;

1 lowbit函数

关于lowbit这个函数,可能会有点难以理解,但其实你不理解也没关系,把模板背下来就好

根据任意正整数关于2的不重复次幂的唯一分解性质,例如十进制21用二进制表示为10101,其中等于1的位是第0,2,4(最右端是第0位)位,即21被二进制分解成\(2^4+2^2+2^0\);

进一步地,整个区间[1,21]可以分成如下3个小区间:

长度为\(2^4\)的小区间[1,\(2^4\)];

长度为\(2^2\)的小区间[\(2^4+1\),\(2^4+2^2\)];

长度为\(2^0\)的小区间[\(2^4+2^2+1\),\(2^4+2^2+2^0\)];

对于给定的初始序列A,我们可以建立一个数组c,c[x]表示序列A的区间[x-lowbit(x)+1,x)]中所有数的和;

int lowbit(int x){return x&-x;}

2 单点增加操作

void update(int x,int y){
    for(;x<=n;x+=lowbit(x))
        c[x]+=y;
}

3 查询前缀和

int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

4 扩展

上述查询前缀和是统计[1,x]的前缀和,若要统计区间[x,y]的和,则调用sum函数即可:sum(y)-sum(x-1);

多维树状数组:

(扩充为m维)将原来的修改和查询函数中的一个循环,改成m个循环m维数组c中的操作;

以\(n*m\)的二维数组为例:

将(x,y)的值加上z,不是把区间[x,y]中的每个值加z
int update(int x,int y,int z){
    int i=x;
    while(i<=n){
        int j=y;
        while(j<=m){
            c[i][j]+=z;
            j+=lowbit(j);
        }
        i+=lowbit(i);
    }
}

int sum(int x,int y){
    int res=0,i=x;
    while(i>0){
        int j=y;
        while(j>0){
            res+=c[i][j];
            j-=lowbit(j);
        }
        i-=lowbit(i);
    }
    return res;
}

注意树状数组的下标绝对不能为0,因为lowbit(0)=0,这样会陷入死循环

两道模板题,多打打模板~~

https://www.luogu.org/problemnew/show/P3374

https://www.luogu.org/problemnew/show/P3368

线段树

线段树是一种基于分治思想的二叉树结构,比树状数组更加通用,下文总结会比较这两种数据结构;

线段树的基本用途是对序列进行维护,支持查询与修改指令;

线段树的每个节点都代表一个区间;

线段树具有唯一的根节点,代表的是整个区间,即[1,n];

线段树的每个叶节点都代表一个长度为1的区间,即[x,x];

对于每个内部节点[l,r],它的左子节点是[l,mid],右子节点是[mid+1,r],其中mid=(l+r)/2(向下取整);(子节点与父节点的性质近似于二叉堆的结构);

**保存的数组长度要不小于4*n;**

1 建树:线段树的二叉树结构可以很方便地从下往上传递信息;

下面以区间最大值为例:

struct TREE{
    int l,r;
    int dat;
}t[n*4];
//一般会用结构体存储线段树

void build(int p,int l,int r){
    t[p].l=l;t[p].r=r;//节点p代表区间[l,r]
    if(l==r){t[p].dat=a[l];return;}//叶节点
    int mid=(l+r)/2;
    build(p*2,l,mid);//左子节点
    build(p*2+1,mid+1,r);//右子节点
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
    //从下往上传递信息
}

build(1,1,n);//调用入口

2 单点修改:线段树中,根节点(编号为1的节点)是执行各种指令的入口;上述建树过程中的调用入口的第一个1就是这个道理;

我们还是以区间最大值问题为例

void change(int p,int x,int v){
    if(t[p].l==t[p].r){t[p].dat=v;return;}
    //找到叶节点
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid) change(p*2,x,v);
    else change(p*2+1,x,v);
    //判断x属于哪边区间
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}

change(1,x,v);

3 区间查询

以查询区间最大值为例:

若[l,r]完全覆盖了当前节点代表的区间,则立即回溯,并且该节点的dat值为候选答案

若左子节点与[l,r]有重叠部分,则递归访问左子节点;

若右子节点与[l,r]有重叠部分,则递归访问右子节点;

(这里自己稍微理解一下就懂了,或者自己画个图)

假设我们查询区间[2,7]的最大值,现在有一个节点的代表区间为[3,5],那么属于完全覆盖的情况,return,并且该节点的dat值为候选答案;

现在还有[2,3]和[5,7]这两个区间没有处理,我们继续寻找节点,如果有一个节点与该区间有交集,即有重叠的区间,我们就继续向下访问该节点,直至找到一个节点完全被[2,3]或[5,7]覆盖,那么跟上面一样处理即可;(记住我们是从上往下访问线段树,所以区间范围从上往下是越来越小的)

int ask(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r)return t[p].dat;
    int mid=(t[p].l+t[p].r)/2;
    int val=-(1<<30);
    if(l<=mid) val=max(val,ask(p*2,l,r));
    if(r>mid) val=max(val,ask(p*2+1,l,r));
    return val;
}

ask(1,l,r);

延迟标记:标识该节点曾经被修改,但其子节点尚未被修改(即一个节点被打上延迟标记的同时,它自身保存的信息应该已经被修改完毕)

通俗地讲,我们在进行区间增加操作时,如果去更改区间中的每个数(即遍历到每个叶节点),时间复杂度会增加到O(N);

试想一下,有可能我们修改了该区间,但所有的查询中与该区间没有关系(即该区间没有对我们的答案产生贡献),相当于我们做了一次无用的操作;

于是就可以用到延迟标记,我们在执行修改指令时,给节点p一个标记,标识该节点曾经被修改,但其子节点尚未被修改,如果在后续的查询指令中,我们需要知道p节点的子节点的信息,如果p节点有标记,那么更新p的两个子节点,同时给p的两个子节点打上延迟标记,然后清除p的标记;

以区间增加问题为例:

void spread(int p){
    if(t[p].add){//如果节点P有标记
 t[p*2].sum+=t[p].add*(t[p*2].r-t[p*2].l+1);
    //更新左子节点信息
t[p*2+1].sum+=t[p].add*(t[p*2+1].r-t[p*2+1].l+1);
    t[p*2].add+=t[p].add;//给左子节点打延迟标记
    t[p*2+1].add+=t[p].add;
    t[p].add=0;//清除p的标记
    }
}

void change(int p,int x,int y,int v){
    if(x<=t[p].l&&y>=t[p].r){//完全覆盖
        t[p].sum+=v*(t[p].r-t[p].l+1);
        //更新节点信息
        t[p].add+=v;//给节点打上延迟标记
        return;
    }
    spread(p);    //下传延迟标记
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid) change(p*2,x,y,v);
    if(y>mid) change(p*2+1,x,y,v);
    t[p].sum=t[p*2].sum+t[p*2+1].sum;
}

long long ask(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r)
        return t[p].sum;
    spread(p);//下传延迟标记
    int mid=(t[p].l+t[p].r)/2;
    long long ans=0;
    if(l<=mid) ans+=ask(p*2,l,r);
    if(r>mid) ans+=ask(p*2+1,l,r);
    return ans;
}

最后比较一下树状数组和线段树:

树状数组可以实现单点修改和区间求和(如果将序列进行差分,那么还可以实现区间价值和单点询问);

线段树能实现树状数组的所有操作,除此之外,还可以通过标记下传或标记永久化进行区间修改和区间询问;

但树状数组的常数比线段树小,实现也较为简单(线段树真的是随随便便100行,看来我还是太蒻了)

总之,能用树状数组就用树状数组吧,毕竟一般都只能用线段树;

原文地址:https://www.cnblogs.com/PPXppx/p/9898600.html

时间: 2024-11-08 19:26:48

树状数组 线段树的相关文章

hdu 1166 树状数组 线段树

敌兵布阵 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 51177    Accepted Submission(s): 21427 Problem Description C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了.A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务

hdu1394(枚举/树状数组/线段树单点更新&amp;区间求和)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1394 题意:给出一个循环数组,求其逆序对最少为多少: 思路:对于逆序对: 交换两个相邻数,逆序数 +1 或 -1, 交换两个不相邻数 a, b, 逆序数 += 两者间大于 a 的个数 - 两者间小于 a 的个数: 所以只要求出初始时的逆序对数,就可以推出其余情况时的逆序对数.对于求初始逆序对数,这里 n 只有 5e3,可以直接暴力 / 树状数组 / 线段树 / 归并排序: 代码: 1.直接暴力 1

HDU 1394 Minimum Inversion Number 树状数组&amp;&amp;线段树

题目给了你一串序列,然后每次 把最后一个数提到最前面来,直到原来的第一个数到了最后一个,每次操作都会产生一个新的序列,这个序列具有一个逆序数的值,问最小的你逆序数的值为多少 逆序数么 最好想到的是树状数组,敲了一把很快,注意把握把最后一个数提上来对逆序数的影响即可, #include<iostream> #include<cstdio> #include<list> #include<algorithm> #include<cstring> #i

HDU 1166 敌兵布阵 (树状数组&#183;线段树)

题意  中文 动态区间和问题   只会更新点  最基础的树状数组 线段树的应用 树状数组代码 #include <bits/stdc++.h> using namespace std; const int N = 50005; int c[N], n, m; void add(int p, int x) { while(p <= n) c[p] += x, p += p & -p; } int getSum(int p) { int ret = 0; while(p > 0

Curious Robin Hood(树状数组+线段树)

1112 - Curious Robin Hood    PDF (English) Statistics Forum Time Limit: 1 second(s) Memory Limit: 64 MB Robin Hood likes to loot rich people since he helps the poor people with this money. Instead of keeping all the money together he does another tri

士兵杀敌(四)(树状数组+线段树)

士兵杀敌(四) 时间限制:2000 ms  |  内存限制:65535 KB 难度:5 描述 南将军麾下有百万精兵,现已知共有M个士兵,编号为1~M,每次有任务的时候,总会有一批编号连在一起人请战(编号相近的人经常在一块,相互之间比较熟悉),最终他们获得的军功,也将会平分到每个人身上,这样,有时候,计算他们中的哪一个人到底有多少军功就是一个比较困难的事情,军师小工的任务就是在南将军询问他某个人的军功的时候,快速的报出此人的军功,请你编写一个程序来帮助小工吧. 假设起始时所有人的军功都是0. 输入

Color the ball(树状数组+线段树)

Color the ball Time Limit : 9000/3000ms (Java/Other)   Memory Limit : 32768/32768K (Java/Other) Total Submission(s) : 3   Accepted Submission(s) : 1 Problem Description N个气球排成一排,从左到右依次编号为1,2,3....N.每次给定2个整数a b(a <= b),lele便为骑上他的“小飞鸽"牌电动车从气球a开始到气球b

Codeforces Round #225 (Div. 1) C 树状数组 || 线段树

看到这题很开心啊,有印象跟以前做过的很像,貌似最近就做过一个,以时间戳为区间来建立树状数组,然后一开始我以为题意是,给x点加val,它以下的所有节点都加-val:所以一开始就以 加 和 减 建立了两个树状数组,最后 减去就是答案,写完发现跟案例对不上啊,读了题目也没发现读错了,对于那句话 我理解错了,后来看了 这个: http://blog.csdn.net/keshuai19940722/article/details/18967661 仔细看看处理部分,我还以为分奇偶性有规律呢,后来才发现读

HDU 4417 Super Mario (树状数组/线段树)

Super Mario Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Description Mario is world-famous plumber. His “burly” figure and amazing jumping ability reminded in our memory. Now the poor princess is in trouble agai

BZOJ 3211 花神游历各国 树状数组(线段树)+优化

题意:给你一段区间,然后每个点的初始值都告诉你,现有两种操作,一种是给你一个小区间的左右端点,之后把这个区间内的所有值都开根号,另一种就是区间求值. 方法:树状数组维护求和,巧妙开根号.(线段树) 解析:这道是某次考试考的题- -.当时也没想到快的开根号方法,暴力开根号好像70分吧. 首先要明确一个事情:被开根号的数最大是109,而109开几次会开到1呢?用计算器算一下发现5次就将这个最大的数开到1了,而1开根号怎么开都是1,所以如果再对其进行开根号操作,仅仅是无用的浪费时间了.所以怎么维护这种