• ベストアンサー

組み合わせを作るアルゴリズム

アルゴリズムについてうまい考えが浮かばず、お知恵を拝借できればと思い質問致しました。 要件は、値が100個ほど入った配列があり、この配列から任意の個数の値を取り出して処理をする、というものなのですが、とにかく全ての組み合わせを取り出したいのです。 例えば @data = (3, 6, 8, 6); という配列から2個取り出すとすれば、 (3, 6) (3, 8) (3, 6) (6, 8) (6, 6) (8, 6) という全6パターンが欲しいのです。(パターンの出現順は無視できます。) 順番違いはなくてよいので、たとえば (6, 3) というリストはなくて結構です。 また (3, 6) のように、全く同じリストが複数出現してもOKです。 取り出す個数が固定ならばforループのネストで処理のしようもあるのですが、任意ということでここの処理方法が浮かびません。 (ちなみにrは最大で10まで取り、できればforのネストも避けたいところなのですが。) このような処理に適した処理方法をご存知の方おられましたら、ぜひともご教授ください。 よろしくお願い致します。

  • mone
  • お礼率70% (80/114)
  • Perl
  • 回答数4
  • ありがとう数3

質問者が選んだベストアンサー

  • ベストアンサー
  • leaz024
  • ベストアンサー率75% (398/526)
回答No.4

No.3-osamuyさんの > このやり方で100C10を実行すると、うちのiBookでは、約7年半ほどかかります。 についてですが、一般的に再帰は速度とメモリ効率に難があり、それを思ってNo.2で再帰ではないコードを載せたんですが、ちとミスをしており、速度に関してあまり効果が上がっていませんでした。 ミスというのは、頻繁に起きるループからの脱出に goto を使っている点です。 ループからの脱出では「スタックの復帰処理」などをするのですが、gotoは汎用ジャンプ関数のためこれが遅く、足を引っ張られていました。 以下のように変えると、かなり速くなります。 LOOP_TOP:   while (1) {     $func->(@cnt);     for (my $i = 0; $i <= $r; $i++) {       if ($cnt[$r-$i] < $n-$i) {         $cnt[$r-$i]++;         $cnt[$r-$i] = $cnt[$r-$i-1]+1 while $i--;         redo LOOP_TOP;       }     }     last;   }

mone
質問者

お礼

ご回答ありがとうございます。 お礼をする前に2件頂いてしまいましたので、こちらにまとめさせて頂きます。 初めは何の処理をしているのか分かりませんでしたが、@cntをループごとに書き出してみたところ、理解することができました。 速度に関して再度ご投稿を頂きましたので、#2、#3、#4のコードを同様の条件の元に書き直しベンチマークを取ったところ、確かに#4のコードが1番でした。 (#2と#3については、rやnが小さいうちは#3の方が速く、大きくなるにつれ#2の方が速くなるようです。) gotoやredoなどの内部処理については全くの無知でしたので、大変参考になりました。 どうもありがとうございました。

その他の回答 (3)

  • osamuy
  • ベストアンサー率42% (1231/2878)
回答No.3

CPANに、順列/組み合わせを生成するモジュールがあるので、そちらを使うのが手っ取り早いのですが、頭の体操として。 一般的に、この手のものは再帰(recursion)を使うと、簡単に実現できます。 こんな感じ: sub combinatorial { my( $n, $r, $result_set ) = @_; if ( $r == 0 ){ print join( ', ', @$result_set ), "\n"; return; } my $start = $result_set->[ $#$result_set ]; $start += defined $start ? 1 : 0; my $end = $n - $r; for ( my $i = $start; $i <= $end; $i++ ){ push @$result_set, $i; &combinatorial( $n, $r - 1, $result_set ); pop @$result_set; } } &combinatorial( 5, 3, [] ); このやり方で100C10を実行すると、うちのiBookでは、約7年半ほどかかります。

mone
質問者

お礼

ご回答ありがとうございます。 なるほど、再帰という手がありましたね。 #2のleaz024さんの回答もそうでしたが、@dataに適用する添え字のリストを生成するのですね。 大変参考になりました。 どうもありがとうございました。

  • leaz024
  • ベストアンサー率75% (398/526)
回答No.2

そのような可変型のループでは、 ・各ループのカウンタを配列に持つ ・各カウンタの状態管理と更新を行うロジックを書く とよいでしょう。 また、@dataから直接データを取り出すようにするのではなく、n個の配列からr個を取り出す「取り出し方」を生成し、その取り出し方を作業関数に渡すようにするとよいでしょう。 以下は、「7個の配列から4個の値を取り出して処理する」サンプルです。 ※関数workは、7C4 = 35 回実行されます。 my @data = (0, 3, 6, 8, 6, 9, 7); combi($#data+1, 4, \&work);    # 作業関数も渡す # 組み合わせ(nCr)のパターンを作り、1つずつ作業関数に渡す汎用関数 sub combi {   my ($n, $r, $func) = @_;   $n--; $r--;            # 記述簡略化のため1引いておく   my @cnt = (0 .. $r);       # ループカウンタ(組み合わせパターン)を初期化 LOOP_TOP:   $func->(@cnt);          # 組み合わせごとに作業関数を呼び出す   for (my $i = 0; $i <= $r; $i++) {     if ($cnt[$r-$i] < $n-$i) {       # $r-$i番目のカウンタは更新可能か       $cnt[$r-$i]++;                  # $r-$i番目のカウンタを更新       $cnt[$r-$i] = $cnt[$r-$i-1]+1 while $i--;  # それより後ろのカウンタを初期化       goto LOOP_TOP;     }   }   # 全てのカウンタが更新できなければ終了 } # 作業関数 sub work {   print "@data[@_]\n";    # 組み合わせを表示 }

  • riyop
  • ベストアンサー率41% (7/17)
回答No.1

perlでしょうか、、、 $n = @data; for($i=0;$i<$n;$i++){ for($j=$i+1;$j<$n;$j++){ print "($i,$j)\n" } } これでどうでしょうか。(未確認ですが)

mone
質問者

補足

ご回答ありがとうございます。 質問では要素4個の配列から2個を取り出す組み合わせを挙げましたが、実際には要素数は100個ほどあり、取り出す値も最大10まで(場合によって変わります)あるので、for文のネストでは処理が難しいのです。 何か良い処理方法がありましたら、またお願い致します。

関連するQ&A

  • アルゴリズムとデータの個数

    各種配列アルゴリズムのデータ個数と処理時間による比較を行って、このとき,処理時間の平均値を用いたのですが、整列一歩手前等,特別な条件下では処理時間が入れ替わることがありそれはどのようなアルゴリズムの場合かおしえてくれませんか?

  • アルゴリズムの名前を教えてください

    16ビットのデータが、たとえば1000個、配列で与えられているとします。 unsigned short data[1000]; このデータをしらべて、重複するものを除いて何種類の値があるかを数える場合、一番素朴な方法だと、次のようにやると思います。 int i, j; int count = 0; for(i = 0; i < 1000; i++) {  for(j = 0; j < i; j++) {   if(data[j] == data[i]) {    break;   }  }  if(i == j) {   count++;  } } この方法だと、2重のループがあって処理に時間がかかります。そこで、メモリに余裕があれば内側のループをやめて、 int i; int count = 0; int flag[0x10000]; int n; for(i = 0; i < 0x10000; i++) {  flag[i] = 0; } count = 0; for(i = 0; i < 1000; i++) {  n = data[i];  if(flag[n] == 0) {   flag[n] = 1;   count++;  } } これだと、2重のループを使うものよりは速くなりますが、データが1000個しかないのに、65536個のflagをクリアするのに時間がかかり、今ひとつです。 ところが、次のような賢い方法があり、2重のループも配列のクリアも無くすことができます。 int i; int count = 0; unsigned short flag[0x10000]; unsigned short link[0x10000]; int n; for(i = 0; i < 1000; i++) {  n = data[i];  if(flag[n] >= count || link[flag[n]] != n) {   flag[n] = count;   link[count] = n;   count++;  } } 私がこのアルゴリズムを知ったのはかなり昔なので、どこで知ったのか思い出せないのですが、これほど賢いアルゴリズムだから何か名前がついていると思うのですが、それがわかりません。 名前がわからないので、人に頼んだりする時、上のような長い説明をしなくてはなりませんが、名前がわかっていれば「??法でやっといて」、と言えばいいのですけど。

  • 全組み合わせの出力プログラム

    $kana1[0][0] = "a"; $kana1[0][1] = "u"; $kana1[1][0] = "n"; $kana1[1][1] = "m"; という二次元配列があったときに an am un um と出力するようなプログラムのアルゴリズム(?)を教えてほしいです。 簡単なようでforループでやるとうまくいかずwhileを使ってフラグ変数とか作ってやってみましたがどうも駄目です。 ヒントでも何でもいいのでよろしくお願いします! むしろ二次元配列を使うのがダメなら言って下さるとありがたいです。 よろしくお願いします。 最終的には二次元配列の縦も横も任意の数のときにすべての組み合わせを出力できるようにならなければなりません。

    • ベストアンサー
    • Perl
  • アルゴリズムの正当性について

    線形探索法のアルゴリズムの擬似コードを書いて、そのアルゴリズムの正当性をループ不変式を用いて証明するという課題があります。 擬似コードは以下のような流れにしようと思いますが、この場合成り立つループ不変はどのようなことになりますか? 配列A[a1..an]に対してv=A[i]ならば添字iを、vがAの中になければNILを出力するアルゴリズムです。 for i ←1 to N if A[i] = v return i return NIL

  • アルゴリズム・ネストループ方式って何?

    プログラムの性能改善の課題が出ているのですが、アルゴリズムとしてネストループ方式、もしくはその延長上のものを用いること、とあります。 図書館でアルゴリズム関係の本を見てみたのですがどこにもネストループに関して説明がなく、大変困っています。 プログラム自体は、ファイルを読み込んで、表示させるだけの簡単なものです。 簡単に抜き出すと、 for (i=0;; i++){ if ((st = read_a(fd_name, &name_buf, i)) <=0) break; for (j = 0;; j++){ if ((st =read_a (fd_home,&home_buf ,j)) <= 0) break; if (!strcmp(name_buf.a , home_buf.b)){ printf("%s =%s (%s)\n", name_buf.a, name_buf.c , home_buf.c); } } if (st <0) break; といったものです。 注意事項として、break文を入れる手法を使わないこととあります。 お願いします。ネストループって何でしょう?教えてください。

  • アルゴリズムの流れ図

    いつもお世話になってます。アルゴリズムの流れ図の中でいくつかどう処理されているのか分からない箇所がありますので、どなたか教えて頂きたいです。 (1)ループの中で、値が0とかマイナスになるときは増分にあたる所はやらないでいいのですか。 (2)入力文字(入力位置+2)→文字数と書いてあって、その後に文字数-1→文字数ってなっている時、入力文字(入力位置+2)が5(4)だったら文字数に入る値は何になりますか。またそのアルゴリズムの書いてある参考書の隣のページのアルゴリズムの様子が書いてあるのから察すると文字数=4みたいですが、じゃあなんで入力文字(入力位置+2)→文字数を入力位置+2→文字数にしないのですか。 (3)定義済み処理(サブルーチン)Xの中にまたサブルーチンXが入っているときはその値を持ってまた最初に戻ればいいんですか。アルゴリズムの様子の所に書いてある入口、出口とはなんですか。 以上1つでも構いませんので宜しくお願いします。

  • String配列を扱うアルゴリズムについて

    よりパフォーマンスの良いアルゴリズムが、 ございましたらご教示下さい。 数レコード分のDBテーブルデータが格納されたString[][]型が存在するとします。 配列の要素は、String[行(フィールド)][列(カラム)]です。 ここで、全レコード中の列ごとの最大文字列長を int[]型に取得したいと思います。 そうした場合、自作した下記の処理よりも、 よいパフォーマンスを得られるアルゴリズムがございましたら、 ご教示願いたいと思います。 ※処理前提条件 ●String[][]型変数に、過不足無くテーブルデータが格納済みであるとします。 ●配列の第一(行)・第二(列)要素の最大値は取得済みであるとします。 ////////////// // 変数定義 // ////////////// String[][] tableData; ← テーブルデータ格納済み(過不足はありません) int 行数 = 全行数(取得済み); int 列数 = 全列数(取得済み); //列毎の最長文字列値を格納する。 int[] maxLen = new int[列数]; ////////// // 処理 // ////////// //列の個数分、処理を繰り返す for(int i = 0; i < 列数; i++) {   //行の個数分、処理を繰り返す   for(int j = 0; j < 行数; j++) {     //NULLを回避する     if(tableData[i][j] != null) {       //int配列に格納済みの数値より大きければ、改めて格納する       if(maxLen[i] < tableData[i][j].length()) {         maxLen[i] = tableData[i][j].length();       }     }   } } 以上です、どなかお知恵をお貸し頂けませんか。 宜しくお願い致します。

    • ベストアンサー
    • Java
  • 配列から最頻値を求めるアルゴリズム

    電子回路の初心者です。 Arduinoボードをいじっており、温度センサから温度を取得してLCDに表示しているのですが、値がコロコロと変わってしまいます。そこで取得した値を配列に一つずつ格納していき、一定個数そろったら、そこから最頻値を探してLCDを更新しようと思っています。 (例) buf[10] = 3,2,3,3,3,2,3,4,5,3 上記の場合、10個格納した時点で、最も多く格納されている3を抽出したい 配列の左端から値を1つずつ取り出して、残り9個と比較して個数をカウントすることを繰り返す方法を考えたのですが、かなり時間が掛かりそうに思えます。もっと良い方法、一般的なやり方等がありましたら教えて頂けないでしょうか?

  • 組み合わせのパターンを作成する関数

    組み合わせのパターンを作成する関数を作成したいと思っています。 仕様は以下のような感じです。 引数 ・list:材料となる文字の配列 ・len:作成する文字列の数(2以上) 戻り値 ・組合わせた文字列の配列 例 (["a", "b", "c"], 2)→["aa", "ab", "ac", "ba", "bb", "bc", "ca", "cb", "cc"] (["a", "b"], 3)→["aaa", "aab", "aba", "abb", "baa", "bab", "bba", "bbb"] 私はとりあえず作成する総パターン数(Math.pow(list.length, len))回ループでまわし、indexをtoStrungでn進数にし(ゼロパディングし)、最後に数字と対応する文字を置換するという方法で実装しました。とりあえず動いています。 しかし他にもいろいろなアルゴリズムが考えられそうなので、他の実装・よりよい方法を知りたいと思いまして質問させていただくことにしました。 できれば具体的なコードを教えていただきたいです。 よろしくお願いします。

  • perl:ループのカウンタ変数の値を保持したい。

    While文のループのなかにfor文でループをまわしているスクリプトなのですが、 forの中でカウンタ変数をつくり、ループ回数を計測しております。 またforの中である条件を満たした際に、lastでforを抜け、引き続きWhileのループを継続するという処理をしております。 $i=0; While(○○){ 処理1    for(××){ 処理2 $i++;      if($i >=100){  処理3       last; } } } ここで、一度for文のif文で一度forループを抜け、Whileでループをし、またforループに突入した際に、前回forループでカウントした$iの値を保持したまま、そのつづきから$iのカウンタを動作させたいのですが、$iの値はforループを抜けるとリセットしてしまいます。 このような場合、どうすれば$iの値を保持できますでしょうか。 お詳しい方、宜しくお願い致します。 ※ネストがうまく表現できず、みずらくてスミマセン。

    • ベストアンサー
    • Perl