おはやし日記

テーマ……バイク←プログラミング←旅

1024分の1の確率で"mazai"を出力するプログラム

遊んでたら出来たので書く。C言語初学者に重要なエッセンスがあったりなかったりする(知らんけど(責任逃れ))

プログラム

mazaiスロット

解説

#include

  • stdio.h ... いつもの
  • stdlib.h ... 乱数界隈に必要
  • time.h ... 時間界隈

#define

  • pow2(x)
#define pow2(x) ((x)*(x))

xの2乗を返す関数形式マクロ。2のx乗ではない。雑な命名許してクレメンス。

プログラム中のpow2(x)は、xを変数としてすべからく((x)*(x))に変換される。別に

int pow2(int x){
    return x*x;
}

としてもいいのだが無駄だし、いちいち呼び出すほどではない。ということでよく使われる[要出典]関数形式マクロである。

  • pow3(x)
#define pow3(x) ((x)*(x)*(x))

xの3乗を返す関数形式マクロ。

プログラム中のpow3(x)は、xを変数としてすべからく((x)*(x)*(x))に変換される。

  • TRY 10
#define TRY 10

試行回数を定数にしておく。書き換えればいくらでも(?)増やせる。

自作関数 mazai_susiki

char mazai_susiki(int x){
    double a, b, c;
    a = pow3(x)*13/6.0;
    b = pow2(x)*17/2.0;
    c = x*43/3.0;
    return (char)(a-b+c+97);
}

これは単純に言えば、数式

\displaystyle{
y=\frac{13x^3}{6} - \frac{17x^2}{2} + \frac{43x}{3} + 97
}

を示している。xに0から3を入れた時のyの値は以下の表のようになる。さらにそれぞれASCIIコードで変換すると

x 0 1 2 3
y 97 105 109 122
char a i m z

となる。

つまり、xにランダムに0~3を入れるとa, i, m, zが返ってくるわけで、5回やって全てうまくいけばmazai*1を出力することができるということだ。

mazai出力に成功する確率は、(0~3の出かたが「同様に確からしい」として)中学生でもわかるとおり

\displaystyle{
\left( \frac{1}{4} \right) ^5 = \frac{1}{1024}
}

である。

ポイント

(今のところ)この関数で重要なのは、doublea, b, c; a = pow3(x)*13/6.0;である。

ところどころ省くが、C言語の規則として細かい方の型に揃える(雑!)というのがあるので、こう書いておくとdouble型の6.0に引っ張られてaに代入される値はdouble型で計算される。ここを6(.0なし)にしてしまうとすべてint型で処理されるのでaに代入される値もint型の範囲になる。ちなみにa, b, cの計算結果を、double型キャストありとなしで比べると以下のようになった(桁数は環境依存か?)。(a, b, cはdouble型なので代入する値がintでもa, b, cでは.000000がつく)

x 0 1 2 3
double
a (~/6.0) 0.000000 2.166667 17.333333 58.500000
b (~/2.0) 0.000000 8.500000 34.000000 76.500000
c (~/3.0) 0.000000 14.333333 28.666667 43.000000
a-b+c 0.000000 8.000000 12.000000 25.000000
int
a (~/6) 0.000000 2.000000 17.000000 58.000000
b (~/2) 0.000000 8.000000 34.000000 76.000000
c (~/3) 0.000000 14.000000 28.000000 43.000000
a-b+c 0.000000 8.000000 11.000000 25.000000

intで計算してしまうと、いろんなところの値がdoubleで計算した実際の値とずれている(太字)。大体はなんとか誤魔化せているがx=2の時に計算結果がずれてしまう(太字下線)。

書くのを忘れていたのでちょこっと書くが、a, b, cの型はdoubleでなくてはいけない。intで宣言すると、/6と同じことになってしまう(せっかく小数点以下まで計算しても代入する時に切り捨てられてしまうので)。

計算式をこねくり回して切り捨て具合(?)を調整してもいいのだが説得力に欠けるのでやめたほうがいい[要出典]。実際、

int a, b, c;
a = pow3(x)*13/6;
b = pow2(x)*17/2;
c = (x*43+1)/3;

としてもa-b+c = {0, 8, 12, 25}の欲しい結果は出る。

ちなみに

先頭のcharについて。

計算しただけだとdouble型の変数なのだが、この関数はchar型を返すと決めているのでchar型で返さなくてはならないからchar型で返す(小泉進次郎構文)。

プログラムで使う文字は、見た目はaとかbとかの「文字」なのだが、パソコン内部では97とか98という風に数字で処理されている。これを定めているのがASCIIコード表である。半角英数くらいの文字と記号とか(省略)はそこに記述されている。

#include <stdio.h>
int main(){
    char c;
    c = '1';
    printf("%c, %d\n", c, c); //1, 49
    c = 64;
    printf("%c, %d\n", c, c); //@, 64
    c = 'A';
    printf("%c, %d\n", c, c); //A, 65
    c = 97;
    printf("%c, %d\n", c, c); //a, 97
    return 0;
}

こういうのをやると実感できる。

試したところ、数字を返していればmainの中のputchar()で勝手にchar型にキャストするようで、double mazai_susiki()としてdouble型のまま送りつけても怒られなかった。まあ、charと明示しておいたほうが良いのでは(丁寧で良いと思う)。配列を投げたら怒られるだろうけど(試していない)。

returnも最初return (char)(a-b+c+97);とご丁寧にキャストしていたがそれもいらないようだ。return a-b+c+97;でOK。

パワーアップ

勘の良い方はお気づきかと思うが、先ほどのをよくみると、double型で計算したa-b+cの小数点以下は.000000である。今回使っている数式

\displaystyle{
y=\frac{13x^3}{6} - \frac{17x^2}{2} + \frac{43x}{3} (y切片97は省略)
}

は、(0, 0), (1, 8), (2, 12), (3, 25)を通る3次曲線をスマホ関数電卓アプリにはじき出させて得られたものなのだが、実はこいつxにどんな整数を入れてもyは整数になるのだ。例の式を変形すると(また雑な書き方だが)

\displaystyle{
6y=13x^3-51x^2+86x=x(13x^2-51x+86)
}

となる。残念ながらこれ以上因数分解とか出来ないのだが、 x=6k, 6k\pm1, 6k\pm2, 6k+3 (kは整数)を代入すると分かるが右辺が見事に6の倍数になる。(もっとエレガントな方法あったりするんかな?)

というわけで、わざわざdouble型を使わなくても良いのだ。

パワーアップ(というかコンパクト化)した関数mazai_susiki_kai(魔剤数式改)がこちら

char mazai_susiki_kai(int x){
    int ans = (13*pow3(x)-51*pow2(x)+86*x)/6;
    return ans + 97;
}

中身をmazai_susikiととっかえるか、これをプログラムの上の方(main関数よりは上)に書き足してmain関数内のmazai_susikimazai_susiki_kaiに書き換えると良い。これならdouble型の心配をせずにmazai出力チャレンジができる。素晴らしい!なお他の文字を出したい時はそううまくいくとは限らないはずなのでdouble型を使うあたりの話は覚えておいて損はない。

main関数

int main(){
    srand((unsigned)time(NULL));
    int i, j;
    for(i=0; i<TRY; i++){
        for(j=0; j<5; j++){
            putchar( mazai_susiki( rand()%4 ) );
        }
        putchar('\n');
    }
    return 0;
}

ここで#includeが役に立ってくる。

時間界隈

まずはtime.hをインクルードした理由から。それは1行目のtime()関数を使うのに必要なため。詳しくは省略するがtime(NULL)とやると呼び出した時の時間が返ってくる(めっちゃ雑)。

乱数界隈

ここではstdlib.hが使われる。

rand()

これは乱数を生成する関数。どういう仕組みか知らんがrand()を呼び出すたびに乱数を返してくれる。

srand()

上のrand()を使う前に、シード(種)値xsrand(x)として入れる。しかしxが固定だと、rand()呼び出しn回目の値はこれ、という風に定まってしまう。これまた環境依存だと思うが、srand(2020)として実行すると、何回やっても

aimma
mzzaa
mimiz
izzmz
zzizi
aiaaa
mzmzm
zizii
zazia
mzzmi

になってしまった。そこで、xの値を(unsigned)time(NULL)とすることで、プログラムを実行した時間に応じて毎回バラバラのシード値が入るので「面白い」プログラムになる。(unsigned)は型変換。

その他

残りは、ループ回しているだけである。最初に定義した定数TRYを試行回数としてiのループで使っている。

終わり。

まとめ

最初はただバカなプログラムを書いて遊ぶだけのつもりだったが、なんか面白いことに(数式あたり)なったのでブログにまとめてみた。

つかれた。おしまい。

*1:つまり「魔剤」。エナジードリンクの「モンスターエナジー」のことである。

プライバシーポリシー ・お問い合わせはこちら