できる気がしてきた。

僕の興味のあることを共有します。

剰余とじゃんけん

こんにちは、ta2gchです。

プログラミングを始めるとき、必ずといって良いほど出される課題にジャンケンゲームがあります。

問題(ジャンケンゲーム)

次のような動作をするプログラムを書け。

標準入力から一文字(g,c,p)読み込んで、その一文字をユーザーの手とする。 次に乱数を用いてプログラム側の手を決める。 最後に標準出力に勝者を出力する。

さらに、

  • 入力を間違えると、再入力を促せ
  • 入力文字としてqを受け取った場合は終了せよ

などの派生課題もあり、盛り上がります。

この問題の意図は

  1. if文やwhile文(言語によってはswitch文)などの制御構造の理解
  2. I/Oに関する関数の使い方
  3. 乱数関数などのライブラリの使用

にあります。

ところで、普通にジャンケンゲームのコードを書くと思った以上に勝者判定するルーチンが長くなりがちですよね。

switch(user){
case 'g':
  switch(com){
  case 'g': printf("draw\n");
  case 'c': printf("win\n");
  case 'p': printf("lose\n");
  }
  break;
case 'c':
  switch(com){
  case 'c': printf("draw\n");
  case 'p': printf("win\n");
  case 'g': printf("lose\n");
  }
  break;
case 'p':
  switch(com){
  case 'p': printf("draw\n");
  case 'g': printf("win\n");
  case 'c': printf("lose\n");
  }
  break;
}

こんな感じでしょうか。

実はこの勝敗を判定する部分、1行に収めることができます。

今回は、どうやって一行にまとめるのか紹介します。

結論

まず結論を書いておきましょう。

  • user ユーザーの手
  • com プログラムの手
(user - com + 3) % 3

この値で勝敗を判定できます。 なぜこうなるのか、以下で説明します。

予備知識

剰余

「剰余」という言葉をご存知でしょうか?いわゆる「余りの数」というものです。

例えば 「5÷3」ならば

5÷ 3 = 1あまり2

となり、この時の剰余は2となります。

これをC言語では「%」という演算子を用いることで計算できます。

printf("%d\n",5%3); // 2と表示されます。

ちなみに「-5÷ 3」の場合は、

-5÷3=-2あまり1

と書け、剰余は1となります。(他にも定義の仕方はあります)

合同式

ある数aとある数bについてcで割った時、あまりが等しいときならば

a≡b (mod c)

と書き、これを合同式と呼びます。

より具体的には

1≡4(mod 3)

が成り立ちますね。(どちらも3で割ると1余るから)

数学とプログラミング

プログラミングでは、数学的知識を使うことで課題解決を大幅に短縮できることがたくさんあります。 例えば、1~100までの数の総和を求めるプログラムを書く場合、

int i,sum;
sum = 0;
for(i = 1;i <= 100 ; i++) {
sum += i;
}

と書くこともできますが、総和の公式を知っていれば

int n = 100;
int sum = 1/2 * n * (n + 1);

とかけ、100回の足し算が「1回の割り算と2回の掛け算、1回の足し算」だけで計算できるようになりました。

このように数学的知識をプログラミングに応用することで劇的な改善が望める場合があることを覚えておいてください。

では、ジャンケンの場合はどうすれば良いのでしょう?

本題

まず、入力が文字です。g,c,hでは四則演算できませんね。 そこで、とりあえずg,c,hに適当な数字を割り当ててみます。

  • 'g' ならば 1
  • 'c' ならば 2
  • 'p' ならば 3

まず引き分けの判定はすぐに思いつくかと思います。

if(user - com == 0) {
  printf("draw\n");
}

なぜなら、ユーザーとプログラムの手は同じなのでその値は同じ値(1か2か3)をとるはずですので、その差は0となります。

引き分け以外の時はどうでしょう。 ユーザーからプログラムの手を引き算した結果を下の表にまとめました。

user-com グー チョキ パー
グー 0 -1 -2
チョキ 1 0 -1
パー 2 1 0

0以外には一見周期がないように見えますが、

  • -1 ≡2 (mod 3)
  • -2 ≡1 (mod 3)

であることに注意して改めて表の値を3で割った時の剰余として見てみましょう

user-com グー チョキ パー
グー 0 2 1
チョキ 1 0 2
パー 2 1 0

なんと、

  • ユーザーが勝った場合は 2
  • プログラムが勝った場合は1

となっています!

したがって、ジャンケン勝敗判定は

「(user-com)%3の結果で求められる」 がわかりました!

しかし!

しかし、C言語ではこれではうまくいきません。 なぜならC言語の剰余計算は「絶対値が0に近い」値を返すという性質があり、つまり

-abs(a)%b 

負の値の場合はこのように計算してるようです。なので-2%3 の値は -2のままとなってしまいます。

困りましたね…

そこで使われるのが

-2 + 3 ≡ -2 (mod 3)

という、割る数を足しても剰余は変わらない性質です。

user - comは -2 から 2までの値しか取らないので3を加えることで 正の値となることに注意すれば、

(-2 + 3) % 3 は1 となり計算したい値が求められます!

ようやく求めたい式がC言語用につくれました。

int result = (use - com + 3) % 3; ///ジャンケン勝敗の判定

switch(result) {
case 0: print("draw\n");
case 1: print("lose\n");
case 2: print("win\n");

以上でジャンケンの判定を一行で計算できましたね!

これで、ふとジャンケン相手がほしたくなったときにすぐに作れます。

それでは。