std :: next_permutation Giải thích triển khai


110

Tôi tò mò muốn biết cách std:next_permutationtriển khai nên tôi đã trích xuất gnu libstdc++ 4.7phiên bản và làm sạch các số nhận dạng và định dạng để tạo ra bản trình diễn sau ...

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

Đầu ra như mong đợi: http://ideone.com/4nZdx

Câu hỏi của tôi là: Nó hoạt động như thế nào? Ý nghĩa của i, jvà là kgì? Giá trị nào họ giữ ở các phần khác nhau của quá trình thực hiện? Bản phác thảo của một bằng chứng về tính đúng đắn của nó là gì?

Rõ ràng trước khi vào vòng lặp chính, nó chỉ kiểm tra các trường hợp danh sách phần tử 0 hoặc 1 nhỏ. Tại mục nhập của vòng lặp chính, tôi đang trỏ đến phần tử cuối cùng (không phải một phần tử quá khứ) và danh sách dài ít nhất 2 phần tử.

Điều gì đang xảy ra trong phần nội dung của vòng lặp chính?


Này, làm thế nào bạn giải nén đoạn mã đó? Khi tôi kiểm tra #include <thuật toán>, mã là hoàn toàn khác nhau trong đó bao gồm nhiều chức năng hơn
Manjunath

Câu trả lời:


172

Hãy xem xét một số hoán vị:

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

Làm thế nào để chúng ta đi từ hoán vị này sang hoán vị tiếp theo? Đầu tiên, hãy nhìn mọi thứ khác đi một chút. Chúng ta có thể xem các phần tử dưới dạng chữ số và các hoán vị dưới dạng số . Xem bài toán theo cách này, chúng ta muốn sắp xếp các hoán vị / số theo thứ tự "tăng dần" .

Khi chúng ta đặt hàng số chúng ta muốn "tăng chúng lên một lượng nhỏ nhất". Ví dụ khi đếm, chúng ta không đếm 1, 2, 3, 10, ... vì vẫn còn 4, 5, ... ở giữa và mặc dù 10 lớn hơn 3, nhưng có những số bị thiếu có thể được lấy bằng tăng 3 một lượng nhỏ hơn. Trong ví dụ trên, chúng ta thấy rằng nó 1vẫn là số đầu tiên trong một thời gian dài vì có nhiều sự sắp xếp lại của 3 "chữ số" cuối cùng làm "tăng" hoán vị một lượng nhỏ hơn.

Vì vậy, khi nào chúng ta cuối cùng "sử dụng" 1? Khi chỉ có không còn hoán vị của 3 chữ số cuối cùng.
Và khi nào thì không còn hoán vị nào của 3 chữ số cuối? Khi 3 số cuối theo thứ tự giảm dần.

Aha! Đây là chìa khóa để hiểu thuật toán. Chúng tôi chỉ thay đổi vị trí của một "chữ số" khi mọi thứ ở bên phải theo thứ tự giảm dần vì nếu nó không có thứ tự giảm dần thì vẫn còn nhiều hoán vị hơn để thực hiện (tức là chúng ta có thể "tăng" hoán vị lên một lượng nhỏ hơn) .

Bây giờ hãy quay lại mã:

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

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

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

Từ 2 dòng đầu tiên trong vòng lặp, jlà một phần tử và ilà phần tử trước nó.
Sau đó, nếu các phần tử theo thứ tự tăng dần, ( if (*i < *j)) hãy làm điều gì đó.
Ngược lại, nếu toàn bộ điều theo thứ tự giảm dần, ( if (i == begin)) thì đây là hoán vị cuối cùng.
Nếu không, chúng ta tiếp tục và chúng ta thấy rằng j và i về cơ bản đang giảm dần.

Bây giờ chúng ta đã hiểu if (i == begin)một phần vì vậy tất cả những gì chúng ta cần hiểu là if (*i < *j)một phần.

Cũng lưu ý: "Sau đó, nếu các phần tử theo thứ tự tăng dần ..." hỗ trợ quan sát trước đây của chúng tôi rằng chúng tôi chỉ cần thực hiện điều gì đó với một chữ số "khi mọi thứ ở bên phải theo thứ tự giảm dần". Câu lệnh thứ tự tăng dần ifvề cơ bản là tìm vị trí ngoài cùng bên trái, nơi "mọi thứ ở bên phải theo thứ tự giảm dần".

Hãy xem lại một số ví dụ:

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

Chúng ta thấy rằng khi mọi thứ ở bên phải của một chữ số theo thứ tự giảm dần, chúng ta tìm chữ số lớn nhất tiếp theo và đặt nó ở phía trước và sau đó đặt các chữ số còn lại theo thứ tự tăng dần .

Hãy xem mã:

It k = end;

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

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

Chà, vì những thứ ở bên phải theo thứ tự giảm dần, để tìm "chữ số lớn nhất tiếp theo", chúng ta chỉ cần lặp lại từ cuối mà chúng ta thấy trong 3 dòng mã đầu tiên.

Tiếp theo, chúng ta hoán đổi "chữ số lớn nhất tiếp theo" ở phía trước với iter_swap()câu lệnh và sau đó vì chúng ta biết chữ số đó lớn nhất tiếp theo, chúng ta biết rằng các chữ số ở bên phải vẫn theo thứ tự giảm dần, vì vậy, để đặt nó theo thứ tự tăng dần, chúng ta chỉ cần reverse()nó.


12
Tuyệt vời giải thích

2
Cám ơn vì sự giải thích! Thuật toán này được gọi là Thế hệ theo thứ tự từ vựng . Có rất nhiều thuật toán như vậy Combinatorics, nhưng đây là thuật toán cổ điển nhất.
chain ro

1
Độ phức tạp của thuật toán đó là gì?
user72708

leetcode có lời giải thích tốt, leetcode.com/problems/next-permutation/solution
bicepjai

40

Việc triển khai gcc tạo ra các hoán vị theo thứ tự từ vựng. Wikipedia giải thích nó như sau:

Thuật toán sau tạo ra hoán vị tiếp theo về mặt từ vựng sau một hoán vị nhất định. Nó thay đổi hoán vị đã cho tại chỗ.

  1. Tìm chỉ số k lớn nhất sao cho a [k] <a [k + 1]. Nếu không tồn tại chỉ số này thì hoán vị là hoán vị cuối cùng.
  2. Tìm chỉ số l lớn nhất sao cho a [k] <a [l]. Vì k + 1 là một chỉ số như vậy nên l được xác định rõ và thỏa mãn k <l.
  3. Đổi dấu [k] với [l].
  4. Đảo ngược dãy từ a [k + 1] đến và bao gồm phần tử cuối cùng a [n].

AFAICT, tất cả các triển khai đều tạo ra cùng một thứ tự.
MSalters

12

Knuth đi sâu về thuật toán này và những khái quát của nó trong phần 7.2.1.2 và 7.2.1.3 của Nghệ thuật lập trình máy tính . Ông gọi nó là "Thuật toán L" - rõ ràng nó có từ thế kỷ 13.


1
Bạn có thể vui lòng nêu tên cuốn sách được không?
Grobber

3
TAOCP = The Art of Computer Programming

9

Đây là cách triển khai hoàn chỉnh bằng cách sử dụng các thuật toán thư viện tiêu chuẩn khác:

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

Bản giới thiệu


1
Điều này nhấn mạnh tầm quan trọng của tên biến tốt và tách biệt các mối quan tâm. is_final_permutationnhiều thông tin hơn begin == end - 1. Gọi is_sorted_until/ upper_boundtách logic hoán vị khỏi các phép toán đó và làm cho điều này dễ hiểu hơn nhiều. Ngoài ra, upper_bound là một tìm kiếm nhị phân, trong khi while (!(*i < *--k));là tìm kiếm tuyến tính, vì vậy điều này hiệu quả hơn.
Jonathan Gawrych

1

Có thể tự giải thích về việc sử dụng 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;
        }
    }
}

Thay đổi nội dung thành hoán vị tiếp theo từ điển (tại chỗ) và trả về true nếu tồn tại, nếu không, sắp xếp và trả về false nếu không tồn tại.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.