【问题标题】:Compute smaller and bigger values for an array position为数组位置计算越来越小的值
【发布时间】:2020-07-21 15:50:43
【问题描述】:

我有以下问题需要优化。对于给定的数组(允许重复键),对于数组中的每个位置i,我需要计算i 右侧的所有较大值,以及i 左侧的所有较小值。如果我们有:

1 1 4 3 5 6 7i = 3(值3),i左边较小值的个数是1(没有重复键),右边较大值的个数是3 .

这个问题的强力解决方案是~N^2,并且有了一些额外的空间,我可以设法从较大的值中计算出较小的值,从而将复杂性降低到~(N^2)/2。 我的问题是:有没有更快的方法来完成它?也许NlgN?我想有一个我不知道的数据结构可以让我更快地进行计算。

编辑:感谢大家的回复和讨论。您可以找到两个很好的解决方案两个以下问题。在 stackoverflow 中向开发人员学习总是一种乐趣。

【问题讨论】:

  • 哪里有问题的代码?
  • (1) 请发布您正在使用的实际代码;明确地理解比叙述要容易得多。 (2) 这可能比 Stack Overflow 更适合 Code Review。 (3) 虽然你可能能够做出重大改进,但从 N^2 到 N^2/2 是一个线性因素,所以你仍然是 O(N^2)。
  • 问题对我来说似乎很清楚,他要求的是比蛮力更好的算法
  • 蛮力的代码很简单:for循环,里面有另外2个,一个到右,一个到左并计数。就这样。是的,我问是否有办法改善 O(N^2)。 @宗政李能详细点吗?哈希集可以让我提防重复计数,但我不知道如何提高整体性能

标签: java arrays algorithm optimization data-structures


【解决方案1】:

这是O(n log n) 解决方案。

正如@SayonjiNakate 所暗示的,使用段树的解决方案(我在实现中使用了Fenwick 树)在O(n log M) 时间运行,其中M 是数组中可能的最大值。

首先,注意“左边较小元素的个数”的问题等价于“右边的较大元素个数”的问题,通过反转和取反数组。所以,在我下面的解释中,我只描述了“左侧较小元素的数量”,我称之为“lesser_left_count”。

算法 for lesser_left_count:

这个想法是能够找到小于特定数字的数字的总和。

  1. 定义一个数组tree,大小最大为MAX_VALUE,它将存储值1 用于查看数字,否则存储0

  2. 然后当我们遍历数组时,当我们看到一个数字num 时,只需将值1 分配给tree[num]更新操作)。然后,对于数字 num 的 lesser_left_count 是从 1num-1 的总和(求和操作),因为当前位置左侧的所有较小数字都将设置为 @987654335 @。

简单吧?如果我们使用Fenwick tree,则更新和求和操作可以在O(log M) 时间内完成,其中M 是数组中可能的最大值。由于我们正在遍历数组,因此总时间为O(n log M)

幼稚解决方案的唯一缺点是它使用大量内存,因为M 变得更大(我在我的代码中设置了M=2^20-1,这需要大约 4MB 的内存)。这可以通过将数组中的不同整数映射为更小的整数来改进(以保持顺序的方式)。通过对数组进行排序,可以简单地在O(n log n) 中完成映射。因此数字M 可以重新解释为“数组中不同元素的数量”

所以内存不会再有任何问题了,因为如果经过这次改进之后你确实需要巨大的内存,那意味着你的数组中有那么多不同的数字,以及@的时间复杂度无论如何,987654343@ 已经太高,无法在普通机器上计算。

为了简单起见,我没有在我的代码中包含这种改进。

哦,由于 Fenwick 树仅适用于正数,因此我将数组中的数字转换为最小 1。请注意,这不会改变结果。

Python 代码:

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()

将产生:

原始数组:[1, 1, 3, 2, 4, 5, 6] 左数较少:[0, 0, 1, 1, 3, 4, 5] 大右 cnt:[5, 5, 3, 3, 2, 1, 0]

或者如果你想要 Java 代码:

import java.util.Arrays;

class Main{
    static int MAX_VALUE = 1048575;
    static int[] fArr = new int[MAX_VALUE];

    public static void main(String[] args){
        int[] arr = new int[]{1,1,3,2,4,5,6};
        System.out.println("Original array:    "+toString(arr));
        int[][] leftRight = lesserLeftRight(arr);
        System.out.println("Lesser left count: "+toString(leftRight[0]));
        System.out.println("Greater right cnt: "+toString(leftRight[1]));
    }

    public static String toString(int[] arr){
        String result = "[";
        for(int num: arr){
            if(result.length()!=1){
                result+=", ";
            }
            result+=num;
        }
        result+="]";
        return result;
    }

    public static void reset(){
        Arrays.fill(fArr,0);
    }

    public static void update(int idx, int val){
        while(idx < MAX_VALUE){
            fArr[idx]+=val;
            idx += (idx & -idx);
        }
    }

    public static int cntSum(int idx){
        int result = 0;
        while(idx > 0){
            result += fArr[idx];
            idx -= (idx & -idx);
        }
        return result;
    }

    public static int[] lesserLeftCount(int[] arr){
        reset();
        int[] result = new int[arr.length];
        for(int i=0; i<arr.length; i++){
            result[i] = cntSum(arr[i]-1);
            if(cntSum(arr[i])==result[i]) update(arr[i],1);
        }
        return result;
    }

    public static int[][] lesserLeftRight(int[] arr){
        int[] left = new int[arr.length];
        int min = Integer.MAX_VALUE;
        for(int i=0; i<arr.length; i++){
            left[i] = arr[i];
            if(min>arr[i]) min=arr[i];
        }
        for(int i=0; i<arr.length; i++) left[i]+=min+1;
        left = lesserLeftCount(left);

        int[] right = new int[arr.length];
        int max = Integer.MIN_VALUE;
        for(int i=0; i<arr.length; i++){
            right[i] = arr[arr.length-1-i];
            if(max<right[i]) max=right[i];
        }
        for(int i=0; i<arr.length; i++) right[i] = max+1-right[i];
        right = lesserLeftCount(right);
        int[] rightFinal = new int[right.length];
        for(int i=0; i<right.length; i++) rightFinal[i] = right[right.length-1-i];
        return new int[][]{left, rightFinal};
    }
}

这将产生相同的结果。

【讨论】:

  • 这是一个很好的解决方案,尽管如果您还假设 n &lt; M 或者更确切地说,如果您假设 n 也是常数,那么您不能争辩说 M 是一个常数你的算法变成O(1)。所以我要么争辩M 是不变的,我们正在计划巨大的n,或者用nM 表达复杂性。无论哪种方式,都做得很好。
  • @ErikP.,7 年后,我更新了我的答案以删除该误导性陈述。
【解决方案2】:

尝试用于解决 RMQ 的段树数据结构。 它会给你准确的 n log n。

一般看RMQ problem,你的问题可能会归结为它。

【讨论】:

  • 想开导?我没有看到这个问题如何减少到 RMQ,也不清楚如何使用分段树来计算 x[0..i) 中小于 x[i] 的不同值的数量。
  • 在我的回答中有详细说明,它使用 Fenwick Tree 代替段树(两者是等效的)
【解决方案3】:

这是一个相对简单的解决方案 O(N lg(N)),它不依赖于有限多个整数中的条目(特别是它应该适用于任何有序数据类型)。

我们假设输出存储在两个数组中; lowleft[i] 将在末尾包含 x[j]j &lt; ix[j] &lt; x[i] 的不同值的数量,而highright[i] 将在末尾包含不同值的数量 x[j]j &gt; ix[j] &gt; x[i] .

创建一个平衡的树数据结构,在每个节点中维护以该节点为根的子树中的节点数。这是相当标准的,但不是我认为的 Java 标准库的一部分;做一个AVL树可能是最容易的。节点中值的类型应该是数组中值的类型。

现在首先遍历数组forward。我们从一棵空的平衡树开始。对于我们遇到的每个值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]。因此,总复杂度O(N lg(N))

【讨论】:

  • 是的,这将是一个很好的解决方案,但如果我理解正确,就会出现问题:对于数组 1 5 4 3 6 5 9 10,前 5 个有 6 作为更大的数字,但是第二个5没有。因此,对于第二个 5,我们会得到一个不符合要求的 +1 值。我认为。谢谢
  • @user1812632 对于highright 条目,您向后 遍历数组(在第二遍中)。所以一开始你遇到最右边的 5 并看到你的树中有一个更大的数字(9),后来你遇到最左边的 5 并看到 6 和 9 是你的树中更大的数字。跨度>
【解决方案4】:

这是一个应该给你O(NlgN)的算法:

  1. 遍历列表一次并构建key =&gt; indexList 的映射。因此,对于任何键(数组中的元素),您都存储了该键在数组中的所有索引的列表。这将采取O(N)(遍历列表)+N*O(1)(将N 个项目附加到列表)步骤。所以这一步是O(N)。第二步要求对这些列表进行排序,因为我们从左到右遍历列表,因此列表中新插入的索引将始终大于所有其他已在列表中的索引。

  2. 再次遍历列表,并为每个元素在索引列表中搜索大于当前元素的所有键,以查找当前索引之后的第一个索引。这为您提供了当前元素右侧大于当前元素的元素数量。随着索引列表的排序,您可以进行二进制搜索,这将采取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,以防有人感兴趣;

【讨论】:

  • 我不明白你在这里做什么。您的第一部分(从伪代码中可以看出)是将 one 元素放入键映射到的每个数组中
  • 不,它记录了数组中每个键所在的所有索引。
  • 不,我的意思是k 可以很大并不重要。就 big-O 而言,它是一个常数,因为它不依赖于数组中元素的数量。
  • 如果你将一个元素插入到一个排序列表中,它需要O(N)时间,所以你建议的是O(N^2)。这可以通过使用平衡树来修复 - 然后你会得到O(N lg(N))(固定k)。
  • @ErikP。嗯,实际上列表会自动排序,因为如果我们从左到右循环,索引总是会增加。所以没有什么特别的要求。附加到列表的是O(1)
猜你喜欢
  • 2019-05-15
  • 2023-03-31
  • 2022-01-05
  • 1970-01-01
  • 1970-01-01
  • 2018-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多