std :: next_permutation Пояснення реалізації


110

Мені було цікаво, як це 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. При введенні основного циклу i вказує на останній елемент (не один минулий кінець), і список має принаймні 2 елементи.

Що відбувається в тілі основної петлі?


Гей, як ви витягли цей фрагмент коду? Коли я перевірив #include <алгоритм>, код був зовсім інший, який складався з більше функцій
Манджунат

Відповіді:


172

Давайте розглянемо деякі перестановки:

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)
    { // ...
    }
}

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

Тепер ми розуміємо 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;

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

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


12
Дивовижне пояснення

2
Дякую за пояснення! Цей алгоритм називається поколінням у лексикографічному порядку . У такому алгоритмі є номери Combinatorics, але це найбільш класичний.
ланцюжок ро

1
У чому полягає складність такого алгоритму?
користувач72708

leetcode має гарне пояснення, leetcode.com/problems/next-permutation/solution
bicepjai

40

Реалізація gcc генерує перестановки в лексикографічному порядку. Вікіпедія пояснює це так:

Наступний алгоритм генерує наступну перестановку лексикографічно після заданої перестановки. Це змінює задану перестановку на місці.

  1. Знайдіть найбільший індекс k такий, що a [k] <a [k + 1]. Якщо такого індексу немає, перестановка є останньою перестановкою.
  2. Знайдіть найбільший індекс l такий, що a [k] <a [l]. Оскільки k + 1 - такий показник, l добре визначений і задовольняє k <l.
  3. Поміняйте a [k] на [l].
  4. Зворотну послідовність від [k + 1] до кінцевого елемента a [n].

AFAICT, всі реалізації створюють однаковий порядок.
MSalters

12

Кнут розглядає цей алгоритм та його узагальнення в розділах 7.2.1.2 та 7.2.1.3 «Мистецтва комп’ютерного програмування» . Він називає це «Алгоритм L» - мабуть, він датується 13 століттям.


1
Чи можете ви згадайте назву книги?
Grobber

3
TAOCP = Мистецтво комп’ютерного програмування

9

Ось повна реалізація з використанням інших стандартних алгоритмів бібліотеки:

template <typename I, typename C>
    // requires BidirectionalIterator<I> && Compare<C>
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 rsorted_end = std::is_sorted_until(rbegin, rend, comp);
    bool has_more_permutations = rsorted_end != rend;
    if (has_more_permutations) {
        auto next_permutation_rend = std::upper_bound(
            rbegin, rsorted_end, *rsorted_end, comp);
        std::iter_swap(rsorted_end, next_permutation_rend);
    }
    std::reverse(rbegin, rsorted_end);
    return has_more_permutations;
}

Демо


1
Це підкреслює важливість добрих назв змінних та розділення проблем. is_final_permutationє більш інформативним, ніж begin == end - 1. Виклик is_sorted_until/ upper_boundвідділення логіки перестановки від цих операцій і робить це набагато зрозумілішим. Крім того, верхній_біл - це двійковий пошук, а while (!(*i < *--k));лінійний, тому це є більш ефективним.
Джонатан Гаврич

1

Можлива реалізація можливих реалізацій щодо використання 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, якщо його не існує.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.