Определенный массив и ключ, найдите количество элементов, меньшее или равное самому себе в его левом и правом подвале?
Мне нужно найти число элементов, меньшее или равное 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