平成14年度 春期
             ソフトウェア開発技術者
               午後2 問題

修正履歴 2002/7/6
図2 関数make_pos中;誤「y=y%10」→正「y=y/10」,誤「m=y/10」→正「m=y%10」 
図3 関数sim中;     誤「x=x/!0」→正「x=x/10」,誤「p=x/10」→正「p=x%10」
 

問 1
 ゲームプログラムの作成に関する次の記述を読んで,設問 1〜4 に答えよ。
 人(以下,プレイヤという)とコンピュータが対戦する数当てゲームのプログラ
ムを作ることになった。ゲームのルールは,次のとおりである。

(1)対戦者(プレイヤとプログラム)は,四つの異なる数字からなる 4 けたの数を
 それぞれ決める。ここでは,これを正解と呼ぶことにする。正解の先頭(最上位
 けた)は 0 でも構わない。
(2)相手の正解を交互に推測して,先に当てた方が勝ちとする。同じ推測回数で当
 てたときは引き分けとする。
(3)互いに相手が推測した数を受け取ると,その数と自分の正解を比べ,数字もけ
 たも一致している数字の個数(hit)と,数字は含まれているがけたが違う数字の
 個数(miss)を返す。例えば,正解 1357 に対し 5310 を推測値として受け取ると,
 hit は 1,miss は 2 を返す。

数当てゲームのプログラムの概略は,次のとおりである。
1 乱数を使って正解を決める。
2 どちらかが相手の正解を当てるまで,次を繰り返す。
 2-1 正解との比較処理
     プレイヤの推測値を受け取り,正解と比較し,hit と miss を返す。
 2-2 推測処理
    2-2-1 それまでに受け取った hit と miss を基に次の推測値を決め,
        プレイヤに渡す。
    2-2-2 プレイヤの計算した hit と miss を受け取る。

 以下では,関数の中で定義している変数は局所変数とし,関数の引数は,断りの
ない限り,値渡しとする。配列の添字は 0 から始まるものとし,配列の要素は,0 番
目,1 番目と数える。
 正解を決めるための関数を図 1 に示す。この関数では 0〜9 の数をランダムに一
つずつ生成し,10 個の要素からなる配列tabを用いて数字に重複のない 4 けたの数
を求めている。random は自然数をランダムに生成する関数,% は剰余演算子であり,
配列tabを次のように利用している。

(1)すべての要素の初期値は 0 とする。
(2)ランダムに生成した 1 けたの数 a に対して,tab[a] の値が初期値 0 のままなら,
 a は初めて現れた数なので,tab[a] に 1 を入れる。
(3)tab[a] の値が 1 なら,その値は変更しない。
 

function set_answer()
  int tab[10], n, i, a
  tabの要素をすべて 0 にする
  n = 0
  for(iを 1 から 4 まで)
     repeat
       a = random() % 10
     until(tab[a]が 0 に等しい)
     tab[a] = 1
     n = 【  a  】
  endfor
  return n
endfunction

               図 1 関数Set_answer
 

 次に,hit と miss を計算するアルゴリズムを考える。
 まず,二つの 4 けたの数 x と y の類似度 similarity を,二つの数の hit と
miss を用いて,
         similarity(x, y) = hit * 10 + miss

と定義する。similarity の値と(hit,miss)の値の組は 1 対 1 に対応するので,
プログラムの中では hit と miss の代わりに similarity を使う。
 similarity の計算は,プレイヤの推測値を正解と比較する場合と,プログラム
が推測値を決める場合に利用する。この計算は頻繁に行うので,効率の良い実行方
式を考える。

 実行効率を上げるため,ゲーム中に行う similarity の計算の前に,比較対象に
なる一方の数を配列表現と呼ぶ形式に変換しておく。この変換処理では,対象とす
る数を d4d3d2d1 とすると,配列の di 番目の要素には i を,そのほかの要素には 0を
格納する。数 y を配列表現に変換する関数を図 2 に示す。pos は 10 個の要素からな
る配列で,参照渡しである。
 

function make_pos(y, pos)
  int i,m
  pos の要素をすべて 0 にする
  for(i を 1 から 4 まで)
    m = y % 10
    y = y / 10
    pos[【  b  】] = i
  endfor
endfunction
               図 2 関数make_pos
 

 数 y の配列表現(pos)を利用すると,similarity(x,y)は,図 3 の関数sim
で求めることができる。
 

function sim(x, pos)
  int hit, miss, i, p
  hit = 0
  miss = 0
  for(i を 1 から 4 まで)
    p = x % 10
    x = x / 10
    if(【  c  】)
      if(【  d  】)hit = hit +1
      else miss = miss + 1
      endif
    endif
  endfor
  return hit * 10 + miss
endfunction
               図 3 関数sim
 

 次に推測のアルゴリズムを考える。
 四つの異なる数字からなる 4 けたの数をすべて生成すれば,その中には正解が必
ず含まれるので,それを前もって生成して配列 nums に入れておくことにする。
 プレイヤの正解を当てるために推測値を選ぶアルゴリズムは次のとおりとする。
nums から順次,数を取り出し,それまでに得た similarity の情報に矛盾しない
数を次の推測値とする。
 例えば,3 回目の推測値を選ぶ場合を考える。正解とそれまでの 2 回の推測値との
similarity の値が次のとおりだったとする。
 
 
推測値
similarity
1回目
1234
12
2回目
1345
02
 
 このとき,nums から取り出された次の数が 2304 だったとする。2304 が次の推
測値として適切かどうか計算してみると,similarity(2304, 1234) = 12,
similarity(2304, 1345) = 11 となり,2 回目の結果と矛盾するので,2304 は
次の推測値としては適切でない。
 そこで,更に nums から数を取り出し,その値が 0432 だったとする。0432 が次
の推測値として適切かどうか計算してみると,similarity(0432, 1234) = 12,
similarity(0432, 1345) = 02 で今までの結果と矛盾しない。したがって,
0432 を次の推測値とする。
 これらの考察をまとめると,k 回目の推測値を求める関数は図 4 のようになる。こ
こで,配列 nums は正解の候補の入った配列,guess[i-1] は i 回目の推測値の配
列表現,log[i-1] は i 回目の推測値と正解の similarity の値である。index は,
nums の要素の中で,これより前には正解が存在しない位置を指している添字で,初
期値は -1 である。
 

function make_quess(k)
  int j
  index = index + 1
  for()                 //無限ループ
    j = 0
    while (j<k-1 かつ sim(nums[index], guess[j])が log[j] に等しい)
      【  e  】
    endwhile
    if(j<k-1)
      【  f  】
    else
      return nums[index]
    endif
  endfor
endfunction
             図 4 関数make_guess
 

 先に述べた各関数を使うと,数当てゲームのプログラムは図 5 のようになる。
nums,guess,log,index は大域変数である。
 

int log[M], guess[M][10], nums[N], index  //M,N は配列サイズ
main()
  char r
  nums に,四つの異なる数字からなる 4 けたのすべての数を入れる
  for()                    //ゲームを繰り返す
    p1ay()
    write "ゲームを続けますか? Y/N"
    read r
    if(r が "N"に等しい)          //ゲーム終了をプレイヤが選択
      exit
    endif
  endfor
endmain

function play()
  int ans, pos_a[10], i, r, g, hit, miss, guess_person
  ans = set_answer()
  make_pos(ans, pos_a)
  index = -1
  i = 1
  for()                      //当たるまで繰り返す
    read guess_person              //プレイヤの推測値を読む
    r = sim(guess_person, pos_a)         //similarity を計算
    write r / 10, r % 10            //結果を表示
    g = make_guess(i)              // i 回目の推測値を計算
    Write g                   //推測値を表示
    read hit, miss                //推測の結果を読む
    if(hit が 4 に等しい又は r が 40 に等しい)  //当たったか
       break                 
    endif                    
    log[i-1] =10 * hit + miss          //結果を記録
    make_pos(g, guess[i-1])           //推測値を記録
    i = i+ 1                  
  endfor
  if(【  g  】)                //プレイヤの勝ち
    write "あなたの勝ちです"
  elseif(【  h  】)
    write "私の勝ちです"             //コンピュータの勝ち
  else
    write "引き分けです"             //引き分け
  endif
endfunction
             図 5 数当てゲームのプログラム
 

設問 1
 図 5 のプログラムで,配列 nums の大きさ N は最低幾つでなければならないか。

設間 2
 図 1〜5 中の【  a  】【  h  】に入れる適切な字句を答えよ。

設問 3
 プレイヤがしばらく対戦を進めたところ,プログラムから応答がなくなった。
調査したところ,プレイヤが hit 又は miss の値を間違って返したので,正解
になる候補が見付からないことが原因であった。この場合には,make_guess
が -1 を返すようにしたい。
 図 4 の関数 make_guess にどのような判定を追加すればよいか。20 字以内で
述べよ。

設問 4
 図 5 のプログラムのアルゴリズムでは,プレイヤがコンピュータとの対戦を
繰り返しても,コンピュータは毎回同じ推測を繰り返すので,プレイヤはコン
ピュータが当てにくい数を見付けられるようになる。
 これを回避するために,プログラムで毎回,異なる推測の系列を使いたい。
毎回異なる推測系列を使うには,main から play を呼ぶ直前で配列 nums の値
にある操作を行えばよい。操作内容を 20 字以内で述べよ。

-----------------------------------------------------------------------
解答例(若林研二氏)