challenge 重複無し乱数

整数nを渡すと1 ~ n までの整数を重複しないようランダムに出力する関数「bingo」を作ってください。

このお題はraynstardさんの投稿を元にしています。ご投稿ありがとうございました。 投稿の内容には表示のしかたも含まれていたのですが、 このお題では「重複しない1~nまでの乱数をどうやって作るか」という点に集中することにして、 結果の整形は続編としてこの後のお題で出すことにします。 サンプル入出力は下のようになります。

>>> bingo(10)
[10, 7, 8, 4, 5, 2, 3, 1, 6, 9]
>>> bingo(3)
[2, 3, 1]
>>> bingo(3)
[2, 3, 1]
>>> bingo(3)
[3, 1, 2]
>>> bingo(10)
[7, 3, 8, 6, 4, 10, 9, 2, 1, 5]

Posted feedbacks - Nested

Flatten Hidden
Squeak Smalltalk で。
1
2
3
| bingo |
bingo := [:n | (1 to: n) asArray shuffled].
bingo value: 10   "=> #(6 9 3 4 2 1 10 5 8 7) "
Pythonでも1~nのリストを作ってシャッフルする方法が一番てっとりばやいですね。

>>> bingo(10)
[10, 8, 2, 7, 4, 5, 3, 6, 1, 9]
1
2
3
4
5
def bingo(n):
	from random import shuffle
	result = range(1, n + 1)
	shuffle(result)
	return result

	
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Text;
class Program
{
  static void Main()
  {
    Console.WriteLine(bingo(3));
  }
  static string bingo(int n)
  {
    Random r = new Random();
    int[] a = new int[n];
    for (int i = 0; i < n; ++i) a[i] = i + 1;
    for (int i = n; i > 1; --i)
    {
      int k = r.Next(i);
      int tmp = a[i - 1];
      a[i - 1] = a[k];
      a[k] = tmp;
    }
    StringBuilder sb = new StringBuilder("(");
    for (int i = 0; i < n; ++i)
    {
      if (i > 0) sb.Append(", ");
      sb.Append(a[i]);
    }
    sb.Append(")");
    return sb.ToString();
  }
}
久々の1行問題
1
2
3
4
5
6
7
8
def bingo(n)
  (1..n).to_a.sort_by{rand}
end
bingo 10                        # => [4, 2, 1, 10, 8, 5, 3, 7, 6, 9]
bingo 3                         # => [3, 1, 2]
bingo 3                         # => [1, 3, 2]
bingo 3                         # => [2, 3, 1]
bingo 10                        # => [3, 2, 6, 5, 7, 4, 1, 8, 10, 9]

巧みだなあと思ったんですが、ごくわずかの確率でrandが同じ値を返すケースがあるはずで、その場合の要素の並び順はsortの実装によって固定されてしまうから、微妙に偏りが出てくるんじゃないかな、という気もします。

(例えば簡略化のために要素数を2つ、randは0か1しか返さないとして、sortがstableだとすると、可能な4通りの組み合わせのうち(0 1)が3回、(1 0)が1回となる)

現実にはdoubleが重なる確率なんて極めて低いでしょうけど。

ライブラリを使ってシャッフルしているケースでも もしかするとそういう偏りがある可能性はありますね。

気になったのでPythonのrandomライブラリを見てみたところ#2254と同じアルゴリズムでした。

for i in reversed(xrange(1, len(x))):
    # pick an element in x[:i+1] with which to exchange x[i]
    j = int(random() * (i+1))
    x[i], x[j] = x[j], x[i]

ご指摘ごもっともと思ったのでMATLABでこのアルゴリズムで生成する数列の偏りをチェックしてみました(#2615)。このチェック方法がいいのかどうかは実はよく分からないのですが、これでおおざっぱに見る限り普通の精度の乱数を使えば偏りはものすごく小さそうです。ちなみにMATLABの標準のrandpermという同じ目的の関数も#2256と同様のアルゴリズムになっています。

もちろん、IEEE doubleを使った場合2^53通りの値がrandで出現するので (Rubyはメルセンヌツイスタでしたよね? )、「値が衝突する可能性」自体がものすごく少ないです (サンプル数10^5くらいじゃその計算をdoubleでやったら計算誤差よりはるかに小さいくらいの影響)。

ただ、暗号とかストカスティックシミュレーションみたいな、膨大なデータから統計的に意味を見つけ出すような分野で、素材となる元のランダム列に偏りが入っていると結果に影響が出てくるわけで、「そういう用途には使えない」ということだけわかっていれば良いのでは。標準Cの rand() が「用途によっては使えない」のと同じような意味で。

今回のお題はbingoですから、その範囲では十分に題意に沿った回答であると思います。

先の投稿の意図は(rand自体の品質の話ではなく)このアルゴリズム自体に起因する偏りがrandの精度に対してどれくらい小さいかを直感的に見たというだけのもので、最初の投稿について題意云々を批判したつもりはまったくないのですが、そのように読めたのでしたらもうしわけなかったです。失礼しました。

ああ、私もshgさんの投稿を批判と取ったわけではありません。2番目と3番目のパラグラフはshgさんの投稿に対してではなく、このやりとりを読んだ第3者の人が必要以上に問題を大げさにとらえたらまずいなという予防線みたいなつもりでした。まぎらわしくってすみません。

で、実際の偏りの評価なんですが、「randの精度に対して急速に小さくなる」というのはまあ予想できることで、「ものすごーく小さいはずなんだけれど実際のところ53bit精度では具体的にどのくらいの偏りなのかな」という点に触れられていなかったのでああいう書き方になってしまった、ようです。今読み返すとちゃんとしたコメントになっていませんね。失礼しました。

実は#2264時点で衝突する確率の計算も試みてみたのですが、doubleで計算したらすぐに誤差に埋もれてしまうし、無限多倍長数で計算したら計算時間が爆発したので諦めていたのでした。ちゃんと近似式を導いて評価するのが良いのでしょうが…


乱数が衝突する確率を直接計算すると、pを乱数の精度(最小単位の意味で)として、

1-(1-p)(1-2p)(1-3p)...(1-(n-1)p)

みたいな感じでしょうか。p=2^(-53)でn=25だとおよそ2.1e-14。p=2^(-31)でn=100だとおよそ2.3e-6。n個の中で衝突する確率なのでこの値は当然nに大きく依存しますが、実際は衝突は全体にまんべんなく確率的に起きるわけでnが大きいと偏りの問題が大きくなるというわけではないですよね。

bingoの評価としては、1〜nのそれぞれが1番目、2番目、.. n番目に来る確率みたいなのをチェックする方がより直接的かな? (してません)

古い話へのフォローですが、ちゃんと衝突確率を計算してるページを見つけました。(タイトルだけ見るとsort_by{rand}のせいでBigDecimalに問題が生じたのかと勘違いしそうですが、単に衝突確率計算で多倍長整数演算を使いまくったというだけの話のようです。)

	
1
2
use List::Util qw/shuffle/;
sub bingo {shuffle(1..$_[0]);}

	
1
2
3
4
5
6
Number bingo := method(
    Range
    1 to(self) asList shuffle
}

10 bingo println
普通にやってみた
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def bingo(num)
        raise ArgumentError.new("fixnum only") unless num.kind_of? Fixnum
        raise ArgumentError.new("1 ijyou de onegai simasu") unless num >= 1

        results = []

        ary=(1..num).to_a
        ary.size.times{
                index = (rand * ary.size).to_i
                results << ary.delete_at(index)
        }

        results
end
ランダムソートでいいのかな
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>

static int cmp(const void *p1, const void *p2)
{
    return (rand() % 2)?1:-1;
}

void bingo(int num)
{
    int i;
    int *nums;

    if(num <= 0) return;
    nums = (int*)malloc(sizeof(int) * num);
    for(i=0; i<num; i++){
        nums[i] = i + 1;
    }
    qsort(nums, num, sizeof(int), (int (*)(const void*, const void*))cmp);
    for(i=0; i<num; i++){
        printf("%d\n", nums[i]);
    }
    free(nums);
}

int main(int argc, char *argv[])
{
    if(argc < 2){
        return EXIT_FAILURE;
    }
    srand(time(NULL));
    bingo(atoi(argv[1]));
    return EXIT_SUCCESS;
}
rand() % 2 ではだめでしょう。
参考になりました、あと stdlib.h の qsort() を使い回したのも良くないですね。
皆さんのより良いシャッフル方法を参考にさせて頂こうと思います。
% awk -f bingo.awk
5
[3, 5, 1, 4, 2]
24
[22, 11, 5, 2, 4, 3, 9, 24, 6, 7, 17, 16, 21, 15, 19, 18, 12, 14, 13, 23, 10, 20, 8, 1]
10
[7, 4, 9, 3, 5, 10, 2, 1, 8, 6]
10
[10, 8, 3, 2, 6, 7, 1, 9, 5, 4]
10
[5, 7, 1, 8, 10, 3, 6, 2, 4, 9]
10
[6, 2, 3, 5, 1, 9, 8, 10, 7, 4]
10
[6, 9, 8, 5, 4, 2, 1, 3, 10, 7]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
BEGIN {
	srand
}

{
	n = $1

	bingo(n,ar)
	show(n,ar)
}

function show(n,ar, i)
{
	printf "["
	for (i=1; i<n; i++) printf("%d, ", ar[i])
	printf("%d]\n", ar[n])
}

function rand_between_1_and_n(n, x)
{
	x = 1 + int(rand * n)
	return (x <= n)? x : rand_between_1_and_n(n)
}

function bingo(n,ar,  i,x,y,t)
{
	delete ar
	for (i=1; i<=n; i++) ar[i] = i

	for (i=n*2; i>0; i--) {
		x = rand_between_1_and_n(n)
		y = rand_between_1_and_n(n)
		if (x == y) continue

		t = ar[x] ; ar[x] = ar[y] ; ar[y] = t
	}
}
Cleanでやってみた。
擬似乱数を生成するライブラリをつかってます。
本当の乱数じゃないので、同じ数を与えると同じ乱数表になってしまう…
1
2
3
4
5
module RandList
import StdEnv, MersenneTwister

Start = bingo 10
bingo n  = map snd  (sort  (zip ((genRandInt n),[1..n])))
System.Random を使う。シャッフルをナイーブに実装したもの
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module Main (main) where

import System.Environment
import System.Random

shuffle :: [a] -> StdGen -> Int -> [a] -> [a]
shuffle acc _ 0 _  = acc
shuffle acc g n xs = case randomR (0,n-1) g of
 (i,g') -> case splitAt i xs of
             (ys,z:zs) -> shuffle (z:acc) g' (n-1) (ys++zs)

main :: IO ()
main = do { x:_ <- getArgs
          ; g0  <- getStdGen
          ; let n = read x
          ; putStrLn $ show $ shuffle [] g0 n [1..n]
          }

{-
% ./bingo 10
[4,10,3,2,9,5,7,8,6,1]
% ./bingo 10
[2,7,6,8,4,1,9,5,3,10]
% ./bingo 10
[1,7,3,10,6,4,8,2,5,9]
% ./bingo 10
[6,8,9,3,10,4,1,5,2,7]
-}
1
2
import scala.util.Sorting.stableSort
def bingo(n:int) = stableSort[int,double](1 to n,{x=>Math.random})
 破壊的シャッフル。
1
2
3
4
5
6
7
8
9
Array.prototype.$huffle = function(){
  for(var $, r, i = this.length; i;)
    $ = this[r = Math.random() * i-- | 0], this[r] = this[i], this[i] = $;
  return this;
}
function bingo(n){
  for(var a = []; n > 0;) a[a.length] = n--;
  return a.$huffle();
}
C言語と再帰で解いて見た。
ソート関数は利用していません。多分線形時間。

**intを与えているのがちょっとかっこ悪いですが。
あと↓の部分をまともになるように書き換えればOKだけど、アルゴリズムの本質ではないので放置。
swap_point = rand() % (num+1);

アルゴリズムは以下のような感じ
[1~n-1までがシャッフルされた配列] [n]
↑のような配列があったときに、
末尾要素と、ランダムで選ばれた末尾より前の要素とスワップすることで、
全体が1~nまでがシャッフルされた配列になります。
あとはそいつを再帰している感じ。
ループで書いてもよかったかな。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>

void _bingo(int num, int *result, int num_max){
	int temp;
	int swap_point;

	result[num] = num + 1;
	swap_point = rand() % (num+1);
	temp = result[swap_point];
	result[swap_point] = result[num];
	result[num] = temp;

	if (num + 1 == num_max){
		return;
	}
	_bingo(num+1, result, num_max);
	return;
}

void bingo(int num, int** result)
{
	*result = (int*)malloc(num * sizeof(int));
	_bingo(0, *result,num);
}


int main(void)
{
	int *result;
	int num;
	int i;

	num = 100;
	bingo(num, &result);
	
	for(i = 0; i < num; i++){
		printf("%d,", result[i]);
	}
}
あ、freeしてねぇ orz...
mainの最後の行に free(result) を追加してください。
投稿してから気づきましたが、#2254やpythonのshuffleアルゴリズムと同じですね。

	
1
2
3
4
5
6
#light
let shuffle ls =
    let rnd = new System.Random() in
    List.sort (fun x y-> rnd.Next(-1,2)) ls;;

let bingo n = shuffle [1..n]
このshuffle関数ではリストは完全にシャッフルされない。
具体的には
[1..1000]というリストを与えた場合。
500 <-> 501, 10 <-> 15
などの近距離では問題ないが
1 <-> 1000
などの遠距離の交換が起こる可能性が極めて低い。

良い実装法だれか教えてください。
1~nまでのリストをRangeで生成して,
RandomSampleでシャッフルしました.

実行例:
In[2]:= bingo[10]
Out[2]= {6, 3, 10, 8, 1, 2, 9, 5, 4, 7}

RandomSampleで再利用無しの要素取得,
RandomChoiceで再利用OKの要素取得が
できるようになってます.
1
bingo[n_] := RandomSample[Range[n]];
AddしてRemoveAt。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Public Sub bingo(ByVal n As Integer)
    Dim list As New List(Of Integer)
    For i As Integer = 1 To n
        list.Add(i)
    Next
    Dim r As New Random
    For i As Integer = 1 To n
        Dim index As Integer = r.Next(0, n)
        list.Add(list(index))
        list.RemoveAt(index)
    Next

    For Each i As Integer In list
        Console.Write(i.ToString & ", ")
    Next
End Sub
std::random_shuffle を使います。乱数生成関数オブジェクトを指定しないと、何度実行しても同じ乱数列を発生させてしまうので、srand()を使う関数オブジェクトを定義します。

普通はtime()で得られる値を種にするらしいのですが、これは秒単位なので、同一秒内で再度実行すると、やはり同じ乱数列が得られてしまい、うまくありません。

そこで、Windowsでは、起動からのミリ秒を種として渡すことにしました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <cstdlib>

#ifdef _WIN32
# include <windows.h>
# define seed() ::GetCurrentTime()
#else
# include <ctime>
# define seed() std::time(NULL)
#endif

struct RandomGenerator
{
    RandomGenerator()
    {
        std::srand(static_cast<unsigned>(seed()));
    }

    int operator()(int n) const
    {
        return std::rand() % n;
    }
};

void bingo(size_t n)
{
    std::vector<size_t> v;

    for (size_t i = 0; i < n; ++i)
    {
        v.push_back(i);
    }

    static RandomGenerator gen;

    std::random_shuffle(v.begin(), v.end(), gen);

    std::copy(v.begin(), v.end(), std::ostream_iterator<size_t>(std::cout, " "));

    std::cout << std::endl;
}

int main()
{
    bingo(10);
    bingo(3);
    bingo(3);
    bingo(3);
    bingo(10);
}
#2269 によると rand() % n はまずいということなので、修正しました。std::min や std::max は [0, n) の範囲にあることを保証するため、念のため。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--- main.orig	Sat Aug 18 00:35:58 2007
+++ main.cpp	Sat Aug 18 00:41:24 2007
@@ -21,7 +21,9 @@ struct RandomGenerator
 
     int operator()(int n) const
     {
-        return std::rand() % n;
+        const int i = static_cast<int>(static_cast<double>(std::rand()) * n / RAND_MAX);
+
+        return std::max(0, std::min(i, n - 1));
     }
 };
標準のrandpermという関数が問のbingoと全く同じ動作をします。投稿のコードはsortがソート前の順序を返すことを使った別解です。
1
2
function r = bingo(n)
[m r] = sort(rand(1,n));

標準で必要な関数があるのにあえて別解を用いるメリットが最初の例では特に無かったので、少し書き換えてk個の列を一度に返せるようにした。標準のrandpermでk個生成するにはループでk回呼ばないといけない。余談だがMATLAB7.4ではデフォルトの乱数生成アルゴリズムとして#2269さんのリンク先でも紹介されているメルセンヌ・ツイスタが採用されている。

1
2
3
4
5
6
7
8
9
function r = bingo(n,k)
% r = bingo(n) retruns a random permutation of 1:n.
% r = bingo(n,k) returns a k-by-n array, each row of which is a random
% permutation of 1:n.
% (ja.doukaku.org Q46)
if nargin<2
    k = 1;
end
[m r] = sort(rand([k n]),2);

最初の投稿でえらそうに別解です、なんて書いたけどMATLABのrandpermは実は.mファイル(つまりMATLABで書かれたスクリプト)でその中身は投稿したコードと全く同じだということに気付いた。情けないのであらためて別解。前と同様第二引数で何個生成するか指定する。sortを使う方法よりずいぶん遅い。次のようにすると計算時間の測定と順序が均等かどうかのおおまかな確認ができる。

tic;r=bingo(100,10000);bar(sum(r,1));toc
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function r = bingo(n,k)
% r = bingo(n) retruns a random permutation of 1:n.
% r = bingo(n,k) returns a k-by-n array, each row of which is a random
% permutation of 1:n.
% (ja.doukaku.org Q46)
if nargin<2
    k = 1;
end
[s r] = meshgrid(1:k,1:n);
% q = zeros(k,2);
for p = n:-1:2
    q(:,1) = ceil(rand(k,1)*p);
    q(:,2) = p;
    q = q+s(1:2,:)'*n-n;
    r(q) = r(q(:,[2 1]));
end
r = r';

#2264の指摘をみてなるほどと思ったので簡単なテスト。順位相関を使ってみた。

#2285のコード(MATLAB標準のrandpermも同じ方法)は最初に乱数を要求された数列の個数分だけ作り、その順序を返す。MATLABのsortは値が同じ場合は順序を保存するので、たとえば1番目と2番目の乱数が同じ数字だと必ず1が2の前になってしまう。

投稿のコードは乱数の有効桁数(最初の行の最後の引数)を制限し、生成した数列とその位置の順位相関の分布と平均を表示する。有効桁数を1にした場合はかなり偏りが生じるが、3桁くらいでほとんど偏りが無い。順位相関を見る限りでは、普通の精度の乱数を使えばこのアルゴリズムでの偏りを気にする必要は無さそう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
x = bingo(25,1000,1);
rho = corr((1:size(x,2))',x(1:size(x,1),:)','type','Kendall');
figure(1); hist(rho,-1:0.1:1); axis([-1 1 0 300])
fprintf('Mean = %f, skewness = %f\n', mean(rho), skewness(rho));

function r = bingo(n,k,p)
% r = bingo(n,k) returns a k-by-n array, each row of which is a random
% permutation of 1:n, using random numbers with p significant digits .
[m r] = sort(randr([k n],p),2);

function r = randr(M,p)
r = floor(rand(M).*10^p)./10^p;
(print (bingo 10))
 => (2 4 8 10 6 7 5 9 3 1)
1
2
3
4
5
6
7
8
9
(defun bingo (n)
  (loop repeat n
        for rand = (loop 
                     (let* ((*random-state* (make-random-state t))
                            (rand (1+ (random n))))
                       (if (not (member rand lst))
                         (return rand))))
        collect rand into lst
        finally (return lst)))
Rもそうですが、MathematicaやMATLABのような数値解析系はこういうの簡単に書けますね。
元のお題は「n * nのビンゴを出力せよ」みたいな感じだったのかな。

> sample(10)
 [1]  1  9  4 10  3  5  7  2  8  6
1
bingo <- sample
4年前に/.J日記界隈で話題になったネタです。
当時のblogへのリンクを晒しておきます。
# 今も拙いって?
配列にするかリストにするか迷ったのですが、とりあえずリストにしておきました。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.util.*;

public class Sample {
    public static List<Integer> bingo(int num) {
        Integer[] deck = new Integer[num];
        for (int i = 0; i < num; i++) {
            deck[i] = i + 1;
        }
        List<Integer> nums = (List<Integer>) Arrays.asList(deck);
        Collections.shuffle(nums);
        return nums;
    }

    public static void main(String[] args) {
        System.out.println(bingo(