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 <algorithm>을 체크했을 때 더 많은 기능으로 구성된 코드가 완전히 달랐습니다
Manjunath

답변:


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 자리의 순열 만 더 이상 없을 때.
마지막 3 자리의 순열이 더 이상 없을 때는 언제입니까? 마지막 3 자리가 내림차순 일 때.

아하! 이것은 알고리즘을 이해하는 데 중요합니다. 오른쪽에있는 모든 것이 내림차순 일 때만 "숫자"의 위치를 ​​변경합니다. 내림차순 이 아니면 더 많은 순열이있을 수 있기 때문입니다 (예 : 순열을 더 적은 양으로 "증가"시킬 수 있습니다). .

이제 코드로 돌아 갑시다.

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

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

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

루프의 처음 두 줄에서는 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이 많이 있지만 이것이 가장 고전적인 알고리즘입니다 .
chain ro

1
그러한 알고리즘의 복잡성은 무엇입니까?
user72708

leetcode은 좋은 설명이 있습니다 leetcode.com/problems/next-permutation/solution
bicepjai

40

gcc 구현은 사전 순으로 순열을 생성합니다. Wikipedia 는 다음과 같이 설명합니다.

다음 알고리즘은 주어진 순열 후에 사전 식으로 다음 순열을 생성합니다. 주어진 순열을 제자리에서 변경합니다.

  1. a [k] <a [k + 1]이되는 가장 큰 인덱스 k를 찾으십시오. 이러한 인덱스가 없으면 순열이 마지막 순열입니다.
  2. a [k] <a [l]과 같은 가장 큰 인덱스 l을 찾습니다. k + 1은 이러한 인덱스이므로 l은 잘 정의되고 k <l을 충족합니다.
  3. a [k]를 a [l]로 바꿉니다.
  4. a [k + 1]에서 마지막 요소 a [n]까지 순서를 반대로합니다.

AFAICT, 모든 구현은 동일한 순서를 생성합니다.
MSalters

12

Knuth는 The Art of Computer Programming 의 섹션 7.2.1.2 및 7.2.1.3에서이 알고리즘과 일반화에 대해 자세히 설명합니다 . 그는 그것을 "알고리즘 L"이라고 부릅니다. 분명히 그것은 13 세기로 거슬러 올라갑니다.


1
책의 이름을 말씀해 주시겠습니까?
Grobber 2014 년

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은 이러한 연산에서 순열 논리를 분리하고이를 훨씬 더 이해하기 쉽게 만듭니다. 또한 upper_bound는 이진 검색이지만 while (!(*i < *--k));선형이므로 더 성능이 좋습니다.
Jonathan Gawrych

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를 반환합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.