競技プログラミングのべんきょうきろく

ブログ名からひと目でわかる圧倒的チラ裏

全列挙神降臨

C#からC++に乗り換えたからいままでの記事は一体何だったのか
そもそも前の更新がいつか覚えてないあばばばばばb

どうでもいいけど全列挙神関数 next_permutation  が降臨したから
これはチラシの裏に書きなぐらなければならない

http://www.cplusplus.com/reference/algorithm/next_permutation/

何をする関数かというと(前提条件→)昇順にソートされた配列内のn要素の順列を順に書き出す関数

僕の勉強し始めて数日のC++力では、
next_permutation(始点のアドレス,終点のアドレス,比較用関数)
があるのは知ってるけど、
next_permutation(始点のアドレス,終点のアドレス)
しか使えないです><;

あと、下のコードではこの怪しいマクロをつかってます。

#define rep(i,val) for(int i = 0; i < val; i++)

何がすごいのか
1. 重複組み合わせが効率的に計算される
O(n!)だからNP困難(???)無理ゲー!! で終わらせる前に、本当はうまく列挙を工夫すれば大幅に計算量をバッサリ切れる。


定式化すると、n要素を愚直に全列挙しようとするとn!だけど、もし、重複分がa1,a2,a3,...個あると、
n!/a1!/a2!/a3!/...と場合の数が減る。
なので、うまく重複分を削れることができれば、数え上げ回数がかなーーーり削減されることになる。
うまく、場合の数を絞り上げればメモ化が捗りそう

下は一次元配列の場合のシンプルな使い方

int main()
{
    int count = 0;
    int syoken[5] = {1,2,3,4,5};
    int syoken_sagi[5] = {1,1,2,2,2};

    do
    {
        count++;
        rep(i,5)
        {
           // cout << syoken[i] << " " << endl;
        }
     //   cout << endl;
    }while (std::next_permutation(syoken, syoken + 5));
    cout << count << endl;
    count = 0;
    do
    { 
        count++;
        rep(i,5)
        {
           // cout << syoken_sagi[i] << " " << endl;
        }
      //  cout << endl;
    }while (std::next_permutation(syoken_sagi, syoken_sagi + 5));
    cout << count << endl;
    return 0;
}

出力結果
120
10

重複分を無駄に計算してたら110回も余分なことをすることになってたのでこれはデカイ

下のコードは二次元配列 {{0,0,0},{3,3,3},{9,9,9}}の全列挙を試したコードで、
countがループの回数を計測してるので、出力結果が9!/3!/3!/3! = 1680 回のループで考えうるすべての場合の数え上げができてる!
重複分を無視して愚直に数え上げしようとすると、9! = 362880回のループになるので、これは 圧 倒 的 削 減
なんかよくわかんないけど array[0]で先頭のアドレス渡すとうまくいけた
なんでだろう、わからない・・・

int main()
{
    int count = 0;
    int syoken[3][3];
    rep(i,3)
        rep(j,3)
        syoken[i][j] = 3*i;
    do
    {
        rep(i,3)
        {
            rep(j,3)
            {
               //cout << syoken[i][j] << " ";
            }
            //cout << endl;
        }
        count++;
        //cout << endl;
    }while (std::next_permutation(syoken[0], syoken[0] + 9));
        cout << count << endl;
    return 0;
}


上のように2次元配列でもアドレスをうまく指定すれば、うまくやってくれるらしい
まぁ、減らせると言ってもn = 50とかになっちゃうと普通に無理
それでも全列挙の心強い味方なのは確定的に明らかで、配列のデータをエンコーディングしてキーとしてもれば、mapとかDictionaryとかでのメモ化が捗りそう(まだやってないけど)

2.実装がくっそ楽
これを自前で実装しようと思ったら繰り返しや再帰の高度なコーディングがいるけど、
do whileとか3行のテンプレでいけちゃう、知ってるか知ってないかで別ゲー

この記事の結論
ぶっちゃけこの記事はただの自分用のメモ、二次元配列の全列挙ができることを書きたかった
どーでもいいけど、do while ってゴミだと思ってたけど(リーダブルコードで読みにくいってdisられてた)ちゃんと有用なのね