让我们考虑以下问题,以了解没有递归的段树。
我们有一个数组arr [0。 。 。 n-1]。我们应该能够
- 查找从索引l到r的元素的总和,其中0 <= l <= r <= n-1
- 将数组的指定元素的值更改为新值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
是的!这是所有的了。与以前的递归代码相比,用更少的代码行数完整地实现了包含查询和更新功能的段树。现在让我们了解每个函数的工作方式:
- 图片清楚地表明叶节点存储在i + n处,因此我们可以清楚地直接插入所有叶节点。
- 下一步是构建树,这需要O(n)时间。父节点的索引始终小于子节点的索引,因此我们仅按降序处理所有节点,计算父节点的值。如果构建函数用于计算父级的代码令人困惑,那么您可以看到此代码,它等同于构建函数。
tree[i]=tree[2*i]+tree[2*i+1]
- 在任何位置更新值也很简单,花费的时间将与树的高度成比例。我们仅更新要更改的给定节点的父节点中的值。因此,为了获取父节点,我们只需转到节点p的父节点,即p / 2或p >> 1。 p ^ 1将(2 * i)变成(2 * i +1),反之亦然,得到p的第二个孩子。
- 计算总和也可以在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