2011年12月25日日曜日

[ExcelVBA] n人中m人が当選するくじ


■お題
n人中m人が当選するくじ除(問題出典:どう書く?org)
- 問題詳細 -
n人の中から公平にm人を選ぶ、くじ引きプログラムを作ってください。

■解答例
流れとしては、配列にn人分の要素スペースを作って
シャッフルしてm人分を取り出すだけの簡単なお仕事ですね。

シャッフルは、Fisher-Yatesを使いました。
最終的なコードはこちら→最終的なコード


■Fisher-Yatesアルゴリズムを使ってみる
Fisher-Yatesアルゴリスムとは・・・
配列の一番最後の要素とそれより前の要素群の中から乱数で一つ選び入れ入替える。
そして、配列の要素を一つ前に移動して上記を繰り返すことで入替える。

例えば5つの要素をもった配列があったとします

1サイクル目--------------------------------------------------------------
最後の要素(5)とそれより前(1,2,3,4)の要素から乱数で一つ数字を選びます


乱数で選ばれた数字(ここでは2)と入替をします。




2サイクル目--------------------------------------------------------------
一つ前の要素(4)とそれより前(1,5,3)の要素から乱数で一つ数字を選びます


乱数で選ばれた数字(ここでは1)と入替をします。




3サイクル目--------------------------------------------------------------
一つ前の要素(3)とそれより前(4,5)の要素から乱数で一つ数字を選びます



乱数で選ばれた数字(ここでは5)と入替をします。




4サイクル目--------------------------------------------------------------
1番目と2番目の要素を入れ替えます




では、コードです。
モジュール名:SubModule
Option Explicit

Function lot(n As Integer, m As Integer) As Variant
    Dim list As Variant, i As Integer
    ReDim list(n - 1)
    
    For i = 0 To n - 1
        list(i) = i + 1
    Next
    
    
    Call ArrayShuffle(list)
    
    
    Dim result As Variant
    ReDim result(m - 1)
    For i = 0 To m - 1
        result(i) = list(i)
    Next
    
    lot = result
End Function
3行目:引数は2つ。全数であるnと抽選される人数であるm
4行目:抽選に使うn人分の要素を持った配列を作るためlistを宣言する
5行目:配列の要素を確定する
7~9行目:For分で各要素に抽選番号替わりの数字を格納する
12行目:配列の要素をシャッフルする自作関数。
引数は、参照渡しで渡しています
15行目:当選者を格納する配列用の変数を宣言
16行目:要素数を定義
17~19行目:m人当選するのでlistの最初の要素からm番目までをresult変数に取り出す
21行目:取り出した値を格納しているresult変数を返す



モジュール名:CommonModule
Option Explicit

Sub ArrayShuffle(ByRef list As Variant)
    Randomize
    Dim i As Integer, temp As Integer, RandNum As Integer
    For i = UBound(list) To 1 Step -1
        RandNum = Int(Rnd() * i)
        temp = list(RandNum)
        list(RandNum) = list(i)
        list(i) = temp
    Next
End Sub
参照渡しで引数を受け取ります。
配列の要素が大量にある場合、値渡しで受け取るより参照渡しで
やりとりしたほうが効率的なので、参照渡しにしています。
3行目:配列を参照渡しでうけとります。
4行目:乱数を使うのでRandomizeで乱数の初期化します。
5行目:配列のインデックス用にi変数、
配列の2要素を入れ替えるようにtemp変数
乱数を格納するようにRandNum変数を宣言
6行目:For文で一番最後の要素から2番目の要素までループさせる
7行目:乱数でi番目より前の要素から一つ選ぶ
8行目:同時に2要素を入れ替えれないので、乱数で選んだ要素の値を一時退避する
9行目:乱数で選ばれた要素にi番目の要素を複製する
10行目:i番目の要素に一時退避していた乱数で選んだ要素の値を複製する
11行目:繰り返し


実際にこんな感じで使う

モジュール名:MainModule
Option Explicit

Sub main()
    Dim result As Variant
    result = lot(10, 3)
    
    Dim i As Integer
    For i = 0 To UBound(result)
        Debug.Print result(i)
    Next
    
    Debug.Print "-------------"
End Sub


実行結果
7 
 4 
 6 
-------------
 8 
 3 
 5 
-------------
 7 
 9 
 1 
-------------
 5 
 4 
 8 
-------------


以下に最終的なコードを示しておきます
モジュール名:SubModule
Option Explicit

Function lot(n As Integer, m As Integer) As Variant
    '### n人の中からm人を選ぶ関数
    '### 引数
    '###   省略不可:n…参加者全員の数を指定
    '###   省略不可:m…選ばれる人数を指定
    '###
    '### ArrayShuffle関数を必要
    '########################################

    Dim list As Variant, i As Integer
    ReDim list(n - 1)
    
    For i = 0 To n - 1
        list(i) = i + 1
    Next
    
    
    Call ArrayShuffle(list)
    
    
    Dim result As Variant
    ReDim result(m - 1)
    For i = 0 To m - 1
        result(i) = list(i)
    Next
    
    lot = result
End Function



モジュール名:CommonModule
Option Explicit

Sub ArrayShuffle(ByRef list As Variant)
    '### 配列の要素をシャッフルする関数
    '### 変数
    '###   省略不可:list…参照渡しで配列を渡す
    '##########################################

    Randomize
    Dim i As Integer, temp As Integer, RandNum As Integer
    For i = UBound(list) To 1 Step -1
        RandNum = Int(Rnd() * i)
        temp = list(RandNum)
        list(RandNum) = list(i)
        list(i) = temp
    Next
End Sub

0 件のコメント: