std :: next_permutation Implementasi Penjelasan

110

Saya ingin tahu bagaimana std:next_permutationditerapkan, jadi saya mengekstrak gnu libstdc++ 4.7versinya dan membersihkan pengenal dan pemformatan untuk menghasilkan demo berikut ...

#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()));
}

Outputnya seperti yang diharapkan: http://ideone.com/4nZdx

Pertanyaan saya adalah: Bagaimana cara kerjanya? Apa artinya i, jdan k? Nilai apa yang mereka pegang di berbagai bagian eksekusi? Apa sketsa bukti kebenarannya?

Jelas sebelum memasuki loop utama itu hanya memeriksa kasus daftar elemen 0 atau 1 sepele. Pada entri loop utama, saya menunjuk ke elemen terakhir (bukan satu ujung terakhir) dan daftarnya setidaknya terdiri dari 2 elemen.

Apa yang terjadi di badan loop utama?

Andrew Tomazos
sumber
Hei, bagaimana Anda mengekstrak potongan kode itu? Ketika saya memeriksa #include <algorithm>, kodenya benar-benar berbeda yang terdiri dari lebih banyak fungsi
Manjunath

Jawaban:

172

Mari kita lihat beberapa permutasi:

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
...

Bagaimana kita beralih dari satu permutasi ke permutasi berikutnya? Pertama, mari kita lihat hal-hal dengan sedikit berbeda. Kita dapat melihat elemen sebagai digit dan permutasi sebagai angka . Melihat masalah dengan cara ini kami ingin mengurutkan permutasi / angka dalam urutan "naik" .

Ketika kita memesan nomor, kita ingin "meningkatkannya dengan jumlah terkecil". Misal saat menghitung kita tidak menghitung 1, 2, 3, 10, ... karena masih ada 4, 5, ... di antaranya dan walaupun 10 lebih besar dari 3, ada bilangan yang hilang yang bisa didapat meningkatkan 3 dengan jumlah yang lebih kecil. Dalam contoh di atas kita melihat bahwa 1tetap sebagai angka pertama untuk waktu yang lama karena ada banyak pengurutan ulang dari 3 "digit" terakhir yang "meningkatkan" permutasi dengan jumlah yang lebih kecil.

Jadi kapan kita akhirnya "menggunakan" 1? Ketika tidak ada lagi permutasi dari 3 digit terakhir.
Dan kapan tidak ada lagi permutasi dari 3 digit terakhir? Saat 3 digit terakhir berada dalam urutan menurun.

Aha! Ini adalah kunci untuk memahami algoritme. Kami hanya mengubah posisi "digit" ketika segala sesuatu di sebelah kanan dalam urutan menurun karena jika tidak dalam urutan menurun maka masih ada lebih banyak permutasi yang harus dilakukan (yaitu kita dapat "meningkatkan" permutasi dengan jumlah yang lebih kecil) .

Sekarang mari kembali ke kode:

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

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

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

Dari 2 baris pertama dalam loop, jmerupakan elemen dan imerupakan elemen sebelumnya.
Kemudian, jika elemen berada dalam urutan menaik, ( if (*i < *j)) lakukan sesuatu.
Sebaliknya, jika semuanya dalam urutan menurun, ( if (i == begin)) maka ini adalah permutasi terakhir.
Jika tidak, kami melanjutkan dan kami melihat bahwa j dan i pada dasarnya berkurang.

Kami sekarang memahami if (i == begin)bagian tersebut sehingga yang perlu kami pahami hanyalah if (*i < *j)bagian tersebut.

Perhatikan juga: "Maka jika elemen-elemennya dalam urutan menaik ..." yang mendukung pengamatan kita sebelumnya bahwa kita hanya perlu melakukan sesuatu pada satu digit "ketika segala sesuatu di sebelah kanan dalam urutan menurun". ifPernyataan urutan menaik pada dasarnya mencari tempat paling kiri di mana "segala sesuatu di sebelah kanan dalam urutan menurun".

Mari kita lihat kembali beberapa contoh:

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

Kita melihat bahwa ketika semua yang ada di sebelah kanan digit berada dalam urutan menurun, kita menemukan digit terbesar berikutnya dan meletakkannya di depan dan kemudian meletakkan digit yang tersisa dalam urutan menaik .

Mari kita lihat kodenya:

It k = end;

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

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

Karena hal-hal di kanan berada dalam urutan menurun, untuk menemukan "digit terbesar berikutnya" kita hanya perlu mengulang dari akhir, yang kita lihat di 3 baris kode pertama.

Selanjutnya, kita menukar "digit terbesar berikutnya" ke depan dengan iter_swap()pernyataan dan kemudian karena kita tahu digit tersebut adalah digit terbesar berikutnya, kita tahu bahwa digit di sebelah kanan masih dalam urutan turun, jadi untuk mengurutkannya dalam urutan naik, kita hanya harus melakukannya reverse().

penerbangan
sumber
12
Penjelasan yang luar biasa
2
Terima kasih atas penjelasannya! Algoritma ini disebut Generasi dalam urutan leksikografik . Ada beberapa algoritme seperti itu Combinatorics, tapi ini yang paling klasik.
rantai ro
1
Apa kompleksitas algoritma tersebut?
pengguna72708
leetcode memiliki penjelasan yang baik, leetcode.com/problems/next-permutation/solution
bicepjai
40

Implementasi gcc menghasilkan permutasi dalam urutan leksikografis. Wikipedia menjelaskannya sebagai berikut:

Algoritme berikut menghasilkan permutasi berikutnya secara leksikografis setelah permutasi tertentu. Ini mengubah permutasi yang diberikan di tempat.

  1. Tentukan indeks k terbesar sehingga a [k] <a [k + 1]. Jika tidak ada indeks seperti itu, permutasi adalah permutasi terakhir.
  2. Tentukan indeks terbesar l sehingga a [k] <a [l]. Karena k + 1 adalah indeks seperti itu, l terdefinisi dengan baik dan memenuhi k <l.
  3. Tukar a [k] dengan [l].
  4. Membalik urutan dari a [k + 1] hingga dan termasuk elemen terakhir a [n].
TemplateRex
sumber
AFAICT, semua implementasi menghasilkan urutan yang sama.
MSalters
12

Knuth membahas lebih dalam tentang algoritma ini dan generalisasinya di bagian 7.2.1.2 dan 7.2.1.3 Seni Pemrograman Komputer . Dia menyebutnya "Algoritma L" - rupanya itu berasal dari abad ke-13.

rvighne
sumber
1
Bisa tolong sebutkan nama bukunya?
Grobber
3
TAOCP = Seni Pemrograman Komputer
9

Berikut implementasi lengkap menggunakan algoritme pustaka standar lainnya:

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;
}

Demo

Brian Rodriguez
sumber
1
Ini menggarisbawahi pentingnya nama variabel yang baik dan pemisahan perhatian. is_final_permutationlebih informatif daripada begin == end - 1. Memanggil is_sorted_until/ upper_boundmemisahkan logika permutasi dari operasi tersebut, dan membuatnya jauh lebih bisa dimengerti. Selain itu upper_bound adalah pencarian biner, sedangkan while (!(*i < *--k));linier, jadi ini lebih berkinerja.
Jonathan Gawrych
1

Ada kemungkinan implementasi yang jelas pada penggunaan 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;
        }
    }
}

Ubah konten menjadi permutasi berikutnya secara leksikografis (di tempat) dan kembalikan true jika ada, jika tidak urutkan dan kembalikan false jika tidak ada.

Shreevardhan
sumber