Std:: next_permutation Объяснение реализации

Мне было любопытно, как был реализован std:next_permutation, поэтому я извлек версию gnu libstdc++ 4.7 и дезинфицировал идентификаторы и форматирование, чтобы создать следующую демонстрационную версию...

#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

template<typename It>
bool next_permutation(It begin, It end)
{
        if (begin == end)
                return false;

        It i = begin;
        ++i;
        if (i == end)
                return false;

        i = end;
        --i;

        while (true)
        {
                It j = i;
                --i;

                if (*i < *j)
                {
                        It k = end;

                        while (!(*i < *--k))
                                /* pass */;

                        iter_swap(i, k);
                        reverse(j, end);
                        return true;
                }

                if (i == begin)
                {
                        reverse(begin, end);
                        return false;
                }
        }
}

int main()
{
        vector<int> v = { 1, 2, 3, 4 };

        do
        {
                for (int i = 0; i < 4; i++)
                {
                        cout << v[i] << " ";
                }
                cout << endl;
        }
        while (::next_permutation(v.begin(), v.end()));
}

Вывод выполняется так, как ожидалось: http://ideone.com/4nZdx

Мои вопросы: как это работает? В чем смысл i, j и k? Какую ценность они играют в разных частях исполнения? Что такое эскиз доказательства его правильности?

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

Что происходит в теле основного цикла?

Ответ 1

Посмотрим на некоторые перестановки:

1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
2 1 3 4
...

Как мы переходим от одной перестановки к следующей? Во-первых, давайте посмотрим на вещи немного по-другому. Мы можем рассматривать элементы как цифры и перестановки как числа. Рассматривая проблему таким образом , мы хотим заказать перестановки/числа в порядке возрастания.

Когда мы заказываем номера, мы хотим "увеличить их на минимальную сумму". Например, при подсчете мы не учитываем 1, 2, 3, 10,... потому что есть еще 4, 5,... между ними и хотя 10 больше 3, отсутствуют числа, которые могут быть получены увеличивая 3 на меньшую величину. В приведенном выше примере мы видим, что 1 остается как первое число в течение длительного времени, так как существует много переупорядочений последних 3 "цифр", которые "увеличивают" перестановку на меньшее количество.

Итак, когда мы наконец "используем" 1? Когда нет только перестановок из последних трех цифр.
И когда нет больше перестановок из последних трех цифр? Когда последние 3 цифры находятся в порядке убывания.

Ага! Это ключ к пониманию алгоритма. Мы только меняем положение "цифры", когда все справа находится в порядке убывания, потому что, если оно не находится в порядке убывания, есть еще больше перестановок (т.е. мы можем "увеличить", перестановка на меньшее количество).

Теперь вернемся к коду:

while (true)
{
    It j = i;
    --i;

    if (*i < *j)
    { // ...
    }

    if (i == begin)
    { // ...
    }
}

Из первых двух строк в цикле j - это элемент, а i - это элемент перед ним.
Затем, если элементы находятся в порядке возрастания, (if (*i < *j)) что-то делать.
В противном случае, если все это в порядке убывания, (if (i == begin)), то это последняя перестановка.
В противном случае мы продолжаем и видим, что j и я существенно уменьшены.

Теперь мы понимаем часть if (i == begin), поэтому все, что нам нужно понять, это часть if (*i < *j).

Также обратите внимание: "Тогда, если элементы находятся в порядке возрастания...", который поддерживает наше предыдущее наблюдение, нам нужно только что-то сделать цифру "когда все в порядке находится в порядке убывания". Оператор восходящего порядка if по существу находит самое левое место, где "все справа находится в порядке убывания".

Посмотрим еще раз на некоторые примеры:

...
1 4 3 2
2 1 3 4
...
2 4 3 1
3 1 2 4
...

Мы видим, что когда все справа от цифры находится в порядке убывания, мы находим следующую большую цифру и ставим ее впереди, а затем помещаем оставшиеся цифры в порядке возрастания.

Посмотрите на код:

It k = end;

while (!(*i < *--k))
    /* pass */;

iter_swap(i, k);
reverse(j, end);
return true;

Ну, так как вещи справа находятся в порядке убывания, чтобы найти "следующую большую цифру", нам просто нужно итерации с конца, что мы видим в первых трех строках кода.

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

Ответ 2

gcc-реализация генерирует перестановки в лексикографическом порядке. Wikipedia объясняет это следующим образом:

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

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

Ответ 3

Кнут глубже описывает этот алгоритм и его обобщения в разделах 7.2.1.2 и 7.2.1.3 "Искусство компьютерного программирования". Он называет это "Алгоритмом L" - по-видимому, это относится к XIII веку.

Ответ 4

Здесь полная реализация с использованием других стандартных библиотечных алгоритмов:

template <typename I, typename C>
    // requires BidirectionalIterator<I> && StrictWeakOrdering<C, ValueType<I>>
bool my_next_permutation(I begin, I end, C comp) {
    auto rbegin = std::make_reverse_iterator(end);
    auto rend = std::make_reverse_iterator(begin);
    auto sorted_end = std::is_sorted_until(rbegin, rend, comp);
    bool has_greater_permutation = (sorted_end != rend);
    if (has_greater_permutation) {
        std::iter_swap(
            sorted_end,
            std::upper_bound(rbegin, sorted_end, *sorted_end, comp));
    }
    std::reverse(rbegin, sorted_end);
    return has_greater_permutation;
}

template <typename I>
    // requires BidirectionalIterator<I> && TotallyOrdered<ValueType<I>>
bool my_next_permutation(I begin, I end) {
    return my_next_permutation(
        begin, end, std::less<typename std::iterator_traits<I>::value_type>{});
}

Демо

Ответ 5

Существует самоочевидная возможная реализация на cppreference с помощью <algorithm>.

template <class Iterator>
bool next_permutation(Iterator first, Iterator last) {
    if (first == last) return false;
    Iterator i = last;
    if (first == --i) return false;
    while (1) {
        Iterator i1 = i, i2;
        if (*--i < *i1) {
            i2 = last;
            while (!(*i < *--i2));
            std::iter_swap(i, i2);
            std::reverse(i1, last);
            return true;
        }
        if (i == first) {
            std::reverse(first, last);
            return false;
        }
    }
}

Измените содержание на лексикографически следующую перестановку (на месте) и верните true, если существует иначе сортировка и возврат false, если он не существует.