<span>线段树和树状数组</span>

线段树

引入1:有n个数(n<=50000)个数,m(m<=50000)次询问。每次询问区间L到R的数的和。要求输出每一次询问的结果......
分析:
1.用前缀和问题进行求解:再开一个数组(暂且记为b[n],设n个数所组成的数组为a[n]),b[i]用来记录从a[1]到a[i]的所有数字的和(即 b[1]=a[1],b[2]=b[1]+a[2],...,b[i]=b[i-1]+a[i])。这样对于m次询问,只需要对数组b进行相应的操作就可以了。
eg:求数组a中区间[L,R]的所有数字的和sum,即sum=a[L]+a[L+1]+...+a[R-1]+a[R]。由于我们之前已经对数组a求了前缀和,并得到前缀和数组b[n]。因此对b[n]进行操作就可以了,即sum=b[R]-b[L-1]。
2.用线段树进行求解:这里先告诉大家可以用线段树来进行求解,具体如何求解我们可以等对线段树进行一定的讲解后再来进行求解。
引入2:RMQ(Range Minimum/Maximum Query)问题
有n(n<=50000)个数,m(m<=50000)次询问,要求每次输出区间[L,R]的数的最大值。
分析:
1.用ST表进行求解:一种利用dp思想求解区间最值的倍增算法。
定义:f(i,j)表示[i,i+\(2^j\)−1]这段长度为2^j的区间中的最大值。
预处理:f(i,0)=a[i]。即[i,i]区间的最大值就是a[i]。
状态转移:将[i,j]平均分成两段,一段为[i,i+2^(j−1)−1],另一段为[i+2^(j−1),i+2^j−1]。两段的长度均为2^(j−1)。f(i,j)的最大值即这两段的最大值中的最大值。得到f(i,j)=max{f(i,j−1),f(i+2^(j−1),j−1)}。
查询:若需要查询的区间为[i,j],则需要找到两个覆盖这个闭区间的最小幂区间。
这两个区间可以重复,因为两个区间是否相交对区间最值没有影响。(如下图)

当然求区间和就肯定不行了。这也就是RMQ的限制性。
因为区间的长度为j−i+1,所以可以取k=\(\log_2 (j-i+1)\)
则所求即为max{f(i,k),f(j−2k+1,k)}。
参考代码如下:

#include<iostream>//这段代码以询问最大值为例
#include<algorithm>
#include<cmath>
using namespace std;
int a[50005];
int f[50005][16];
int main()
{
    ios::sync_with_stdio(false);
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    cin>>a[i];
    for(int i=1;i<=n;i++)
    f[i][0]=a[i];//可以参照上文的预处理部分
    /*
    注意外部循环从j开始,
    因为初始状态为f[i][0],
    以i为外层会有一些状态遍历不到。
    */ 
    for(int j=1;j<=16;j++)//2^16>50000足够了
       for(int i=1;i<=n;i++)//可以参照上文的状态转移部分
          if(i+(1<<j)-1<=n)//判断区间最右端的值是否超过n
          {
              f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
          }
    int l,r;
    while(m--)//处理m次询问
    {
        cin>>l>>r;
        int k=(int)(log((double)(r-l+1))/(log(2.0)));
        cout<<max(f[l][k],f[r-(1<<k)+1][k])<<endl;
    }
    return 0;
}

参考例题:
【模板】ST表
质量检测
2.用线段树进行求解:这里先告诉大家可以用线段树来进行求解,具体如何求解我们可以等对线段树进行一定的讲解后再来进行求解。
概念:
线段树是用一种树状结构来存储一个连续区间的信息的数据结构。它主要用于处理一段连续区间的插入,查找,统计,查询等操作。
复杂度:设区间长度是n,所有操作的复杂度是logn级别。

  • 线段树也是一种树形结构,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶节点。线段树是建立在线段的基础上,每个节点都代表了一条线段[a,b],长度为 1 的线段称为元线段。非元线段都有两个子节点,左节点代表的线段为[a,(a+b)/2],右节点代表的线段为[((a+b)/2)+1,b]。
  • 线段[1,7]的线段树和[2,5]的分解

    线段树的几点性质:
  • 线段树是平衡的二叉树,最大深度logn(n为线段树所表示区间的长度)
  • 树中的每一个节点代表对应一个区间(叶子节点是一个点……)
  • 每个节点(所代表的区间)完全包含它的所有孙节点
  • 对于任意两个节点(所代表的区间):要么完全包含,要么互不相交
  • 在进行区间操作和统计时把区间等价转化成若干个子区间(logn个)的相同操作
  • 任意的线段[a,b]在线段树的查询或查找过程中把这个线段最多分成log(b-a)份(显然每一层最多两个区间)
  • So,线段树除建树外的操作都是log(n)级别的复杂度。
    使用线段树可以快速查找某个节点在若干条区间中出现的次数、某个区间的最大或最小值、某个区间的和等问题。这时就需要了解线段树的 3 个基本操作:建树、更新和查询。

例1:
给你一个长度为n的序列,有如下操作,将第i个数加或减x,求区间Li到Ri的和。
解析:
1:建树
-建树时,可以使用结构体的方法,将节点的左右端点、记录的信息(最大值、最小值、区间和等)存储进去;也可使用一个数组保存,使用数组时,省略了左右端点,因为在递归过程中,左右端点可以通过值传递的方式得到。建树就是一个遍历二叉树的过程,从根节点出发,依次遍历左子树,到元线段返回,再遍历右子树,直到遍历结束。
建树过程代码如下:

void buildTree(int p, int l,int r)//p:数组下标;l:区间左端点;r:区间右端点
{
    if(l==r)  
    {
        tree[p]=arr[l];
        return;
    }
    int mid=(l+r)/2;
    buildTree(p*2,l,mid);//建立左子树
    buildTree(p*2+1,mid+1,r);//建立右子树
    tree[p]=tree[p*2]+tree[p*2+1];
}

-更新操作和查询操作类似,时间复杂度都为O(logn)。当执行更新操作时,要遍历到叶子节点,再层层递归更新,而查询操作只需要查询到包括的区间为止。
更新操作如下:

void change(int p,int l, int r,int x, int num)//给下标为x的位置加上num
{
    if(l==r)
    {
        tree[p]+=num;
        return;
    }
    int mid=(l+r)/2;
    if(x<=mid)
    {
        change(p*2,l,mid,x,num);
    }
    else
    {
        change(p*2+1,mid+1,r,x,num);
    }
    tree[p]=tree[p*2]+tree[p*2+1];
}

查询操作如下:

int query(int p,int l,int r, int x, int y)
{
    if(x<=l&&r<=y)
    return tree[p];
    int mid=(l+r)/2;
    if(y<=mid)
    return query(p*2,l,mid,x,y);
    if(x>mid)
    return query(p*2+1,mid+1,r,x,y);
    return (query(p*2,l,mid,x,y)+query(p*2+1,mid+1,r,x,y));
}

完整代码如下:

#include<iostream>
using namespace std;
int arr[10005],tree[10005];
void buildTree(int p, int l,int r)//p:数组下标;l:区间左端点;r:区间右端点
{
    if(l==r)  
    {
        tree[p]=arr[l];
        return;
    }
    int mid=(l+r)/2;
    buildTree(p*2,l,mid);//建立左子树
    buildTree(p*2+1,mid+1,r);//建立右子树
    tree[p]=tree[p*2]+tree[p*2+1];
}
void change(int p,int l, int r,int x, int num)//给下标为x的位置加上num
{
    if(l==r)
    {
        tree[p]+=num;
        return;
    }
    int mid=(l+r)/2;
    if(x<=mid)
    {
        change(p*2,l,mid,x,num);
    }
    else
    {
        change(p*2+1,mid+1,r,x,num);
    }
    tree[p]=tree[p*2]+tree[p*2+1];
}
int query(int p,int l,int r, int x, int y)
{
    if(x<=l&&r<=y)
    return tree[p];
    int mid=(l+r)/2;
    if(y<=mid)
    return query(p*2,l,mid,x,y);
    if(x>mid)
    return query(p*2+1,mid+1,r,x,y);
    return (query(p*2,l,mid,x,y)+query(p*2+1,mid+1,r,x,y));
}
int main()
{
    ios::sync_with_stdio(false);
    int n,m,k;//n个元素,m次修改,k次询问
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++)
    cin>>arr[i];
    buildTree(1,1,n);
   while(m--)
    {
        int x,num;
        cin>>x>>num;
        change(1,1,n,x,num);
    }
    while(k--)
    {
        int x,y;
        cin>>x>>y;
        cout<<query(1,1,n,x,y)<<endl;
    }
    return 0;
}

线段树和树状数组的题单:https://ac.nowcoder.com/acm/problem/collection/621
参考资料:
1.https://www.cnblogs.com/YSFAC/p/7189571.html
2.https://www.cnblogs.com/TheRoadToTheGold/p/6254255.html

全部评论

相关推荐

02-26 09:15
已编辑
蚌埠学院 golang
点赞 评论 收藏
分享
上周组里招人,我面了六个候选人,回来跟同事吃饭的时候聊起一个让我挺感慨的现象。前三个候选人,算法题写得都不错。第一道二分查找,五分钟之内给出解法,边界条件也处理得干净。第二道动态规划,状态转移方程写对了,空间复杂度也优化了一版。我翻他们的简历,力扣刷题量都在300以上。后三个呢,就有点参差不齐了。有的边界条件没处理好,有的直接说这道题没刷过能不能换个思路讲讲。其中有一个女生,我印象特别深——她拿到题之后没有马上写,而是先问我:“面试官,我能先跟你确认一下我对题目的理解吗?”然后她把自己的思路讲了一遍,虽然最后代码写得不是最优解,但整个沟通过程非常顺畅。这个女生的代码不是最优的,但当我问她“如果这里是线上环境,你会怎么设计’的时候,她给我讲了一套完整的方案——异常怎么处理、日志怎么打、怎么平滑发布。她对这是之前在实习的时候踩过的坑。”我在想LeetCode到底在筛选什么?我自己的经历可能有点代表性。我当年校招的时候,也是刷了三百多道题才敢去面试。那时候大家都刷,你不刷就过不了笔试关。后来工作了,前三年基本没再打开过力扣。真正干活的时候,没人让你写反转链表,也没人让你手撕红黑树。更多的是:这个接口为什么慢了、那个服务为什么OOM了、线上数据对不上了得排查一下。所以后来我当面试官,慢慢调整了自己的评判标准。算法题我还会出,但目的变了。我出算法题,不是想看你能不能背出最优解。而是想看你拿到一个陌生问题的时候,是怎么思考的。你会先理清题意吗?你会主动问边界条件吗?你想不出来的时候会怎么办?你写出来的代码,变量命名乱不乱、结构清不清楚?这些才是工作中真正用得到的能力。LeetCode是一个工具,不是目的。它帮你熟悉数据结构和常见算法思路,这没问题。但如果你刷了三百道题,却说不清楚自己的项目解决了什么问题、遇到了什么困难、你是怎么解决的,那这三百道题可能真的白刷了。所以还要不要刷LeetCode?要刷,但别只刷题。刷题的时候,多问自己几个为什么:为什么用这个数据结构?为什么这个解法比那个好?如果换个条件,解法还成立吗?把刷题当成锻炼思维的方式,而不是背答案的任务。毕竟面试官想看到的,从来不是一台背题机器,而是一个能解决问题的人。
国企上岸了的向宇同桌...:最害怕答非所问了,但是频繁反问确定意思又害怕面试官觉得我笨
AI时代还有必要刷lee...
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务