优化sum(i,j)和更新(i,value)整数的arryas

时间:2016-06-18 09:54:56

标签: optimization segment-tree

给定一个庞大的整数数组,优化函数sum(i,j)和update(i,value),这样两个函数都小于O(n)。

更新

这是一个面试问题。我尝试了O(n)sum(i,j)和O(1)update(i,value)。其他解决方案是将输入数组预处理为2-d数组,以得到sum(i,j)的O(1)答案。但这使得O(n)的更新功能。

例如,给定一个数组:

A[] = {4,5,4,3,6,8,9,1,23,34,45,56,67,8,9,898,64,34,67,67,56,...}

要定义的操作是sum(i,j)update(i,value)

  • sum(i,j)提供从索引ij的数字总和。
  • update(i, value)使用给定的i更新索引value的值。

非常直接的答案是sum(i,j)可以在O(n)时间内计算,并在(i,value)时间更新O(1)

第二种方法是预先计算sum(i,j)并将其存储在二维数组SUM[n][n]中,并在查询时,在O(1)时间内给出答案。但随后更新函数update(i,value)变为订单O(n),因为必须更新与索引i对应的整个行/列。

面试官给我提示做一些预处理并使用一些数据结构,但我无法想到。

1 个答案:

答案 0 :(得分:1)

您需要的是细分树。细分树可以在sum(i, j)时间内执行update(i, value)O(log(n))

引自Wikipedia

  

在计算机科学中,分段树是用于存储间隔或分段的树数据结构。它允许查询哪些存储的段包含给定的点。原则上,它是静态结构;也就是说,它的结构一旦建成就无法修改。

树的叶子将是初始数组元素。他们的父母将是他们孩子的总和。例如:假设data[] = {2, 5, 4, 7, 8, 9, 5},那么我们的树将如下:

Photo created using Visualgo.net

此树结构使用数组表示。让我们调用这个数组seg_tree。因此 seg_tree [] 的根将存储在数组的索引1处。它的两个孩子将被存储在索引2和3中。1-indexed表示的一般趋势是:

  • 索引i的左子项位于索引2*i
  • 索引i的右子项位于索引2*i+1
  • 索引i的父级位于索引i/2

0-indexed代表:

  • 索引i的左子项位于索引2*i+1
  • 索引i的右子项位于索引2*i+2
  • 索引i的父级位于索引(i-1)/2

上图中的每个区间[i, j]表示区间data[i, j]中所有元素的总和。根节点将表示整个数据[]的总和,即sum(0, 6)。它的两个孩子将表示sum(0, 3)sum(4, 6)等等。

seg_tree [] MAX_LEN 的长度将是(如果n = length of data[]):

  • 2*n-1当n为2的幂时
  • 2*(2^(log_2(n)+1) - 1当n不是2的幂时。

从data []构建seg_tree []:

在这种情况下,我们假设0-indexed构造。索引0将是树的根,初始数组的元素将存储在叶子中。

data[0...(n-1)]是初始数组,seg_tree[0...MAX_LEN] data [] 的分段树表示。理解如何从伪代码构造树将更容易:

build(node, start, end) {

    // invalid interval
    if start > end:
        return

    // leaf nodes
    if start == end:
        tree[node] = data[start]
        return

    // build left and right subtrees
    build(2*node+1, start, (start + end)/2);
    build(2*node+2, 1+(start+end)/2, end);

    // initialize the parent with the sum of its children
    tree[node] = tree[2*node+1] + tree[2*node+2]
}

下面,

  • [start, end]表示要为其形成分段树表示的data []中的间隔。最初,这是(0, n-1)
  • node 表示 seg_tree [] 中的当前索引。

我们通过调用build(0, 0, n-1)开始构建过程。第一个参数表示 seg_tree [] 中根的位置。第二个和第三个参数表示 data [] 中要为其形成分段树表示的区间。在每个后续调用中,节点将表示 seg_tree [] 的索引,(开始,结束)将表示 seg_tree的间隔[node] 将存储总和。

有三种情况:

  • start > end是一个无效的间隔,我们只是从此调用返回。
  • if start == end代表 seg_tree [] 的叶子,因此我们初始化tree[node] = data[start]
  • 否则,我们处于一个不是叶子的有效间隔中。因此,我们首先通过调用build(node, start, (start + end)/2)来构建此节点的左子节点,然后通过调用build(node, 1+(start+end)/2, end)来调用正确的子树。然后我们通过其子节点的总和初始化 seg_tree [] 中的当前索引。

对于sum(i,j):

我们需要检查节点的间隔是否与给定的间隔(i,j)重叠(部分/完整),或者它们根本不重叠。这是三种情况:

  • 如果没有重叠,我们只需返回0。
  • 对于完全重叠,我们将返回存储在该节点的值。
  • 对于部分重叠,我们将访问两个孩子并递归地继续此检查。

假设我们需要找到sum(1, 5)的值。我们按如下方式进行:

让我们拿一个空容器(Q)来存储感兴趣的间隔。最终,所有这些范围将被它们返回的值替换。最初,Q = {(0, 6)}

我们注意到(1,5)(0,6)不完全重叠,因此我们删除此范围并添加其子范围。 Q = {(0, 3), (4, 6)}

现在,(1,5)部分重叠(0,3)。所以我们删除(0,3)并插入它的两个子节点。 Q = {(0, 1), (2, 3), (4, 6)}

(1,5)部分重叠(0,1),因此我们将其删除并插入其两个子范围。 Q = {(0, 0), (1, 1), (2, 3), (4, 6)}

现在(1,5)(0,0)不重叠,所以我们用值替换(0,0)它将返回(由于没有重叠,它为0)。 Q = {(0, (1, 1), (2, 3), (4, 6)}

接下来,(1,5)(1,1)完全重叠,因此我们返回存储在代表此范围的节点中的值(即5) 。 Q = {0, 5, (2, 3), (4, 6)}

接下来,(1,5)再次完全重叠(2,3),因此我们返回值11. Q = {0, 5, 11, (4, 6)}

接下来,(1,5)部分重叠(4,6),所以我们用它的两个孩子替换这个范围。 Q = {0, 5, 11, (4, 5), (6, 6)}

快速转发操作,我们注意到(1,5)(4,5)完全重叠,因此我们将其替换为17和(1, 5)不重叠(6,6),所以我们将其替换为0.最后,Q = {0, 5, 11, 17, 0}。查询的答案是Q中所有元素的总和,即33。

更新(i,value):

对于update(i, value),过程有点类似。首先,我们将搜索范围(i,i)。我们在此路径中遇到的所有节点也需要更新。让change = (new_value - old_value)。然后在搜索范围(i,i)时遍历树时,我们将此更改添加到除最后一个节点之外的所有节点,这些节点将被新值替换。例如,让查询为update(5, 8)

change = 8-9 = -1

遇到的路径为(0, 6) -> (4, 6) -> (4, 5) -> (5, 5)

(0, 6) = 40 + change = 40 - 1 = 39的最终价值。

(4, 6) = 22 + change = 22 - 1 = 21的最终价值。

(4, 5) = 17 + change = 17 - 1 = 16的最终价值。

(5, 5) = 8的最终价值。 最终的树将如下所示:

Photo created using Visualgo.net

我们可以在O(n)时间内使用数组创建分段树表示,这两个操作的时间复杂度均为O(log(n))

通常,Segment Trees可以有效地执行以下操作:

  • 更新:它可以更新:
    • 给定索引处的元素。
    • 区间中给定值的所有元素。在这种情况下,通常采用 Lazy Propagation 技术来实现效率。
  • 查询:我们在给定的时间间隔内查询某些值。几种基本类型的查询是:
    • 区间中的最小元素
    • 间隔中的最大元素
    • 间隔中所有元素的总和/乘积

另一个数据结构Interval Tree也可用于解决此问题。推荐读物: