Почему временная сложность алгоритма перестановок равна n*n! и не n^3 * n!?

#algorithm #time-complexity #permutation

Вопрос:

Автор утверждает, что временная сложность этого алгоритма перестановки:

 from collections import deque

def find_permutations(nums):
    
    nums_l = len(nums)
    perms = deque()
    perms.append([])
    
    for num in nums:                     # (1)
        for _ in range(len(perms)):      # (2)
            perm = perms.popleft() 
            for j in range(len(perm) 1): # (3)
                new_perm = list(perm)    # (4)
                new_perm.insert(j, num)  # (5)
                perms.append(new_perm)   # (6)
    
    return perms
 

является n*n!

Но я не понимаю, почему. Вот мое вычисление временной сложности для каждой строки кода:

 (1): len(nums) ~ n
(2): len(perms) ~ n!
(3): len(perm) ~ n
(4): n
(5): n
(6): n
 

Таким образом, общая временная сложность составляет:

 n * n! * n * (n   n   n) ~ n^3 * n!
 

Или я ошибаюсь?

Ответ №1:

Прежде чем перейти к основной проблеме, на шаге (6) обнаружена незначительная ошибка: append это O(1). Но это не влияет на общую сложность.

Ваш анализ идет неправильно на шаге (2):

 for _ in range(len(perms)):
 

Это не выполняет O(n!) итераций. В первый раз он даже выполняет только 1 итерацию, во второй раз все еще 1 итерацию, а в третий раз 2 итерации, затем 2 * 3, затем 2 * 3 * 4, … и в последний раз (n-1)! итерации.

Затем на шаге (3)также происходит переоценка:

 for j in range(len(perm) 1)
 

Размер perm изначально невелик (соответствует циклу шага 2): сначала перестановки имеют размер 0, затем на следующей итерации внешнего цикла они имеют размер 1, … затем 2, … до n-1.

Таким образом, для общего числа итераций внутреннего цикла мы имеем эту сумму:

1 * 1 1 * 2 2 * 3 (2 * 3) * 4 … кей! … н!

Более простой способ взглянуть на это-понять, что на каждой итерации внешнего цикла этот код создает все перестановки, которые на один элемент длиннее, чем на предыдущей итерации. Таким образом, на первой итерации вы получаете все перестановки размера 1 (используя первое входное значение), на второй итерации вы получаете все перестановки размера 2 (используя два первых входных значения) и т. Д. Это подход «снизу вверх». Также обратите внимание, что popLeft выполняется нужное количество раз, чтобы удалить все перестановки предыдущей (внешней) итерации, в то время append как выполняется для всех новых перестановок (которые появляются только в следующей внешней итерации).

Таким образом, теперь мы можем легко видеть, что append выполняется это количество раз:

1! 2! 3! 4! … n! = O(n!)

Если мы теперь включим в это сложность шага 4 (который «поглощает» сложность шага 5), то получим:

1.1! 2.2! 3.3! 4.4! … n.n! = O(n.n!)

Комментарии:

1. Как насчет сложности шага 1? Это также должно быть O(n). Таким образом, похоже, что общая сложность будет O(n.n.n!), верно? Спасибо.

2. Нет, шаг 1 включен, когда я написал «для общего числа итераций внутреннего цикла» — я обновил фразу, чтобы указать, что учитывается внешний цикл.

3. Похоже, я наконец-то это понял. Если мы сравним алгоритм перестановки с чем-то с временной сложностью n^2, например [i для i в диапазоне(n) для j в диапазоне(n)], Разница в том, что для алгоритма перестановки у нас разное количество исключений на каждом шаге, поэтому временная сложность будет просто сложностью последнего шага (n!). Но для алгоритма n^2 у нас есть n исполнений для каждого внешнего цикла, например, для n =5: O(5 5 5 5 5) = O(5*5) или O(n^2). Чтобы сравнить его с алгоритмом перестановок, у нас есть временная сложность O(1! 2! 3! 4! …) и конечная сложность будет просто сложностью последнего шага (n!)

Ответ №2:

Этот алгоритм генерирует перестановки, вставляя число во все возможные позиции существующих перестановок

теперь первый цикл четко O(n)

давайте посмотрим, что происходит с другими циклами:

на последней итерации (в худшем случае):

  1. просмотр всех (n-1)! перестановок (сгенерированных с помощью предыдущих итераций) — O((n-1)!)
  2. копирование каждого из них и добавление нового номера внутри O(n) каждого

в общей (n-1)! * (n) = n! сложности это для наихудшего случая других циклов

так что O(n * n!) в целом

Комментарии:

1. разве это не O(n * n!) просто O(n!) в Большом О?