Какова временная сложность Ruby, встроенная в методы #permutation и #repeated_permutation?

Мне было интересно узнать о временной сложности некоторых встроенных методов Ruby, в частности этих двух. Я думаю, что лучшее, что я смог придумать для метода перестановки самостоятельно, - это Θ (n · n!), Работает ли Ruby? Если да, пожалуйста, помогите мне понять их алгоритм.

Ответ 1

Перестановка

Array#permutation возвращает Enumerator с массивами n!, поэтому временная сложность будет не менее O(n!).

Я написал этот метод:

def slow_method(n)
  (1..n).to_a.permutation.each do |p|
    p
  end
end

Он ничего не делает с p, ожидая принудительного генерации всех перестановок. Построение массива всех перестановок будет использовать слишком много памяти.

Этот метод был вызван 10 раз для n между 10 и 13, а среднее время в секундах:

t10 = 0.618895
t11 = 6.7815425
t12 = 82.896605
t13 = 1073.015602

O(n!) выглядит как разумное приближение:

t13/fact(13)*fact(12)/t12 #=> 0.995694114280165
t13/fact(13)*fact(11)/t11 #=> 1.0142685297667369
t13/fact(13)*fact(10)/t10 #=> 1.0103498450722133

O(n*n!) не:

t13/(fact(13)*13)*fact(12)*12/t12 #=> 0.9191022593355369
t13/(fact(13)*13)*fact(11)*11/t11 #=> 0.8582272174949312
t13/(fact(13)*13)*fact(10)*10/t10 #=> 0.777192188517087

Похоже, что генерация O(n!), но выполнение чего-либо с созданными массивами приведет к общей сложности O(n*n!).

Почему не поколение O(n*n!)? Это может исходить из того, что при рекурсивном генерации [1,2,3,4,5].permutation остальные перестановки одинаковы для [1,2] и [2,1].

O(n!) уже настолько медленен, что n никогда не будет намного больше 10, поэтому O(n*n!) не намного хуже. Для n=20, n! есть 2432902008176640000 и n*n! is 48658040163532800000.

Повторная перестановка

[1,2,...n].repeated_permutation(k) генерирует n**k Массивы из k элементов.

Сложность должна быть либо O(n**k), либо O(k*n**k).

Для k=n он становится O(n**n) или O(n**(n+1)), что даже (намного) хуже, чем для permutation.

Ответ 2

Существуют алгоритмы для итеративного создания всех перестановок списка.

Как? Алгоритм генерирует все перестановки [1,2,3,4,...,n] в лексикографическом порядке. При одной перестановке алгоритм генерирует следующую лексикографическую перестановку в O(1) времени.

Это шаги

  • Найти наибольший индекс k такой, что a[k] < a[k + 1], если такой индекс не существует, перестановка является последней перестановкой
  • Найти наибольший индекс l больше k, чтобы a[k] < a[l]
  • Поменяйте значение a[k] на значение a[l]
  • Отмените последовательность от a[k + 1] до конечного элемента и включите его a[n]

В чем сложность?

Каждый шаг O(1) и существуют перестановки O(n!), поэтому общая сложность O(n!).