我有以下需要优化的问题。对于给定数组(允许重复键),对于数组中的每个位置i
,我需要计算i
右侧的所有较大值,以及i
左侧的所有较小值。如果我们有:
1 1 4 3 5 6 7
和i = 3
(值3),i
左侧较小值的计数为1
(无重复键),右侧为较大值的数量为3
。
这个问题的强力解决方案是~N^2
,我可以通过一些额外的空间来计算较大值的较小值,从而将复杂度降低到~(N^2)/2
。
我的问题是:有没有更快的方法来完成它?也许NlgN
?我想有一个数据结构,我不知道哪个允许我更快地进行计算。
编辑:谢谢大家的回复和讨论。你可以找到两个好的解决方案,下面的问题。总是很高兴从stackoverflow中的开发人员那里学习。
答案 0 :(得分:2)
尝试用于解决RMQ的分段树数据结构。 它会准确地给你n log n。
通常会查看RMQ problem,您的问题可能会减少。
答案 1 :(得分:2)
您要求O(n log n)
,我在技术上给您一个O(n)
解决方案。
正如@SayonjiNakate暗示的那样,使用分段树的解决方案(我在实现中使用Fenwick树)在O(n log M)
时运行,其中M
是数组中可能的最大值。假设M
是常量(嘿它受int的大小限制!),算法在O(n)
中运行。对不起,如果你觉得我在报告复杂性时作弊,但是嘿,从技术上来说这是真的! = d
首先,请注意问题“左边较小元素的数量”通过反转和否定数组等同于“右边较大元素的数量”的问题。所以,在我下面的解释中,我只描述了“左边较小元素的数量”,我称之为“lesser_left_count”。
lesser_left_count的算法:
我们的想法是能够找到小于特定数字的总数。
定义大小最高为tree
的数组MAX_VALUE
,其中会为看到的数字存储值1
,否则会存储0
。
然后,当我们遍历数组时,当我们看到数字num
时,只需将值1
指定给tree[num]
(更新操作) 。然后,对于数字num
,lesser_left_count是从1
到num-1
(总和操作)的总和到目前为止,因为当前位置左边的所有较小数字都会已设置为1
。
简单吧?如果我们使用Fenwick tree,则可以在O(log M)
时间内完成更新和求和操作,其中M
是数组中的最大可能值。由于我们迭代数组,如果我们将O(n log M)
视为常量(在我的代码中我将其设置为O(n)
,则总时间为M
,或仅2^20-1 = 1048575
,因此它是O(20n)
,即O(n)
)
朴素解决方案的唯一缺点是它使用了大量内存,因为M
变得更大(我在代码中设置了M=2^20-1
,这需要大约4MB的内存)。这可以通过将数组中的不同整数映射到较小的整数(以保持顺序的方式)来改进。通过对数组进行排序,可以简单地在O(n log n)
中完成映射(确定,这会产生复杂性O(n log n)
,但正如我们所知n < M
,您可以将其视为O(n)
)。因此,数字M
可以重新解释为“数组中不同元素的数量”。
因此内存不再是任何问题,因为如果在这个改进之后你确实需要大量内存,那就意味着你的数组中有许多不同的数字,以及{的时间复杂度{1}}已经太高而无法在普通机器中计算出来。
为了简单起见,我没有在代码中包含这种改进。
哦,由于Fenwick树仅适用于正数,我将数组中的数字转换为最小值1.请注意,这不会改变结果。
Python代码:
O(n)
将产生:
Original array: [1, 1, 3, 2, 4, 5, 6] Lesser left count: [0, 0, 1, 1, 3, 4, 5] Greater right cnt: [5, 5, 3, 3, 2, 1, 0]
或者如果你想要Java代码:
MAX_VALUE = 2**20-1
f_arr = [0]*MAX_VALUE
def reset():
global f_arr, MAX_VALUE
f_arr[:] = [0]*MAX_VALUE
def update(idx,val):
global f_arr
while idx<MAX_VALUE:
f_arr[idx]+=val
idx += (idx & -idx)
def cnt_sum(idx):
global f_arr
result = 0
while idx > 0:
result += f_arr[idx]
idx -= (idx & -idx)
return result
def count_left_less(arr):
reset()
result = [0]*len(arr)
for idx,num in enumerate(arr):
cnt_prev = cnt_sum(num-1)
if cnt_sum(num) == cnt_prev: # If we haven't seen num before
update(num,1)
result[idx] = cnt_prev
return result
def count_left_right(arr):
arr = [x for x in arr]
min_num = min(arr)
if min_num<=0: # Got nonpositive numbers!
arr = [min_num+1+x for x in arr] # Convert to minimum 1
left = count_left_less(arr)
arr.reverse() # Reverse for greater_right_count
max_num = max(arr)
arr = [max_num+1-x for x in arr] # Negate the entries, keep minimum 1
right = count_left_less(arr)
right.reverse() # Reverse the result, to align with original array
return (left, right)
def main():
arr = [1,1,3,2,4,5,6]
(left, right) = count_left_right(arr)
print 'Array: ' + str(arr)
print 'Lesser left count: ' + str(left)
print 'Greater right cnt: ' + str(right)
if __name__=='__main__':
main()
会产生相同的结果。
答案 2 :(得分:2)
这是一个相对简单的解决方案,O(N lg(N))
不依赖于有限多个整数中的条目(特别是,它应该适用于任何有序的数据类型)。
我们假设输出存储在两个数组中; lowleft[i]
最后会包含x[j]
和j < i
的不同值x[j] < x[i]
的数量,而highright[i]
最后会包含不同值的数量{ {1}} x[j]
和j > i
。
创建一个平衡的树数据结构,在每个节点中维护以该节点为根的子树中的节点数。这是相当标准的,但我认为不是Java标准库的一部分;做AVL树可能最容易。节点中值的类型应该是数组中值的类型。
现在首先通过数组迭代前进。我们从一个空的平衡树开始。对于我们遇到的每个值x[j] > x[i]
,我们将其输入到平衡树中(在该树的末尾附近有x[i]
个条目,因此此步骤需要O(N)
次。在搜索要输入O(lg(N))
的位置时,我们会在每次采用正确的子树时,通过将所有左子树的大小相加来跟踪小于x[i]
的值的数量,并添加将是x[i]
左子树的大小。我们将此号码输入x[i]
。
如果值lowleft[i]
已经在树中,我们继续进行该循环的下一次迭代。如果值x[i]
不在那里,我们输入它并重新平衡树,注意正确更新子树大小。
此循环的每次迭代都需要x[i]
个步骤,总计O(lg(N))
。我们现在从一个空树开始,并做同样的事情,通过数组迭代向后,找到树中每个O(N lg(N))
的位置,并且每次记录所有子树的大小到新节点的权限为x[i]
。因此总复杂度为highright[i]
。
答案 3 :(得分:0)
这是一个应该给你O(NlgN)
的算法:
迭代列表一次并构建key => indexList
的地图。因此,对于永远的键(数组中的元素),您存储该键在该数组中的所有索引的列表。这将采用O(N)
(遍历列表)+ N*O(1)
(将N个项目附加到列表)步骤。所以这一步是O(N)
。第二步要求对这些列表进行排序,因为我们从左到右迭代列表,所以列表中新插入的索引总是比已经存在的所有其他索引大。
再次迭代列表,并为每个元素搜索索引列表中所有键,这些键大于当前索引之后的第一个索引的当前元素。这将为您提供当前元素右侧的元素数量,这些元素的数量大于当前元素。由于索引列表已排序,您可以进行二进制搜索,其中O(k * lgN)
步骤k
是大于当前键的键数。如果键的数量有一个上限,那么就big-O而言这是一个常数。这里的第二步是搜索所有较小的键,并在列表中找到当前索引之前的第一个索引。这将为您提供当前较小的元素的数量。与上述相同的推理是O(k * lgN)
因此,假设密钥数量有限,如果我没有弄错的话,这应该给你O(N) + N * 2 * O(lgN)
整体O(NlgN)
。
修改:伪代码:
int[] list;
map<int => int[]> valueIndexMap;
foreach (int i = 0; i < list.length; ++i) { // N iterations
int currentElement = list[i]; // O(1)
int[] indexList = valueIndexMap[currentElement]; // O(1)
indexList.Append(i); // O(1)
}
foreach (int i = 0; i < list.length; ++i) { // N iterations
int currentElement = list[i]; // O(1)
int numElementsLargerToTheRight;
int numElementsSmallerToTheLeft;
foreach (int k = currentElement + 1; k < maxKeys; ++k) { // k iterations with k being const
int[] indexList = valueIndexMap[k]; // O(1)
int firstIndexBiggerThanCurrent = indexList.BinaryFindFirstEntryLargerThan(i); // O(lgN)
numElementsLargerToTheRight += indexList.Length - firstIndexBiggerThanCurrent; // O(1)
}
foreach (int k = currentElement - 1; k >= 0; --k) { // k iterations with k being const
int[] indexList = valueIndexMap[k]; // O(1)
int lastIndexSmallerThanCurrent = indexList.BinaryFindLastEntrySmallerThan(i); // O(lgN)
numElementsSmallerToTheLeft += lastIndexSmallerThanCurrent; // O(1)
}
}
更新:如果有人有兴趣,我会在with a C# implementation附近修补一下;