Определенный массив и ключ, найдите количество элементов, меньшее или равное самому себе в его левом и правом подвале?

Мне нужно найти число элементов, меньшее или равное i -й элементу массива в левом и правом подмассивах.

Например, если мой массив

A[]=4 3 5 2 2 5

Мои 2 массива будут

0 0 2 0 0 5

И

3 2 3 1 1 0

i -й элемент 1-го массива обозначает количество элементов, меньших или равных i -й элементу слева от i -го элемента.

i -й элемент 2-го массива обозначает число элементов, меньших или равных i -th элементу справа от i -го элемента.

Я могу найти эти массивы в O (n 2), используя две петли.

Можно ли это сделать в O (n)?

Ответы

Ответ 1

Вы можете сделать это в O (nlogm) (где n - длина A, а m - размер самого большого элемента в массиве) с помощью Fenwick Tree.

Дерево Fenwick (также называемое двоичным индексированным деревом) позволяет добавлять элементы в массив и вычислять суммы последовательных элементов в O (logn) времени. В topcoder есть хороший учебник .

В этой задаче мы можем использовать дерево Фенвика для хранения гистограммы того, сколько раз мы видели каждое значение. Гистограмма начинается пустым, а затем постепенно вставляем элементы в гистограмму.

Итак, нам нужно итерации через массив, каждый раз сначала вычисляя, сколько элементов имеет значение меньше текущего значения, а затем добавляет текущее значение в массив.

Код 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]

Здесь используются некоторые стандартные функции дерева Фенвика:

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

Ответ 2

Нет, это невозможно сделать в O(n). Лучшее, что вы можете сделать, это O(n log n).

Вот доказательство. Предположим, что исходный массив имеет n различные элементы. Дайте понять, сколько возможностей есть для первого массива, который вам нужен (доказательство для другого похоже). Существует первая возможность для первого числа (0). Есть 2 возможности для второго номера (0 или 1). Есть 3 возможности для 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) -th наименьшее число уже выбрано). Любой алгоритм должен определить, какой из возможных ответов n! правильный. Если мы всегда можем закончить алгоритм с помощью сравнений f(n), то 2^f(n) >= n! (каждое сравнение имеет только 2 возможных результата, поскольку исходные числа были разными). Принимая бревна (основание 2) с обеих сторон, получим f(n) >= 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 имеет время add() и remove() в O(log n), и возможно "увеличить" a 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;
}

Ответ 3

Я упрощу ответ @pbabcdefp, используя что-то, называемое сбалансированным двоичным деревом с полем ранга (см. Knuth 6.2.3, Линейное представление списка). Рассмотрим сбалансированное дерево (реализация не имеет значения, так что либо красно-черный, либо AVL будет делать все), но у нас есть дополнительное поле, называемое rank, которое будет хранить размер левого поддерева, цифры будут вставлены в нашу дерево в минимальном и наибольшем порядке, поле ранга для каждого затронутого node обновляется после каждой вставки/вращения. Мы разрешим дублировать элементы в нашем сбалансированном дереве.

Алгоритм A:
  • Установите head в корневой каталог node. Задайте v значение, которое мы ищем. Пусть i - индекс в списке для v, это будет наше возвращаемое значение.
  • Если head равно null/пусто, вернитесь успешно с индексом i.
  • Если v < head.value, тогда head <- head.left, в противном случае i <- i + head.rank + 1 и head <- head.right.
  • Вернитесь к 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 shuffled:

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

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