📜  细分树|高效实施

📅  最后修改于: 2021-04-17 08:22:51             🧑  作者: Mango

让我们考虑以下问题,以了解没有递归的段树。

我们有一个数组arr [0。 。 。 n-1]。我们应该能够

  1. 查找从索引l到r的元素的总和,其中0 <= l <= r <= n-1
  2. 将数组的指定元素的值更改为新值x。我们需要做arr [i] = x,其中0 <= i <= n-1。

一个简单的解决方案是运行从l到r的循环,并计算给定范围内的元素总和。要更新值,只需做arr [i] = x。第一次操作花费O(n)时间,第二次操作花费O(1)时间。

另一个解决方案是创建另一个数组,并将从开始到i的和存储在此数组的第i个索引处。现在可以以O(1)的时间计算给定范围的总和,但是更新操作现在需要O(n)的时间。如果查询操作的数量很大且更新很少,则此方法效果很好。

如果查询次数和更新次数相等怎么办?给定数组后,是否可以在O(log n)时间内执行这两个操作?我们可以使用细分树在O(Logn)时间内完成这两项操作。在之前的文章中,我们已经讨论了段树的完整实现。在这篇文章中,我们将讨论一种比上一篇文章更简单,更有效的实现段树的方法。

考虑如下所示的数组和段树:

从上图可以看到,原始数组在底部,并以16个元素为0索引。该树总共包含31个节点,叶节点或原始数组的元素从节点16开始。因此,我们可以轻松地使用2 * N大小的数组为此数组构造一个分段树,其中N是原始元素的数量大批。叶节点将从该数组中的索引N开始,然后上升到索引(2 * N – 1)。因此,原始数组中索引i处的元素将位于段树数组中的索引(i + N)处。现在计算父母,我们将从索引(N – 1)开始向上移动。对于索引i,其左子节点将在(2 * i)处,而右子节点将在(2 * i +1)索引处。因此,将节点(2 * i)和(2 * i +1)处的值组合在第i个节点上以构造树。
如上图所示,我们可以在[L,R)间隔中查询此树,其中包括左索引(L)和右(R)。

我们将使用按位运算符实现所有这些乘法和加法运算符。

让我们知道完整的实现:

C++
#include 
using namespace std;
  
// limit for array size
const int N = 100000; 
  
int n; // array size
  
// Max size of tree
int tree[2 * N];
  
// function to build the tree
void build( int arr[]) 
{ 
    // insert leaf nodes in tree
    for (int i=0; i 0; --i)     
        tree[i] = tree[i<<1] + tree[i<<1 | 1];    
}
  
// function to update a tree node
void updateTreeNode(int p, int value) 
{ 
    // set value at position p
    tree[p+n] = value;
    p = p+n;
      
    // move upward and update parents
    for (int i=p; i > 1; i >>= 1)
        tree[i>>1] = tree[i] + tree[i^1];
}
  
// function to get sum on interval [l, r)
int query(int l, int r) 
{ 
    int res = 0;
      
    // loop to find the sum in the range
    for (l += n, r += n; l < r; l >>= 1, r >>= 1)
    {
        if (l&1) 
            res += tree[l++];
      
        if (r&1) 
            res += tree[--r];
    }
      
    return res;
}
  
// driver program to test the above function 
int main() 
{
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
  
    // n is global
    n = sizeof(a)/sizeof(a[0]);
      
    // build tree 
    build(a);
      
    // print the sum in range(1,2) index-based
    cout << query(1, 3)<


Java
import java.io.*;
  
public class GFG {
      
    // limit for array size
    static int N = 100000; 
      
    static int n; // array size
      
    // Max size of tree
    static int []tree = new int[2 * N];
      
    // function to build the tree
    static void build( int []arr) 
    { 
          
        // insert leaf nodes in tree
        for (int i = 0; i < n; i++) 
            tree[n + i] = arr[i];
          
        // build the tree by calculating
        // parents
        for (int i = n - 1; i > 0; --i) 
            tree[i] = tree[i << 1] +
                      tree[i << 1 | 1]; 
    }
      
    // function to update a tree node
    static void updateTreeNode(int p, int value) 
    { 
          
        // set value at position p
        tree[p + n] = value;
        p = p + n;
          
        // move upward and update parents
        for (int i = p; i > 1; i >>= 1)
            tree[i >> 1] = tree[i] + tree[i^1];
    }
      
    // function to get sum on
    // interval [l, r)
    static int query(int l, int r) 
    { 
        int res = 0;
          
        // loop to find the sum in the range
        for (l += n, r += n; l < r;
                             l >>= 1, r >>= 1)
        {
            if ((l & 1) > 0) 
                res += tree[l++];
          
            if ((r & 1) > 0) 
                res += tree[--r];
        }
          
        return res;
    }
      
    // driver program to test the
    // above function 
    static public void main (String[] args)
    {
        int []a = {1, 2, 3, 4, 5, 6, 7, 8,
                                9, 10, 11, 12};
      
        // n is global
        n = a.length;
          
        // build tree 
        build(a);
          
        // print the sum in range(1,2)
        // index-based
        System.out.println(query(1, 3));
          
        // modify element at 2nd index
        updateTreeNode(2, 1);
          
        // print the sum in range(1,2)
        // index-based
        System.out.println(query(1, 3)); 
    }
}
  
// This code is contributed by vt_m.


Python3
# Python3 Code Addition
  
# limit for array size 
N = 100000; 
  
# Max size of tree 
tree = [0] * (2 * N); 
  
# function to build the tree 
def build(arr) :
  
    # insert leaf nodes in tree 
    for i in range(n) : 
        tree[n + i] = arr[i]; 
      
    # build the tree by calculating parents 
    for i in range(n - 1, 0, -1) : 
        tree[i] = tree[i << 1] + tree[i << 1 | 1]; 
  
# function to update a tree node 
def updateTreeNode(p, value) : 
      
    # set value at position p 
    tree[p + n] = value; 
    p = p + n; 
      
    # move upward and update parents 
    i = p;
      
    while i > 1 :
          
        tree[i >> 1] = tree[i] + tree[i ^ 1]; 
        i >>= 1; 
  
# function to get sum on interval [l, r) 
def query(l, r) : 
  
    res = 0; 
      
    # loop to find the sum in the range 
    l += n;
    r += n;
      
    while l < r :
      
        if (l & 1) :
            res += tree[l]; 
            l += 1
      
        if (r & 1) :
            r -= 1;
            res += tree[r]; 
              
        l >>= 1;
        r >>= 1
      
    return res; 
  
# Driver Code
if __name__ == "__main__" : 
  
    a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; 
  
    # n is global 
    n = len(a); 
      
    # build tree 
    build(a); 
      
    # print the sum in range(1,2) index-based 
    print(query(1, 3)); 
      
    # modify element at 2nd index 
    updateTreeNode(2, 1); 
      
    # print the sum in range(1,2) index-based 
    print(query(1, 3)); 
      
# This code is contributed by AnkitRai01


C#
using System;
  
public class GFG {
      
    // limit for array size
    static int N = 100000; 
      
    static int n; // array size
      
    // Max size of tree
    static int []tree = new int[2 * N];
      
    // function to build the tree
    static void build( int []arr) 
    { 
          
        // insert leaf nodes in tree
        for (int i = 0; i < n; i++) 
            tree[n + i] = arr[i];
          
        // build the tree by calculating
        // parents
        for (int i = n - 1; i > 0; --i) 
            tree[i] = tree[i << 1] +
                       tree[i << 1 | 1]; 
    }
      
    // function to update a tree node
    static void updateTreeNode(int p, int value) 
    { 
        // set value at position p
        tree[p + n] = value;
        p = p + n;
          
        // move upward and update parents
        for (int i = p; i > 1; i >>= 1)
            tree[i >> 1] = tree[i] + tree[i^1];
    }
      
    // function to get sum on
    // interval [l, r)
    static int query(int l, int r) 
    { 
        int res = 0;
          
        // loop to find the sum in the range
        for (l += n, r += n; l < r;
                             l >>= 1, r >>= 1)
        {
            if ((l & 1) > 0) 
                res += tree[l++];
          
            if ((r & 1) > 0) 
                res += tree[--r];
        }
          
        return res;
    }
      
    // driver program to test the
    // above function 
    static public void Main ()
    {
        int []a = {1, 2, 3, 4, 5, 6, 7, 8,
                            9, 10, 11, 12};
      
        // n is global
        n = a.Length;
          
        // build tree 
        build(a);
          
        // print the sum in range(1,2)
        // index-based
        Console.WriteLine(query(1, 3));
          
        // modify element at 2nd index
        updateTreeNode(2, 1);
          
        // print the sum in range(1,2)
        // index-based
        Console.WriteLine(query(1, 3)); 
    }
}
  
// This code is contributed by vt_m.


输出:

5
3

是的!这是所有的了。与以前的递归代码相比,用更少的代码行数完整地实现了包含查询和更新功能的段树。现在让我们了解每个函数的工作方式:

  1. 图片清楚地表明叶节点存储在i + n处,因此我们可以清楚地直接插入所有叶节点。
  2. 下一步是构建树,这需要O(n)时间。父节点的索引始终小于子节点的索引,因此我们仅按降序处理所有节点,计算父节点的值。如果构建函数用于计算父级的代码令人困惑,那么您可以看到此代码,它等同于构建函数。
    tree[i]=tree[2*i]+tree[2*i+1]
    
  3. 在任何位置更新值也很简单,花费的时间将与树的高度成比例。我们仅更新要更改的给定节点的父节点中的值。因此,为了获取父节点,我们只需转到节点p的父节点,即p / 2或p >> 1。 p ^ 1将(2 * i)变成(2 * i +1),反之亦然,得到p的第二个孩子。
  4. 计算总和也可以在O(log(n))时间中进行。如果我们经过[3,11)的时间间隔,则只需要按该顺序为节点19、26、12和5计算。

查询函数背后的想法是,我们应该在总和中包括元素还是应该包括其父元素。让我们再次看一下图像以进行适当的理解。考虑到L是区间的左边界,R是区间[L,R)的右边界。从图像中可以清楚地看到,如果L为奇数,则表示它是其父项的正确子项,并且我们的区间仅包含L而不是其父项。因此,通过执行L =(L + 1)/ 2,我们将简单地包括此节点即可求和并移动到其下一个节点的父节点。现在,如果L是偶数,则它是其父级的左子级,并且除非右边界干扰,否则interval还将包括它的父级。同样的条件也适用于右边界,以加快计算速度。一旦左右边界相遇,我们将停止此迭代。

先前实现和此实现的理论时间复杂度相同,但实际上,由于没有递归调用,因此效率更高。我们只是简单地迭代所需的元素。同样,这很容易实现。

时间复杂度:

  • 树木结构:O(n)
  • 查询范围:O(Log n)
  • 更新元素:O(Log n)。

参考文献
http://codeforces.com/blog/entry/18051