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

私の質問は:それはどのように機能しますか?何の意味があるijk?彼らは実行のさまざまな部分でどのような価値を持っていますか?その正しさの証明のスケッチとは何ですか?

明らかに、メインループに入る前に、それは単純な0または1要素リストのケースをチェックするだけです。メインループの入り口で、iは最後の要素(1つの過去の端ではない)を指し、リストは少なくとも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)
    { // ...
    }
}

ループの最初の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、これは最も古典的なものです。
チェーンro 2015年

1
そのようなアルゴリズムの複雑さは何ですか?
user72708

leetcodeには良い説明があります、leetcode.com
problems /

40

gcc実装は、辞書式順序で順列を生成します。ウィキペディアはそれを次のように説明しています:

次のアルゴリズムは、与えられた順列の後に辞書順で次の順列を生成します。指定された順列をインプレースで変更します。

  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 2015

12

Knuthは、このアルゴリズムと、その一般化について、Art of Computer Programmingのセクション7.2.1.2および7.2.1.3で詳しく説明しています。彼はそれを「アルゴリズムL」と呼びます-明らかにそれは13世紀にさかのぼります。


1
その本の名前を教えてもらえますか?
Grobber、2014年

3
TAOCP = The Art of Computer Programming

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.