2010.09.21
yna

ちょっとしたアルゴリズムの話

先日プログラムを書いていたら、こんな問題に出くわした。(実際の件数とかは別)

あるデータ10個が0点から5点の範囲で得点をとります。
この時上位5件のデータを取得すること。ただし、同点のデータがあれば、ランダムに選ぶこと。
出題されたとおりに真面目にプログラムすると、こんなコードになる。


    use strict;

    my  $nSelect = 5;
    my  @aryData = (
        { name => "DATA A",     point => 2  },
        { name => "DATA B",     point => 4  },
        { name => "DATA C",     point => 1  },
        { name => "DATA D",     point => 0  },
        { name => "DATA E",     point => 5  },
        { name => "DATA F",     point => 2  },
        { name => "DATA G",     point => 2  },
        { name => "DATA H",     point => 3  },
        { name => "DATA I",     point => 3  },
        { name => "DATA J",     point => 0  },
    );

    # 選択先の配列を用意
    my  @arySelect;

    # 降順に並べる
    my  @arySort = sort{$b->{point} <=> $a->{point}} @aryData;
    # 選択データが一杯でなければ
    while( scalar(@arySelect) < $nSelect){
        my  $nPoint = $arySort[0]->{point};
        my  @aryTemp = grep{$_->{point} == $nPoint;} @arySort;
        #   同じ値の一群が全部入りきるなら
        if( scalar(@aryTemp) < $nSelect - scalar(@arySelect)){
            # 選択用の配列に追加
            push @arySelect, @aryTemp;
            # 元の配列から削除
            splice @arySort, 0, scalar(@aryTemp);
        }
        else {
            while( scalar(@arySelect) < $nSelect){
                # 候補をランダムに選ぶ
                my  $idx = int(rand(@aryTemp));
                push @arySelect, $aryTemp[$idx];
                splice @aryTemp, $idx, 1
            }
        }
    }

 しかしこれでは、効率も悪いしコード量も結構な行数で、コメントを外した状態でも16行もある。
 そこで次のように考える。
 まず、それぞれのデータに乱数でサブになるポイントを付ける。得点でソートして、さらに、乱数のポイントでソートします。このリストの先頭5件を選択します。


    #   randデータを追加する
    map { $_->{rand} = int(rand(1000))} @aryData;
    #   ポイントと乱数データを降順にソート
    my @arySort = sort{
        # まず得点で比較
        if(( $b->{point} != $a->{point} ){
            ( $b->{point} <=> $a->{point}
        }
        # 
        else {
            $b->{rand} <=> $a->{rand};
        }
    }@aryData;
    #   上位5位まで選択
    my @arySelect = @arySort[0..$nSelect-1];

 こうすると、10行くらいになります。
 さらに一歩進めて、ポイントと乱数によるポイントを1回で決めてしまおう。ポイントを1000倍して、乱数を0から999までの乱数を加える。こうすることでことで、比較は1回で済むことになり、たった3行になる。


    #   rankデータを追加する
    map { $_->{rank} = $_->{point}*1000+ int(rand(1000))} @aryData;
    #   ランクを降順にソート
    my @arySort = sort{$b->{rank} <=> $a->{rank}}@aryData;
    #   上位5位まで選択
    my @arySelect = @arySort[0..$nSelect-1];

 プログラムは、誰が書いても同じにならない。ちょっとしたアイディアの差が出るのです。

一覧に戻る