zoukankan      html  css  js  c++  java
  • 线段树和树状数组

    线段树

    引入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+2j−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

  • 相关阅读:
    1105 Spiral Matrix (25分)(蛇形填数)
    1104 Sum of Number Segments (20分)(long double)
    1026 Table Tennis (30分)(模拟)
    1091 Acute Stroke (30分)(bfs,连通块个数统计)
    1095 Cars on Campus (30分)(排序)
    1098 Insertion or Heap Sort (25分)(堆排序和插入排序)
    堆以及堆排序详解
    1089 Insert or Merge (25分)
    1088 Rational Arithmetic (20分)(模拟)
    1086 Tree Traversals Again (25分)(树的重构与遍历)
  • 原文地址:https://www.cnblogs.com/Acapplella/p/13303982.html
Copyright © 2011-2022 走看看