paizaのレベルアップ問題集「じゃんけんの手の出し方 (paizaランク A 相当)」 をPHPで解きたい


paizaのレベルアップ問題集「じゃんけんの手の出し方 (paizaランク A 相当)」 をPHPで解いてみました。
自力では解けなかったので、ググりまくって考え方を導き出しましたが、PHPで書かれている記事は見当たらなかったため、記事にしました。

実現したいこと

paizaのレベルアップ問題集「じゃんけんの手の出し方 (paizaランク A 相当)」 をPHPで解きたい
https://paiza.jp/works/mondai/skillcheck_sample/janken?language_uid=php

開発環境

OS

% sw_vers
ProductName:    macOS
ProductVersion: 11.1
BuildVersion:   20C69

エディター

ATOM version1.53.0

その他

・MAMP version 6.3(PHPversion 7.4.12)
・ブラウザ:Chrome

考え方

①問題文にある通り、最終的に出力したいのは「最高で何回じゃんけんに勝つことができる」か
②ただし、出す指の数には制約がある
③指の数の制約がなければ、全勝できる
以上三点から、以下の考え方に至りました
A、"千里眼の持ち主"当人の出し手の組み合わせを網羅的に特定
B、出し手の指数が、標準入力から得られる指数と合致するかを判定し、合致していれば後続処理に回す
C、出し手の組み合わせごとに、最大の勝利数を算出(最大の勝利数さえわかれば良いので、出し手の組み合わせがどの順番で出されるかは関係ない)
D、考えうる全ての出しを考慮した上で、最大の勝利数を出力

処理フロー図

凡例

メインフロー図

・最大の勝利数算出方法

解説

さて、本題に入りましょう。
一番下に、私が提出したプログラムは書きますが、考え方も読んでいただければ幸いです。
何かご指摘があれば、やさし〜く教えてください。

下準備

標準入力から得られる情報を変数に詰めておきましょう。
1、対戦数
2、当人の出し手の指数
3、相手の出す手を配列化

$file = fopen("/Applications/MAMP/htdocs/test_data/test_data_p_janken3.txt", "r");

// ファイルの内容を一行ずつ配列に代入します
if($file) {
  while ($line = fgets($file)) {
    $tmp[] = trim($line);
  }
}
$temp_explode = explode(" " ,$tmp[0]);
$battle_number = $temp_explode[0];//対戦数
$finger_number = $temp_explode[1];//指の数の合計
$temp_explode_rival = str_split($tmp[1]);//相手の出す手を配列化

※私はこの問題をlocal環境で解いたため、以下のような書きっぷりをしています。

test_data_p_janken3.txt の中身は以下

3 2
GPC

標準入力のPHPでの取得方法について、詳しくは以下を参照
https://qiita.com/one-kelvin/items/09068b8971c4da509cd3
local環境は以下手順で構築しました
https://qiita.com/pilloty/items/4ad567d3f289470e938c

"千里眼の持ち主"当人の出し手の組み合わせを網羅的に特定

例えば、3回の対戦をするとします。
その場合、出し手の組み合わせは
(パー,チョキ,グー)=(0,0,3)、(0,1,2)、(0,2,1)、(0,3,0)、(1,0,2)、(1,1,1)、(1,2,0)、(2,0,1)、(2,1,0)、(3,0,0)
パーが0の場合だけ見ると、(0,0,3)、(0,1,2)、(0,2,1)、(0,3,0)の4通り
パーが1の場合だけ見ると、(1,0,2)、(1,1,1)、(1,2,0)の3通り
パーが2の場合だけ見ると、(2,0,1)、(2,1,0)の2通り
パーが3の場合だけ見ると、(3,0,0)の1通り
になりますね。
つまり、パーの出し手の組み合わせを網羅的に洗い出すには、”パーを出す回数が0回のパターン"、”パーを出す回数が1回のパターン"、・・・、”パーを出す回数がN回(Nは「"対戦数"」)のパターン"としなければなりません
そのために、「"対戦数"(N)+1」回繰り返し処理を行います。

for($i=0; $i<$battle_number+1; $i++){
  $paper_num = $i; //今回の周回のパーを出す回数
 //後続処理
}

今回の周回でパーを出す回数は”$i”を詰めてあげれば、0、1、2、、、N となっていくつくりです。
便宜上、これを「1次元目の繰り返し処理」とよびます。

これで今回の周回で出す手の組み合わせについて、パーを出す回数が特定できたため、次はそのほかの手を出す回数を特定する処理を入れます。
今回の周回の組み合わせでパーを出す回数が、、、

0回の場合、出し手の組み合わせは「"対戦数"(N)+1」通り
1回の場合、出し手の組み合わせはN通り
・・・
N回の場合、出し手の組み合わせは1通り

となるので、「"対戦数"(N)+1-"今回の周回でパーを出す回数"」回繰り返し処理を行います

for($i=0; $i<$battle_number+1; $i++){
  $paper_num = $i; //今回の周回のパーを出す回数
  for($ii=0; $ii<$battle_number-$i+1; $ii++){
    $scissors_num = $ii; //今回の周回のチョキを出す回数
    $rock_num = $battle_number - $i - $ii; //今回の周回のグーを出す回数
    //後続処理
  }
}

今回の周回でチョキを出す回数は、”$ii”詰めてあげれば、0、1、2、、、M(Mの最大値は「$battle_number」) となっていくつくりです。
今回の周回でグーを出す回数は、”対戦数”-"パーを出す回数"-"チョキを出す回数"としてあげれば、問題ないです。
便宜上、これを2次元目の繰り返し処理とよびます。

これで「1次元目の繰り返し処理」が終われば、自然と全ての出し手の組み合わせについて、網羅的に後続処理を通せることになります。

出し手の指数が、標準入力から得られる指数と合致するかを判定し、合致していれば後続処理に回す

前述しましたが、本問は当人の出し手の指数に制約($finger_number)があります。
そのため、今回の周回の出し手の組み合わせの合計指数が、$finger_number と一致するかを確認する必要があります。
もし一致しなければ、今回の周回は後続の処理を通す必要がないのです。

for($i=0; $i<$battle_number+1; $i++){
  $paper_num = $i; //今回の周回のパーを出す回数
  for($ii=0; $ii<$battle_number-$i+1; $ii++){
    $scissors_num = $ii; //今回の周回のチョキを出す回数
    $rock_num = $battle_number - $i - $ii; //今回の周回のグーを出す回数
    //特定した今回の周回の指数と標準入力から取れる指数が一致しているかを確認
    $finger_number_of_a_w = ($paper_num*5) + ($scissors_num*2);
    if($finger_number == $finger_number_of_a_w){ //指数が入力値と一致する場合のみ後続処理へ
        //後続処理
    }
  }
}

出し手の組み合わせごとに、最大の勝利数を算出

この処理を通すのは、考えうる出し手のうち、標準入力から得られる指数と一致する出し手の場合です。
さて、最終的に出力したいのは、考えうる出し手のうち(標準入力と指数が一致して、かつ)最大の勝利数なので、、、

今回の周回の出し手の組み合わせで可能な最大の勝利数 = 
グーを出して勝てる最大の勝利数(ただし、$rock_numより多くは出せない)+
チョキを出して勝てる最大の勝利数(ただし、$scissors_numより多くは出せない)+
パーを出して勝てる最大の勝利数(ただし、$paper_numより多くは出せない)

というように考えてみます。
さらに、「グーを出して勝てる最大の勝利数(ただし、$rock_numより多くは出せない)」は、相手がチョキを出す回数に依存します。
当人がグーを何回出そうと相手がチョキを1回も出さなけば、グーでは1回も勝てないのです。
つまり、相手がチョキを出す回数より、当人がグーを出す回数が多ければ、グーで勝てるのは「相手がチョキを出した回数」になります。
一方、それ以外の場合(相手がチョキを出す回数が、当人がグーを出す回数以上の場合)は、グーで勝てるのは「当人がグーを出した回数」となるわけです。
これを、チョキ、パーを出す場合にも適用してあげれば、前述の「今回の周回の出し手の組み合わせで可能な最大の勝利数」が算出できるというわけです。
※相手がグー、チョキ、パーを出す回数は「1次元目の繰り返し処理」が始まる前に定義しておきましょう。

 //相手がチョキを出す回数
$rock_win_num_b = count(array_keys($temp_explode_rival, "C"));
//相手がパーを出す回数
$scissors_win_num_b = count(array_keys($temp_explode_rival, "P"));
//相手がグーを出す回数
$paper_win_num_b = count(array_keys($temp_explode_rival, "G"));

for($i=0; $i<$battle_number+1; $i++){
  $paper_num = $i; //今回の周回のパーを出す回数
  for($ii=0; $ii<$battle_number-$i+1; $ii++){
    $scissors_num = $ii; //今回の周回のチョキを出す回数
    $rock_num = $battle_number - $i - $ii; //今回の周回のグーを出す回数
    //特定した今回の周回の指数と標準入力から取れる指数が一致しているかを確認
    $finger_number_of_a_w = ($paper_num*5) + ($scissors_num*2);
    if($finger_number == $finger_number_of_a_w){ //指数が入力値と一致する場合のみ後続処理へ
      if($rock_win_num_b<$rock_num){
        $rock_num_win = $rock_win_num_b; //相手がチョキを出す回数より今回の周回のグーを出す回数が多い場合は、グーで勝てるのは、相手がチョキを出した回数
      }else{
        $rock_num_win = $rock_num; //相手がチョキを出す回数が今回の周回のグーを出す回数以上の場合は、グーで勝てるのは、グーを出した回数
      }
      if($scissors_win_num_b<$scissors_num){
        $scissors_num_win = $scissors_win_num_b; //相手がパーを出す回数より今回の周回のチョキを出す回数が多い場合は、チョキで勝てるのは、相手がパーを出した回数
      }else{
        $scissors_num_win = $scissors_num; //相手がパーを出す回数が今回の周回のチョキを出す回数以上の場合は、チョキで勝てるのは、チョキを出した回数
      }
      if($paper_win_num_b<$paper_num){
        $paper_num_win = $paper_win_num_b; //相手がグーを出す回数より今回の周回のパーを出す回数が多い場合は、パーで勝てるのは、相手がグーを出した回数
      }else{
        $paper_num_win = $paper_num; //相手がグーを出す回数が今回の周回のパーを出す回数以上の場合は、パーで勝てるのは、パーを出した回数
      }
      $win_num_b = $rock_num_win + $scissors_num_win + $paper_num_win; //今回の周回の出し手の組み合わせで勝てる最大勝利数
      // 後続処理
    }
  }
}

考えうる全ての出しを考慮した上で、最大の勝利数を出力

最後に今回の周回の出し手の組み合わせで可能な最大の勝利数($win_num_b)が、出力するべき数字なのかを判定してあげましょう
例えば、5回対戦したとして、当人が(チョキ、チョキ、チョキ、チョキ、チョキ)と出す場合と(パー、パー、グー、グー、グー)と出す場合を考えると、両方指数合計($finger_num)が10ですが、どちらが考えうる最大の勝利数を得られるかは、相手の出し手によります。
前述のロジックで、上記2パターンはどちらも「1次元目の繰り返し処理」が終わるときにはここまでの処理を通っているはずなので、前回の周回までの最大の勝利数($win_num)と、今回の周回の最大の勝利数($win_num_b)を比べて、今回の周回の最大の勝利数が多い場合のみ、$win_num を更新する処理を入れます。
※「1次元目の繰り返し処理」が始まる前に、初期値=0を定義するのを忘れずに、、
「1次元目の繰り返し処理」が終わるときに、$win_num に保持している値が、出力するべき値となるわけです。

<?php
//URL https://paiza.jp/works/mondai/skillcheck_sample/janken?language_uid=php

//複数行の標準入力を配列に詰める
$file = fopen("/Applications/MAMP/htdocs/test_data/test_data_p_janken3.txt", "r");

// ファイルの内容を一行ずつ配列に代入します
if($file) {
  while ($line = fgets($file)) {
    $tmp[] = trim($line);
  }
}
$temp_explode = explode(" " ,$tmp[0]);
$battle_number = $temp_explode[0];//対戦数
$finger_number = $temp_explode[1];//指の数の合計
$temp_explode_rival = str_split($tmp[1]);//相手の出す手を配列化

// 出しての組み合わせを網羅的に特定し、そのグー,チョキ,パーの組み合わせで勝てる最大の勝利数を考える

// $win_num;int
// その回の周回の最大の勝利数が、それまでの周回の勝利数を超えていれば、その値を保持
// その回の周回の最大の勝利数が、それまでの周回の勝利数以下の場合は、値の更新を行わない
$win_num = 0;

 //相手がチョキを出す回数
$rock_win_num_b = count(array_keys($temp_explode_rival, "C"));
//相手がパーを出す回数
$scissors_win_num_b = count(array_keys($temp_explode_rival, "P"));
//相手がグーを出す回数
$paper_win_num_b = count(array_keys($temp_explode_rival, "G"));

for($i=0; $i<$battle_number+1; $i++){
  $paper_num = $i; //今回の周回のパーを出す回数
  for($ii=0; $ii<$battle_number-$i+1; $ii++){
    $scissors_num = $ii; //今回の周回のチョキを出す回数
    $rock_num = $battle_number - $i - $ii; //今回の周回のグーを出す回数
    //特定した今回の周回の指数と標準入力から取れる指数が一致しているかを確認
    $finger_number_of_a_w = ($paper_num*5) + ($scissors_num*2);
    if($finger_number == $finger_number_of_a_w){ //指数が入力値と一致する場合のみ後続処理へ
      if($rock_win_num_b<$rock_num){
        $rock_num_win = $rock_win_num_b; //相手がチョキを出す回数より今回の周回のグーを出す回数が多い場合は、グーで勝てるのは、相手がチョキを出した回数
      }else{
        $rock_num_win = $rock_num; //相手がチョキを出す回数が今回の周回のグーを出す回数以上の場合は、グーで勝てるのは、グーを出した回数
      }
      if($scissors_win_num_b<$scissors_num){
        $scissors_num_win = $scissors_win_num_b; //相手がパーを出す回数より今回の周回のチョキを出す回数が多い場合は、チョキで勝てるのは、相手がパーを出した回数
      }else{
        $scissors_num_win = $scissors_num; //相手がパーを出す回数が今回の周回のチョキを出す回数以上の場合は、チョキで勝てるのは、チョキを出した回数
      }
      if($paper_win_num_b<$paper_num){
        $paper_num_win = $paper_win_num_b; //相手がグーを出す回数より今回の周回のパーを出す回数が多い場合は、パーで勝てるのは、相手がグーを出した回数
      }else{
        $paper_num_win = $paper_num; //相手がグーを出す回数が今回の周回のパーを出す回数以上の場合は、パーで勝てるのは、パーを出した回数
      }
      $win_num_b = $rock_num_win + $scissors_num_win + $paper_num_win; //今回の周回の出し手の組み合わせで勝てる最大勝利数
      if($win_num_b > $win_num){
        $win_num = $win_num_b; //これまでの周回の最大勝利数より、今回の周回の最大勝利数が多い場合は、最大勝利数(=アウトプット)を更新
      }
    }
  }
}
$answer = $win_num;
echo $answer;
?>

結び

以上で、今回の記事については終了です。
頭のいい人の書くコードは、当人の指数が「2の倍数なのか」、「5の倍数なのか」、「2の倍数でもあるし、5の倍数でもあるのか」、「2の倍数でも、5の倍数でもないのか」によって、繰り返し処理の回数を絞っているようですが、私にはそこまで考慮する元気がありませんでした。
システムなぞ、多少重くても動けば良いのです
いろいろな言語で、本問題を解いたネット記事は見つけたのですが、私のかけるPHPで書いていた記事を見つけられなかったので、投稿してみました。
また機会があれば、やってみようとおもいます。