シャッフルと乱数 解答ページ | Programming Place Plus 新C++編

トップページ新C++編シャッフルと乱数

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

シード値を固定したシャッフルを実装すると、どのような結果になりますか?


本編では、シャッフルを次のコードで行いました(本編解説)。

std::random_device rand_dev {};
std::mt19937 rand_engine(rand_dev());
std::shuffle(std::begin(v), std::end(v), rand_engine);

この場合、シード値は rand_dev() なので、std::random_device によって生成された乱数が使われるため、シャッフルの結果は毎回ちがったものになります。

シード値を固定するには、rand_dev() の代わりに、適当な整数リテラルを指定します。

#include <algorithm>
#include <iostream>
#include <random>
#include <vector>

int main()
{
    std::vector<int> v {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    std::mt19937 rand_engine(123);  // 適当な整数リテラルを指定
    std::shuffle(std::begin(v), std::end(v), rand_engine);

    for (auto e : v) {
        std::cout << e << " ";
    }
    std::cout << "\n";
}

実行結果:

4 2 8 6 5 1 7 0 3 9

シード値が固定されると、生成される乱数の順番がいつも同じになります。そのような乱数を使っておこなうシャッフルの結果もまた、いつも同じになります。

問題2 (基本★)

-100 ~ 100 の範囲の整数をランダムに 100個生成するプログラムを作成してください。


生成される乱数の範囲を調整するには、分布生成器を使います。std::uniform_int_distribution を使うと、指定範囲内の整数が均等な確率で得られます(本編解説)。

#include <iostream>
#include <random>

int main()
{
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::uniform_int_distribution<int> dist(-100, 100);

    for (int i = 0; i < 100; ++i) {
        std::cout << dist(rand_engine) << "\n";
    }
}

実行結果:

-26
80
-67
-95
-36
22
-8
33
-59
91
-81
-94
56
-37
-15
-5
-45
60
14
60
-22
93
-55
-68
-14
62
3
-53
81
13
89
-88
-53
100
46
35
2
29
-44
77
35
57
-67
-12
-3
21
96
-8
49
21
-32
22
-1
-76
-71
68
-20
73
-30
23
-70
-94
46
80
-95
16
-80
7
74
64
-66
18
-37
52
4
-59
-36
73
70
-18
1
63
81
-6
6
39
11
87
-62
26
54
32
100
-33
-15
-35
80
-66
-20
-17

std::uniform_int_distribution型の変数を宣言するときに、最小値と最大値を指定します。今回は -100 と 100 です。

分布生成器を使って乱数を得るときには、分布生成器のほうの operator() を呼びます。その実引数に、乱数生成エンジンを渡します。

問題3 (基本★)

次のように宣言された std::vector<int> から、要素をランダムに選び出して出力するプログラムを作成してください。

std::vector<int> v {7, -3, 2, 6, 0, 15, -2, 4};


要素をランダムに選び出すためには、添字をランダムに生成できればいいということになります。問題2と同じで、分布生成器を使って、0~末尾までの添字が生成されるようにします。末尾の添字は v.size() - 1 で得られます。

#include <iostream>
#include <random>
#include <vector>

int main()
{
    std::vector<int> v {7, -3, 2, 6, 0, 15, -2, 4};

    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::uniform_int_distribution<int> dist(0, v.size() - 1);

    for (int i = 0; i < 10; ++i) {
        std::cout << v.at(dist(rand_engine)) << "\n";
    }
}

実行結果:

-3
2
4
0
7
6
15
15
0
-2

問題4 (応用★★)

operator() を呼び出すたびに、前回よりも 1 大きい整数を返す関数オブジェクトを作ってください。初期値は使用者の側で指定できるようにします。


たとえば次のように書けます。

#include <iostream>

struct CountUp {
    int operator()() {
        return value++;
    }

    int value;
};

int main()
{
    CountUp count_up {0};

    for (int i = 0; i < 5; ++i) {
        std::cout << count_up() << "\n";
    }
}

実行結果:

0
1
2
3
4

関数オブジェクトとして使えるようにするには、operator() をもった構造体(クラス)を定義します(本編解説)。構造体にすぎないので、データメンバを持つことができますから、ここに現在の値を記憶しておけます。

なお、operator() の実装は、return value++; のように、後置インクリメントを使えば1行で書けます。このコードは次のように処理されます。

  1. 変数value の値を、ソースコード上からはみえないところにコピーしておく
  2. 変数value の値を +1 する
  3. 1で取っておいたコピーを return する

問題5 (応用★★)

ランダムな計算問題を、5問くりかえし出題するプログラムを作成してください。計算式は「整数 演算子 整数」のかたちにするものとし、整数は 1~10 の範囲、演算子は + - * / のいずれかにします。答えを入力させ、正解か不正解かを出力するようにしてください。


たとえば、次のようになります。

#include <iostream>
#include <random>
#include <string>

// 演算子
enum class Operator {
    addition,
    subtraction,
    multiplication,
    division,
};

// 演算子の文字列表現を返す
std::string get_operator_string(Operator op)
{
    switch (op) {
    case Operator::addition:
        return "+";
    case Operator::subtraction:
        return "-";
    case Operator::multiplication:
        return "*";
    case Operator::division:
        return "/";
    }
    return "";
}

// 演算子に応じた計算を行う
int calc(int value1, Operator op, int value2)
{
    switch (op) {
    case Operator::addition:
        return value1 + value2;
    case Operator::subtraction:
        return value1 - value2;
    case Operator::multiplication:
        return value1 * value2;
    case Operator::division:
        return value1 / value2;
    }
    return value1;
}

int main()
{
    constexpr auto QuestionNum = 5;     // 出題数

    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::uniform_int_distribution<int> value_dist(1, 10);
    std::uniform_int_distribution<int> op_dist(0, 3);

    for (int i = 0; i < QuestionNum; ++i) {

        // 出題内容を生成する
        int value1 {value_dist(rand_engine)};
        auto op = static_cast<Operator>(op_dist(rand_engine));
        int value2 {value_dist(rand_engine)};

        // 問題を提示して、答えの入力を得る
        std::cout << value1 << " " << get_operator_string(op) << " " << value2 << " = ?\n";
        int answer {};
        std::cin >> answer;

        // 入力が正しいか判定する
        if (answer == calc(value1, op, value2)) {
            std::cout << "正解!\n";
        }
        else {
            std::cout << "間違っています。\n";
        }
    }
}

実行結果:

4 + 5 = ?
9  <-- 入力した内容
正解!
2 - 5 = ?
-3  <-- 入力した内容
正解!
5 * 5 = ?
20  <-- 入力した内容
間違っています。
7 / 8 = ?
0  <-- 入力した内容
正解!
2 / 2 = ?
1  <-- 入力した内容
正解!

出題する計算式の整数を生成するための分布生成器と、演算子を生成するための分布生成器を別に用意しています。また、演算子の定義には scoped enum も用いました。

整数演算なので、7 / 8 のような出題があると、答えは 0 になり、小数点以下は捨てられています。

ここでは問題の条件で縛りましたが、もし生成される整数に 0 が含まれる場合は、ゼロ除算の問題に注意しなければなりません。

問題6 (発展★★★)

問題5のプログラムに、同じ5問に再び挑戦できるようにする機能を付け足してください。


同じ5問を再現しなければなりません。そのためには、以前使ったシード値を再び使えばいいということになります。

そこで、ユーザーに今回使ったシード値を教えてやります。ユーザーがその値を指定すれば、同じ5問が出題されるようにします。「シード値」という言葉は技術者向けといえますから、ここでは「問題番号」のような表現に置き換えてやるといいでしょう。

たとえば次のように実現できます。

#include <iostream>
#include <limits>
#include <random>
#include <string>

// 演算子
enum class Operator {
    addition,
    subtraction,
    multiplication,
    division,
};

// 演算子の文字列表現を返す
std::string get_operator_string(Operator op)
{
    switch (op) {
    case Operator::addition:
        return "+";
    case Operator::subtraction:
        return "-";
    case Operator::multiplication:
        return "*";
    case Operator::division:
        return "/";
    }
    return "";
}

// 演算子に応じた計算を行う
int calc(int value1, Operator op, int value2)
{
    switch (op) {
    case Operator::addition:
        return value1 + value2;
    case Operator::subtraction:
        return value1 - value2;
    case Operator::multiplication:
        return value1 * value2;
    case Operator::division:
        return value1 / value2;
    }
    return value1;
}

int main()
{
    constexpr auto QuestionNum = 5;     // 出題数

    // 問題番号か -1 を入力させる
    std::cout << "問題番号を入力してください(ランダムに出題する場合は -1 を入力します)\n";
    int questionNo {};
    std::cin >> questionNo;

    // 入力が -1 の場合は、問題番号をランダムに決める。
    // (実際には -1 以下のときはすべてランダムとする)
    if (questionNo <= -1) {
        std::random_device rand_dev {};
        questionNo = rand_dev() % std::numeric_limits<int>::max();  // int型の範囲に限定する
    }

    std::mt19937 rand_engine(questionNo);
    std::uniform_int_distribution<int> value_dist(1, 10);
    std::uniform_int_distribution<int> op_dist(0, 3);

    for (int i = 0; i < QuestionNum; ++i) {

        // 出題内容を生成する
        int value1 {value_dist(rand_engine)};
        auto op = static_cast<Operator>(op_dist(rand_engine));
        int value2 {value_dist(rand_engine)};

        // 問題を提示して、答えの入力を得る
        std::cout << value1 << " " << get_operator_string(op) << " " << value2 << " = ?\n";
        int answer {};
        std::cin >> answer;

        // 入力が正しいか判定する
        if (answer == calc(value1, op, value2)) {
            std::cout << "正解!\n";
        }
        else {
            std::cout << "間違っています。\n";
        }
    }

    // この問題の番号を出力
    std::cout << "今の問題番号は " << questionNo << "でした。\n";
}

実行結果:

問題番号を入力してください(ランダムに出題する場合は -1 を入力します)
-1  <-- 入力した内容
5 * 1 = ?
5  <-- 入力した内容
正解!
9 * 8 = ?
71  <-- 入力した内容
間違っています。
9 + 9 = ?
18  <-- 入力した内容
正解!
4 * 3 = ?
12  <-- 入力した内容
正解!
8 / 5 = ?
1  <-- 入力した内容
正解!
今の問題番号は 1261640437 でした。

問題番号として -1(実際には -1 以下の整数)が入力された場合は、問題5と同様、ランダムなシード値を使います。

実行結果のように、最後に今回の問題番号が何であったかを出力しておきます。ユーザーはこの番号を保存しておけば、次回その番号を入力することで、同じ5問にチャレンジできます。

実行結果:

問題番号を入力してください(ランダムに出題する場合は -1 を入力します)
1261640437  <-- 入力した内容
5 * 1 = ?
5  <-- 入力した内容
正解!
9 * 8 = ?
72  <-- 入力した内容
正解!
9 + 9 = ?
18  <-- 入力した内容
正解!
4 * 3 = ?
12  <-- 入力した内容
正解!
8 / 5 = ?
1  <-- 入力した内容
正解!
今の問題番号は 1261640437 でした。

問題番号が大きすぎるようなら(とても記憶できなさそうです)、適当な範囲に収めてもいいでしょう。


参考リンク



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る