Амортизируемая сложность std :: next_permutation? - PullRequest
19 голосов
/ 11 февраля 2011

Я только что прочитал этот другой вопрос о сложности next_permutation , и хотя я удовлетворен ответом (O (n)), кажется, что алгоритм может иметь хороший амортизированный анализ, который показывает меньшая сложность. Кто-нибудь знает такой анализ?

Ответы [ 3 ]

16 голосов
/ 11 февраля 2011

Похоже, что я собираюсь ответить на свой собственный вопрос утвердительно - да , next_permutation работает за O (1) амортизированное время.

Прежде чем приступить к формальному доказательству этого, рассмотрим, как работает алгоритм. Во-первых, он сканирует назад от конца диапазона к началу, идентифицируя самую длинную непрерывную убывающую подпоследовательность в диапазоне, который заканчивается на последнем элементе. Например, в 0 3 4 2 1 алгоритм идентифицирует 4 2 1 как эту подпоследовательность. Затем он смотрит на элемент непосредственно перед этой подпоследовательностью (в вышеприведенном примере 3), а затем находит наименьший элемент в подпоследовательности больше его (в вышеприведенном примере 4). Затем обменивается позициями этих двух элементов, а затем переворачивает идентифицированную последовательность. Итак, если бы мы начали с 0 3 4 2 1, мы бы поменяли местами 3 и 4, чтобы получить 0 4 3 2 1, а затем обратили бы последние три элемента, чтобы получить 0 4 1 2 3.

Чтобы показать, что этот алгоритм работает в амортизированной форме O (1), мы будем использовать потенциальный метод. Определить & Phi; быть в три раза длиннее самой длинной непрерывно убывающей подпоследовательности в конце последовательности. В этом анализе мы будем предполагать, что все элементы различны. Учитывая это, давайте подумаем о времени выполнения этого алгоритма. Предположим, что мы сканируем в обратном направлении от конца последовательности и находим, что последние m элементов являются частью убывающей последовательности. Это требует сравнения m + 1. Далее, мы находим, из элементов этой последовательности, какой из них наименьший больше, чем элемент, предшествующий этой последовательности. В худшем случае это занимает время, пропорциональное длине убывающей последовательности с использованием линейного сканирования для других m сравнений. Обмен элементов занимает, скажем, 1 кредитное время, а для изменения последовательности требуется не более m операций. Таким образом, реальное время выполнения этого шага составляет примерно 3 м + 1. Однако мы должны учитывать изменение потенциала. После того, как мы изменим эту последовательность длины m, мы в конечном итоге уменьшим длину самой длинной убывающей последовательности в конце диапазона до длины 1, потому что обратная последовательность уменьшения в конце делает последние элементы диапазона отсортированными в порядке возрастания , Это означает, что наш потенциал изменился с & Phi; = 3 м до & Phi; ' = 3 * 1 = 3. Следовательно, чистое падение потенциала составляет 3–3 м, поэтому наше амортизированное время составляет 3 м + 1 + (3–3 м) = 4 = O (1).

В предыдущем анализе я сделал упрощающее предположение, что все значения являются уникальными. Насколько мне известно, это предположение необходимо для того, чтобы это доказательство сработало. Я собираюсь обдумать это и посмотреть, можно ли изменить доказательство, чтобы оно работало в случае, когда элементы могут содержать дубликаты, и я опубликую правку этого ответа, как только проработаю детали.

14 голосов
/ 11 февраля 2011

Я не совсем уверен в точной реализации std :: next_permutation, но если он совпадает с алгоритмом Нараяны Пандиты, как описано в вики здесь: http://en.wikipedia.org/wiki/Permutation#Systematic_generation_of_all_permutations,

при условии, что элементы различны,похоже, это О (1) амортизируется!(Конечно, в приведенном ниже тексте могут быть ошибки)

Давайте посчитаем общее количество выполненных обменов.

Получим рекуррентное соотношение

T (n + 1) = (n + 1) T (n) + Θ (n 2 )

(n + 1) T (n) происходит от фиксации первого элементаи выполнение перестановок для оставшихся n.

Θ (n 2 ) происходит от изменения первого элемента.В момент, когда мы меняем первый элемент, мы делаем Θ (n) перестановок.Сделав это n раз, вы получите Θ (n 2 ).

Теперь позвольте X(n) = T(n)/n!

Тогда мы получим

X (n + 1) = X (n) + Θ (n 2 ) / (n + 1)!

т.е. существует некоторая постоянная C, такая что

X (n + 1) <= X (n) + Cn <sup>2 / (n + 1)!

Запись n таких неравенств дает нам

X (n + 1) - X (n) <= Cn <sup>2 / (n + 1)!

X (n) - X (n-1) <= C (n-1) <sup>2 / (n)!

X (n-1) - X (n-2) <= C (n-2) <sup>2 / (n-1)!

...

X (2) - X (1) <= C1 <sup>2 / (1+1)!

Добавление этих значений дает нам X(n+1) - X(1) <= C(\sum j = 1 to n (j^2)/(j+1)!).

Поскольку бесконечный ряд \sum j = 1 to infinity j^2/(j+1)! сходится к C ', скажем, мы получаем X(n+1) - X(1) <= CC'

Помните, что X (n) подсчитывает среднее количество необходимых свопов (T (n) /n!)

Таким образом, среднее число свопов равно O (1).

Поскольку нахождение элементов для свопинга линейно с количеством свопов, оно амортизируется за O (1), даже еслиВы принимаете во внимание другие операции.

0 голосов
/ 11 февраля 2011

Здесь n обозначает количество элементов в контейнере, а не общее количество возможных перестановок.Алгоритм должен перебирать порядок всех элементов при каждом вызове;для этого требуется пара двунаправленных итераторов, что подразумевает, что для получения одного элемента алгоритм должен сначала посетить один перед ним (если только это не первый или последний элемент).Двунаправленный итератор позволяет выполнять итерацию в обратном направлении, поэтому алгоритм может (фактически должен) выполнять вдвое меньше операций свопинга, чем элементов.Я полагаю, что стандарт мог бы предложить перегрузку для прямого итератора, который поддерживал бы более тупые итераторы за счет n свопов, а не половины n свопов.Но, увы, это не так.

Конечно, для n возможных перестановок алгоритм работает в O (1).

...