【问题标题】:Given array and key, find the number of elements smaller than or equal to itself in its left and right sub array?给定数组和键,找出其左右子数组中小于或等于自身的元素个数?
【发布时间】:2015-03-03 18:15:13
【问题描述】:

我要在左右子数组中找到小于等于i-th元素的数组元素个数。

例如,如果我的数组是

A[]=4 3 5 2 2 5

我的 2 个数组将是

0 0 2 0 0 5

还有

3 2 3 1 1 0

第一个数组的i-th 元素表示i-th 左侧的小于或等于i-th 元素的数量。

第二个数组的i-th 元素表示i-th 右侧的小于或等于i-th 元素的数量。

我可以使用两个循环在 O(n2) 中找到这些数组。

这可以在O(n)中完成吗?

【问题讨论】:

  • 第一个数组不应该是0 0 2 0 1 5,因为倒数第二个2等于它前面的2吗?
  • @templatetypedef 和第二个数组 3 2 3 1 0 0 因为最右边的 2 没有相等或更小的后继。
  • Can This be done in O(n)? No. (IIUC) 可以在O(n log m) 中完成,其中 m 是数组中不同值的数量。 -->> 基数排序
  • @wildplasser radix-sort 是一种排序算法,但 OP 的问题并不直接与排序有关。如何在 O(n log m) 中从排序数组到 OP 想要的数组?
  • 是一样的。查看答案。

标签: algorithm language-agnostic


【解决方案1】:

您可以使用Fenwick Tree 在 O(nlogm) 中执行此操作(其中 n 是 A 的长度,m 是数组中最大元素的大小)。

Fenwick 树(也称为二叉索引树)允许您将元素添加到数组并在 O(logn) 时间内计算连续元素的总和。有很好的教程on topcoder。

在这个问题中,我们可以使用 Fenwick 树来存储我们看到每个值的次数的直方图。直方图开始是空的,然后我们逐渐向直方图中插入元素。

所以我们需要遍历数组,每次先计算有多少元素的值小于当前值,然后将当前值加到数组中。

Python 代码:

def find_lower(A):
    """Return B where B[i] is the number of elements <= A[i] in A[0:i]"""
    top = max(A) + 1
    F = fenwick_new(top)
    left = []
    for a in A:
        left.append( fenwick_sum(F,a) )
        fenwick_increase(F,a,1)
    return left

A=[4, 3, 5, 2, 2, 5]
print find_lower(A)
print find_lower(A[::-1])[::-1]

这使用了一些标准的 Fenwick 树函数:

def fenwick_new(m):
    """Create empty fenwick tree with space for elements in range 0..m"""
    # tree[i] is sum of elements with indexes i&(i+1)..i inclusive
    return [0] * (m+1)

def fenwick_increase(tree,i,delta):
    """Increase value of i-th element in tree by delta"""
    while i < len(tree):
        tree[i] += delta
        i |= i + 1

def fenwick_sum(tree,i):
    """Return sum of elements 0..i inclusive in tree"""
    s = 0
    while i >= 0:
        s += tree[i]
        i &= i + 1
        i -= 1
    return s

【讨论】:

  • 这真是天才!我不明白i &amp;= i + 1 和i |= i + 1 的含义是什么,但它似乎有效。我一定会阅读芬威克树。谢谢。
  • 最佳解决方案.. :)
【解决方案2】:

不,不可能在O(n) 时间内完成。你能做的最好的就是O(n log n)。

这是证据。假设原始数组有n distinct 元素。让我们计算一下您想要的第一个数组有多少种可能性(另一个的证明类似)。第一个数字有 1 种可能性 (0)。第二个数字有 2 种可能性(0 或 1)。第三个数字有 3 种可能性(0、1 或 2),依此类推。因此,最多有1 * 2 * 3 * ... * n = n! 可能的答案。事实上很容易看出,对于这些可能的答案中的每一个,至少有一个不同的数字数组产生它(从左到右工作。如果answer[i] 是0,则将original[i] 设置为小于所有先前选择的数字。如果answer[i] 是i,则将original[i] 设置为大于所有先前选择的数字。否则,将original[i] 设置为介于i-th 最小和(i+1)-已经选择的最小数字)。任何算法都必须确定n! 可能的答案中哪一个是正确的。如果我们总是可以用f(n) 比较来完成算法,那么就可以得出2^f(n) &gt;= n!(每个比较只有两个可能的结果,因为原始数字都是不同的)。取双方的日志(以 2 为基数),我们得到f(n) &gt;= log(n!)。使用斯特林对log(n!) 的近似,我们发现我们不能比O(n log n) 做得更好。

可以在O(n log n) 时间完成。以下 Java 方法(不在O(n log n) 时间运行)返回您需要的第一个数组(第二个可以类似地完成)。 Arrays.sort() 使用 TimSort,即 O(n log n)。但是,为了使整个方法在O(n log n) 时间内运行,您必须将ArrayList 替换为List 的实现,其中add(Object object)、remove(int index) 和lastIndexOf(Object object) 方法都在O(log n) 中运行时间。根据http://www.nayuki.io/page/avl-tree-list,AvlTreeList 在O(log n) 时间执行add() 和remove(),并且可以“增加”AvlTreeList,以便搜索也是O(log n)。这证明你的数组可以在O(n log n)时间找到。

public static int[] firstArray(int[] arr) {
    int[] copy = arr.clone();
    Arrays.sort(copy);
    List<Integer> list = new ArrayList<>();
    for (int a : copy)
        list.add(a);
    int length = arr.length;        
    int[] temp = new int[length];
    for (int i = length - 1; i >= 0; i--) {
        int j = list.lastIndexOf(arr[i]);
        temp[i] = j;
        list.remove(j);
    }
    return temp;
}

【讨论】:

  • 你可能对我的新答案感兴趣
  • @randomusername 谢谢。明天我有一个考虑。我需要了解所有这些树的东西(这对我来说都是全新的)所以可能需要一段时间才能知道它的全部内容。
  • 为什么我们只限于比较?数组索引可以在一个步骤中为我们提供 log(n) 位信息。
  • @tmyklebu 如果您对数组中的数字有所了解,可以进行简化。例如。如果您知道数组仅由 1、2 和 3 组成,则可以获得 O(N) 性能。如果您对数字一无所知,我的回答证明,没有像数组索引这样的技巧可能比 O(n log n) 更好,因为问题最终必须归结为比较。
  • 为什么这个问题“最终归结为比较”?您的回答根本没有解决这个问题。
【解决方案3】:

我将使用称为具有秩字段的平衡二叉树(参见 Knuth 6.2.3,线性列表表示)来简化 @pbabcdefp 的答案。考虑一棵平衡树(实现无关紧要,因此红黑或 AVL 都可以)但我们有一个名为 rank 的附加字段,它将存储 左子树的大小 ,数字将以最小到最大的顺序插入到我们的树中,每个受影响节点的排名字段在每次插入/旋转后更新。我们将在平衡树中允许重复元素。

算法 A:
  1. 将head 设置为根节点。将v 设置为我们正在搜索的值。让i 成为v 列表中的索引,这将是我们的返回值。
  2. 如果head 为null/empty,则成功返回索引i。
  3. 如果v &lt; head.value,则head &lt;- head.left,否则i &lt;- i + head.rank + 1 和head &lt;- head.right。
  4. 回到 2。
原始算法:

对于数组中的每个元素:使用上述算法 A 找到小于或等于它的元素数(在树中,因此在当前元素之前),然后使用修改插入。这给出了第一个数组。再重复一次,这次是向后而不是向前遍历数组,以获得第二个数组。

【讨论】:

    【解决方案4】:

    看来你不能跳过 nlogn 所以基本版本只是:

    def naive(arr)
      res = Array.new(arr.size,0)
      for i in 0..arr.length-1
        for j in i+1..arr.length-1
          res[i] += 1 if arr[i] >= arr[j]
        end
      end
      res
    end
    

    但是,根据您的数据集,您可以进行一些优化。例如,在您的示例中存在重复 - 我们可以使用该事实来避免两次遍历相同数字的数组。在这种情况下,而不是搜索小于当前的数字 - 您可以对大于当前的每个数字(从右到左)进行加法并忽略已经使用的数字:

    def rightleft(arr)
      ignore = {}
      res = Array.new(arr.size,0)
      (arr.size-1).downto(0) do |i|
        current = arr[i]
        next if ignore[current]
        additor = 1
        (i-1).downto(0) do |j|
          if arr[i] <= arr[j]
            res[j] += additor
              if arr[i] == arr[j]
              additor += 1
              ignore[current] = true
            end
          end
        end
      end
      res
    end
    

    让我们举一些重复的例子:

    A = %W{1 2 2 3 3 5 4 6 5 7 8 9 10 11 12 14 5 6 4 3 1 7 8 9 3 5 4 2 2 3 3 5 4 6 5 7 8 9 10 11 12 14 5 6 4 3 1 7 8 9 3 5 4 6 5 7 8 9 10 11 12 14 5 6 4 3 1 10 2 11 12 4 13 1 2 2 3 3 5 4 12 14 5 6 4 3 1 7 8 9 3 5 4 6 5 7 8 9 10 11 12 14 5 6 4 3 1 10 2 11 12 4 13 1 2 2 3 3 5 4 6 5 7 8 9 10 11 12 14 6 5 7 8 9 10 11 12 14 5 6 4 3 1 10 2 11 12 4 13 1 2 2 3 3 5 4 12 14 5 6 4 3 1 7 8 9 3 5 4 6 5 7 8 9 10 11 12 14 5 6 4 3 1 10 2 11 12 4 13 1 2 2 3 3 5 4 6 5 7 8 9 10 11 12 14 5 6 4 3 1 7 8 9 1 10 2 11 12 4 13 14 6 15 12 16 17 18 19}.map(&:to_i)
    

    现在基准测试

    puts "NAIVE 100 times:"
    puts time_elapsed{
      100.times do
        naive(A)
      end
    }
    
    puts "rl 100 times"
    puts time_elapsed{
      100.times do
        rightleft(A)
      end
    }
    

    结果:

    NAIVE 100 times:
    [14, 30, 29, 53,...., 0, 0]
    Time elapsed 485.935997 milliseconds
    
    rl 100 times
    [14, 30, 29, 53,...., 0, 0]
    Time elapsed 81.735048 milliseconds
    

    但是当你没有重复时 - 这种优化会使它稍微变慢。以下是数字 1,2,3...,99,100 的结果:

    NAIVE 100 times:
    [70, 7,... 1, 0]
    Time elapsed 99.58762899999999 milliseconds
    
    rl 100 times
    [70, 7, ... 1, 0]
    Time elapsed 113.186392 milliseconds
    

    【讨论】:

      猜你喜欢
      • 2019-11-06
      • 2022-10-12
      • 1970-01-01
      • 1970-01-01
      • 2013-07-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多