特定の範囲の乱数を正しく作ることは、ゲームやシミュレーション、テストデータの生成などでとても重要です。
本記事ではC言語で1〜100の整数乱数を均等に生成する方法を、初心者向けに基礎から丁寧に説明します。
よく見るrand()%100+1は本当にダメなのかという疑問にも答え、環境差や偏りの理由、避け方、そしてミスを防ぐ動作確認までまとめます。
初心者向け: 1〜100の乱数の生成方法
範囲の乱数のゴールを確認
乱数の「良し悪し」を考える前に、まずゴールをはっきりさせます。
ここでのゴールは1〜100の各整数が同じ確率で出ること(均等)、かつ1と100を含み、それ以外は出ないことです。
さらに、毎回同じ並びにならないように乱数の種(seed)を適切に設定することも実用上たいせつです。
「均等」とは、長い回数引いたときに各値の出現回数がほぼ同じになることを意味します。
ここが崩れると、例えばサイコロの目に偏りが生じ、ゲームの公平性やシミュレーションの信頼性が損なわれます。
randとRAND_MAXの基礎だけ押さえる
C標準ライブラリのrand()は0以上RAND_MAX以下の擬似乱数を返します。
RAND_MAXは環境によって値が異なり、最低でも32767です。
つまりrand()自体は「0〜RAND_MAX」の範囲で整数を一様に返す関数です。
この乱数を「1〜100」に変換するのが本記事の主題です。
もう1つ重要なのはsrand()で種(seed)を設定することです。
毎回同じ並びでよければ不要ですが、普通はsrand((unsigned)time(NULL))のように現在時刻を使って初期化します。
種の設定はプログラム開始時に1回だけ行い、繰り返し呼ばないようにします。
randの基礎を確認する短い例
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
int main(void) {
printf("RAND_MAX: %d\n", RAND_MAX);
// 乱数の種を設定(毎回違う並びにしたい場合)
srand((unsigned)time(NULL));
// 0〜RAND_MAXの例を5つ表示
for (int i = 0; i < 5; ++i) {
int r = rand();
printf("rand() -> %d\n", r);
}
return 0;
}
実行結果(一例):
RAND_MAX: 2147483647
rand() -> 1804289383
rand() -> 846930886
rand() -> 1681692777
rand() -> 1714636915
rand() -> 1957747793
出力は環境や実行時刻により異なります。
NG例(rand()%100+1)はNG?
RAND_MAXに依存して環境差が出る
よく見かけるrand()%100+1は、直感的で短く書けますが、一般には均等ではありません。
理由はRAND_MAXが100の倍数であるとは限らないからです。
具体例としてRAND_MAXが32767の環境を考えます。
このときrand()の取りうる値は32768個です。
これを%100すると、次のように割り当てられます。
- 0〜99の各余りに対して、基本的に327回ずつ割り当てられる
- しかし
32768 % 100 == 68なので、余り0〜67には1回ずつ余計に割り当てられ、328回になる
つまり1〜68が他よりわずかに出やすくなります。
この差は約0.3%で、小さく見えるかもしれませんが、回数が多いと無視できなくなります。
別の環境でRAND_MAXが異なれば、偏る番号の範囲も変わり、環境差が出ます。
理屈の要点を数式で
- 全パターン数 N = RAND_MAX + 1
- 商 q = floor(N / 100)、余り r = N % 100
- 値1〜rは q+1 回、値 r+1〜100 は q 回の割り当て → 値ごとに確率差が発生
簡易用途ならOKな場面も
厳密な均等性が不要で、軽い実験や教育用サンプル、短命のスクリプトなどではrand()%100+1でも実害が出ないことはあります。
また、まれにN(=RAND_MAX+1)が範囲に割り切れる場合には偏りが発生しません。
とはいえ、本番や検証的な用途では避けるのが安全です。
正しい方法: 1〜100の乱数を均等に生成
棄却法(リジェクションサンプリング)を用いると、完全に均等な整数乱数に変換できます。
やり方は「100で割り切れる範囲に収まるrand()の値だけを採用」し、それ以外は引き直すというものです。
コード例(棄却法、1〜100専用と汎用)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
// 1〜100の乱数を均等に返す
static int rand_1_to_100_uniform(void) {
const int range = 100; // 1〜100の範囲幅
// cutoffはrangeの倍数になる最大の値+1を、[0, cutoff)で表すための下限境界
// 具体的には、rand()が返す値rのうち r < cutoff を採用すると
// 採用される個数がrangeの倍数になり、r % rangeが均等になります。
const int cutoff = RAND_MAX - (RAND_MAX % range); // [0, cutoff)を採用
int r;
do {
r = rand();
} while (r >= cutoff); // cutoff以上は捨てて引き直し
return 1 + (r % range); // 1〜100に変換
}
// 汎用: [min, max]の乱数を均等に返す(範囲はint内で、幅がRAND_MAX+1以下を想定)
static int rand_inclusive_uniform(int min, int max) {
// 前提チェック(必要ならassertなど)
if (min > max) {
// 簡易に入れ替え
int tmp = min; min = max; max = tmp;
}
// 範囲幅(オーバーフロー防止のためlongに拡張)
long range = (long)max - (long)min + 1L;
// rangeが正でかつRAND_MAX+1以上でない前提(今回は1〜100なので安全)
const long cutoff = RAND_MAX - (RAND_MAX % range);
int r;
do {
r = rand();
} while ((long)r >= cutoff);
return min + (int)((long)r % range);
}
int main(void) {
// 毎回違う並びにするために、プログラム開始時に1回だけseedを設定します。
srand((unsigned)time(NULL));
printf("RAND_MAX: %d\n", RAND_MAX);
printf("均等な1〜100の乱数を10個: ");
for (int i = 0; i < 10; ++i) {
int v = rand_1_to_100_uniform();
printf("%d ", v);
}
printf("\n");
// 汎用関数の例: [10, 20]から5個
printf("[10, 20]の均等な乱数を5個: ");
for (int i = 0; i < 5; ++i) {
int v = rand_inclusive_uniform(10, 20);
printf("%d ", v);
}
printf("\n");
return 0;
}
実行結果(一例):
RAND_MAX: 2147483647
均等な1〜100の乱数を10個: 89 12 100 1 57 34 96 23 78 5
[10, 20]の均等な乱数を5個: 18 11 10 20 14
もう1つの選択肢: 浮動小数点スケーリング
近似的に均等な方法としてrand()を浮動小数点にスケーリングする手もあります。
// ほぼ均等(浮動小数点の丸め誤差は非常に小さい)
static int rand_1_to_100_almost_uniform(void) {
// (RAND_MAX + 1.0)で正規化し、[0, 1)へ
double u = (double)rand() / ((double)RAND_MAX + 1.0);
// [0, 100)へ拡大し、整数へ切り捨てて1加算で[1, 100]へ
return 1 + (int)(u * 100.0);
}
丸め誤差は理論上ゼロではありませんが、倍精度での影響は極めて小さく、多くの実務では十分です。
完全な均等性が必要なら棄却法を使い、速度優先でほぼ均等で良いならこちらも選択肢になります。
3つの方法の比較
| 方法 | 均等性 | 速度 | 実装の難易度 | 備考 |
|---|---|---|---|---|
| rand()%100+1 | 不均等(偏りあり) | とても速い | かんたん | RAND_MAXに依存し環境差が出る |
| 浮動小数点スケーリング | ほぼ均等 | 速い | かんたん | 丸め誤差は非常に小さい |
| 棄却法 | 完全に均等 | やや遅い | 普通 | ループで一部の値を捨てるが実用上十分速い |
迷ったら棄却法、速度最優先で偏りを極小に抑えたいだけなら浮動小数点スケーリング、と覚えておくとよいです。
ミス防止と動作チェック
1と100を含むことを確認
範囲の端を含められているかの確認はシンプルですが重要です。
以下は均等関数で多回試行し、最小値と最大値を観測します。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
static int rand_1_to_100_uniform(void) {
const int range = 100;
const int cutoff = RAND_MAX - (RAND_MAX % range);
int r;
do { r = rand(); } while (r >= cutoff);
return 1 + (r % range);
}
int main(void) {
srand((unsigned)time(NULL));
int trials = 200000; // 試行回数(必要に応じて増やす)
int minv = 101, maxv = 0; // 観測した最小・最大
for (int i = 0; i < trials; ++i) {
int v = rand_1_to_100_uniform();
if (v < minv) minv = v;
if (v > maxv) maxv = v;
}
printf("観測最小値: %d, 観測最大値: %d\n", minv, maxv);
return 0;
}
実行結果(一例):
観測最小値: 1, 観測最大値: 100
十分な回数を試せば、1と100の両方が観測されるはずです。
101や0が出ないことを確認
範囲外が出ないことも合わせて確認します。
観測統計に範囲外が1つも入らないことをチェックします。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
static int rand_1_to_100_uniform(void) {
const int range = 100;
const int cutoff = RAND_MAX - (RAND_MAX % range);
int r;
do { r = rand(); } while (r >= cutoff);
return 1 + (r % range);
}
int main(void) {
srand((unsigned)time(NULL));
int trials = 500000;
int outside = 0;
for (int i = 0; i < trials; ++i) {
int v = rand_1_to_100_uniform();
if (v < 1 || v > 100) {
++outside;
}
}
printf("範囲外の出現回数: %d (期待値は常に0)\n", outside);
return 0;
}
実行結果(一例):
範囲外の出現回数: 0 (期待値は常に0)
常に0であることが正しい実装の目安です。
分布の偏りを簡単に確認
rand()%100+1の偏りは理屈で説明できますが、簡易的に観測して納得しておくと理解が定着します。
次のプログラムは、同じシードでrand()%100+1と棄却法をそれぞれ500万回試し、各値の回数分布を比較します。
さらにRAND_MAX+1を100で割ったときの余り(偏りの原因)も表示します。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
enum { N = 100 };
static int naive_mod(void) {
return 1 + (rand() % N); // 偏りありのNG例
}
static int uniform_reject(void) {
const int cutoff = RAND_MAX - (RAND_MAX % N);
int r;
do { r = rand(); } while (r >= cutoff);
return 1 + (r % N);
}
static void summarize(const char *title, const int cnt[]) {
int minc = cnt[0], maxc = cnt[0], minv = 1, maxv = 1;
long long total = 0;
for (int i = 0; i < N; ++i) {
if (cnt[i] < minc) { minc = cnt[i]; minv = i + 1; }
if (cnt[i] > maxc) { maxc = cnt[i]; maxv = i + 1; }
total += cnt[i];
}
double mean = (double)total / N;
printf("%s: mean=%.2f, min=%d(value=%d), max=%d(value=%d)\n",
title, mean, minc, minv, maxc, maxv);
}
int main(void) {
// 同じ並びで両方式を比較するため、固定seedを使います
const unsigned seed = 123456789u;
const int trials = 5000000; // 500万回(時間がかかる場合は減らしてください)
int cnt_naive[N] = {0};
int cnt_uniform[N] = {0};
// naive計測
srand(seed);
for (int i = 0; i < trials; ++i) {
int v = naive_mod();
cnt_naive[v - 1]++;
}
// uniform計測(同じrand列で比較するためseedを同じに戻す)
srand(seed);
for (int i = 0; i < trials; ++i) {
int v = uniform_reject();
cnt_uniform[v - 1]++;
}
// 余り(偏りの「余り」)を計算
long long Ntotal = (long long)RAND_MAX + 1LL;
int rem = (int)(Ntotal % N);
printf("RAND_MAX=%d, RAND_MAX+1=%lld, (RAND_MAX+1) %% 100 = %d\n",
RAND_MAX, Ntotal, rem);
summarize("naive_mod", cnt_naive);
summarize("uniform_reject", cnt_uniform);
// 参考: naiveで偏りが出やすい番号群(1..rem)の平均と、それ以外の平均
if (rem > 0) {
long long sum_hi = 0, sum_lo = 0;
for (int i = 0; i < rem; ++i) sum_hi += cnt_naive[i]; // 1..rem
for (int i = rem; i < N; ++i) sum_lo += cnt_naive[i]; // rem+1..100
double avg_hi = (double)sum_hi / rem;
double avg_lo = (double)sum_lo / (N - rem);
printf("naive_mod: avg(1..%d)=%.2f, avg(%d..100)=%.2f, diff=%.2f (期待:前者がやや大きい)\n",
rem, avg_hi, rem + 1, avg_lo, (avg_hi - avg_lo));
}
return 0;
}
実行結果(一例):
RAND_MAX=2147483647, RAND_MAX+1=2147483648, (RAND_MAX+1) % 100 = 48
naive_mod: mean=50000.00, min=49418(value=79), max=50565(value=23)
uniform_reject: mean=50000.00, min=49435(value=21), max=50556(value=59)
naive_mod: avg(1..48)=50032.79, avg(49..100)=49970.54, diff=62.25 (期待:前者がやや大きい)
出力は環境とシードで変わりますが、naive_modの方で「1..余り」側の平均がわずかに高い傾向が読み取れます。
棄却法ではこの差が消え、均等性が回復します。
まとめ
C言語で1〜100の乱数を均等に生成するなら、rand()%100+1は避け、棄却法(リジェクションサンプリング)を使うのが安全です。
理由はRAND_MAXが環境ごとに異なり、100で割り切れない場合に剰余演算が偏りを生むからです。
棄却法は「100で割り切れる範囲に収まったrand()の値だけ採用」することで、数学的に均等性を保証します。
速度面の心配は多くの用途で杞憂であり、品質を優先するなら棄却法一択です。
補足的に、doubleで正規化してからスケーリングする方法もあり、ほぼ均等で高速です。
厳密性が不要なら実用的な選択肢です。
どの方法でも、seedはプログラム開始時に1回だけ設定する、1と100を含み範囲外が出ないことをテストするといった基本を守れば、信頼できる乱数生成ができます。
