给定一个庞大的整数数组,优化函数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)
提供从索引i
到j
的数字总和。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
对应的整个行/列。
面试官给我提示做一些预处理并使用一些数据结构,但我无法想到。
答案 0 :(得分:1)
您需要的是细分树。细分树可以在sum(i, j)
时间内执行update(i, value)
和O(log(n))
。
引自Wikipedia:
在计算机科学中,分段树是用于存储间隔或分段的树数据结构。它允许查询哪些存储的段包含给定的点。原则上,它是静态结构;也就是说,它的结构一旦建成就无法修改。
树的叶子将是初始数组元素。他们的父母将是他们孩子的总和。例如:假设data[] = {2, 5, 4, 7, 8, 9, 5}
,那么我们的树将如下:
此树结构使用数组表示。让我们调用这个数组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的幂时。在这种情况下,我们假设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)
。我们通过调用build(0, 0, n-1)
开始构建过程。第一个参数表示 seg_tree [] 中根的位置。第二个和第三个参数表示 data [] 中要为其形成分段树表示的区间。在每个后续调用中,节点将表示 seg_tree [] 的索引,(开始,结束)将表示 seg_tree的间隔[node] 将存储总和。
有三种情况:
start > end
是一个无效的间隔,我们只是从此调用返回。 start == end
代表 seg_tree [] 的叶子,因此我们初始化tree[node] = data[start]
build(node, start, (start + end)/2)
来构建此节点的左子节点,然后通过调用build(node, 1+(start+end)/2, end)
来调用正确的子树。然后我们通过其子节点的总和初始化 seg_tree [] 中的当前索引。我们需要检查节点的间隔是否与给定的间隔(i,j)重叠(部分/完整),或者它们根本不重叠。这是三种情况:
假设我们需要找到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。
对于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
的最终价值。
最终的树将如下所示:
我们可以在O(n)
时间内使用数组创建分段树表示,这两个操作的时间复杂度均为O(log(n))
。
通常,Segment Trees可以有效地执行以下操作:
另一个数据结构Interval Tree也可用于解决此问题。推荐读物: