0502: Dice - 値渡しとアドレス渡し
年末年始の連休が終わり悲しきかな仕事が始まってしまったのでAOJは週一問を目処に進める所存です。ということで今週のAOJ。
問題
ここで使用するサイコロは,上側に 1,南側に 2 があるときは,東側に 3 があるものとする.サイコロの向かいあう面の和は必ず 7 なので,見えない面はそれぞれ北側 5,西側 4,下側 6 になっている.
North,East,South,West の各操作は指示された方向へサイコロを 90 度回転させる. Right,Left の 2 つの操作は上下の面はそのままで水平方向に 90 度回転させる.(回転させる向きに要注意.)
初期配置で上の面に出ている目 1 を初期値とし, 1 回の操作が終わるたびに,上の面に出ている目の数を加算していき,指示にしたがってすべての操作を終えたときの合計値を出力するプログラムを作成しなさい.
入力ファイルの 1 行目には総指示回数 n が書かれていて,続く n 行の各行には「North,East,South,West,Right,Left」のいずれか 1 つの指示が書かれているものとする.ただし, n ≦ 10000 とする.
回答
import java.io.*; class Main{ public static void main(String[] a)throws IOException{ BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String str = null; int orderNum; int sum = 1; // 各指示の移動パターン(数字はサンプルの初期位置の目に対応) int[] defaultDice = {1,2,3,4,5,6}; int[] north = {5,1,3,4,6,2}; int[] east = {3,2,6,1,5,4}; int[] west = {4,2,1,6,5,3}; int[] south = {2,6,3,4,1,5}; int[] Left = {1,3,5,2,4,6}; int[] Right = {1,4,2,5,3,6}; int[] beforeDice = new int[6]; int[] afterDice = new int[6]; while((str = in.readLine()) != null){ if(str.equals("0")){ // 0:処理終了 System.exit(0); }else{ // 命令回数の読み込み orderNum = Integer.parseInt(str); // 初期化 beforeDice = defaultDice; sum = beforeDice[0]; for(int i=0; i<orderNum; i++){ str = in.readLine(); if(str.equals("North")){ for(int j=0; j<6; j++){ afterDice[north[j] - 1] = beforeDice[j]; } } else if(str.equals("East")){ for(int j=0; j<6; j++){ afterDice[east[j] - 1] = beforeDice[j]; } } else if(str.equals("West")){ for(int j=0; j<6; j++){ afterDice[west[j] - 1] = beforeDice[j]; } } else if(str.equals("South")){ for(int j=0; j<6; j++){ afterDice[south[j] -1] = beforeDice[j]; } } else if(str.equals("Left")){ for(int j=0; j<6; j++){ afterDice[Left[j] -1] = beforeDice[j]; } } else if(str.equals("Right")){ for(int j=0; j<6; j++){ afterDice[Right[j] -1] = beforeDice[j]; } } else{ System.out.println("Input Error"); } beforeDice = afterDice; sum += afterDice[0]; } System.out.println(sum); } } } }
方針
各操作でさいころの目がどこに移動するかを配列で定義しておきました。
例)
north = {5,1,3,4,6,2};
・1は5の位置に移動
・2は1の位置に移動
・3は3の位置に移動(固定)
・4は4の位置に移動(固定)
・5は6の位置に移動
・6は2の位置に移動
間違い探し
実は上記のプログラムでは正しい答えを出力しません。
タイトルでお察しの方もいるかと思いますが、
… beforeDice = defaultDice; … beforeDice = afterDice; …
はいここです。
該当の箇所の処理は、処理の区切りごとにサイコロの目の配置を初期状態{1,2,3,4,5,6}に戻す、という想定で処理を書いています。
うまく動作しないのはここが”アドレス渡し”になっているからです。
アドレス渡し
配列で値を取り出すときは、その値がどこに入っているかを示す”アドレス”を参照して値を取り出します。このアドレスを他の変数に代入することを”アドレス渡し”といいます。
beforeDice = defaultDice;
は”beforeDiceのアドレスにdefaultDiceのアドレスを代入する”ということを表しており2つの配列で同じアドレスを使用しているため、どちらかで配列の値を変更するともう一方の配列にも反映されるわけです。
今回defaultDiceの初期状態{1,2,3,4,5,6}は値が変化しないで欲しいのですが、beforeDiceの値をわちゃわちゃいじくってしまっているのでそれに釣られてdefaultDiceの値も変わってしまい、期待通りに動作しませんでした。。。(実は気づくのに時間かかった)
値渡し
配列でもアドレス渡しではなく、値だけを代入する”値渡し”を行う方法はあります。以下のように記述しましょう。
beforeDice = defaultDice.clone();
これでbeforeDiceには値だけが渡されます。beforeDiceとdefaultDiceで別々のアドレスを参照しているので、一方で値を変更してももう一方には反映されません。
余談:String型のswitch文について
switch(str){ case "North": for(int j=0; j<6; j++){ afterDice[north[j] - 1] = beforeDice[j]; } break; case "West": for(int j=0; j<6; j++){ afterDice[west[j] - 1] = beforeDice[j]; } break; ... default: System.out.println("Input Error"); break; }
最初、NorthだとかWestだとかのif文の箇所は上記のようにswitch文で記載していて、開発環境で動作することを確認してから提出を行ったんだけど判定結果はコンパイルエラー。
String型のswitch文はJava SE 7から使えるようになったのですが、AOJの実行環境を確認してみるとJRE 1.6でした。。。以後気をつけなければ!