CodeIQ:世界は歪んでいる。それでも君は歩む。

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「世界は歪んでいる。それでも君は歩む。」(CodeIQ)を参照してください。

問題の概要

次のようなマップがあります(マップはCodeIQのサイトより)。
map

標準入力から次のようなコマンドが与えられます。
「5R1R2L1」
コマンドは下表のとおりです。
コマンド意味
L反時計回りにおよそ90度向きを変える
R時計回りにおよそ90度向きを変える
1~91〜9 マス、向いている方向にマス目にそって進む

出力は通ったマスの文字を並べたものです。マップの外に出てしまった場合「!」を出力してその後のコマンドを無視してください。

【例】
入力出力
5R1R2L10369cfgdab
3R20369ab
R2L6LL55R5L501258behkheb852!

私のプログラム

Rubyで解答しています。

#!/usr/bin/ruby

# 地図
# 二次元配列で表す
# つながっている部分は'='で示す
MAP = [
	#  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11
	['!', '!', '!', '!', '!', '!', '!', '!', '!', '!', '!', '!'],	# 0
	['!', '0', '3', '6', '9', 'c', 'f', 'i', 'n', 'm', 'l', '!'],	# 1
	['!', '1', '4', '7', 'a', 'd', 'g', 'j', 'q', 'p', 'o', '!'],	# 2
	['!', '2', '5', '8', 'b', 'e', 'h', 'k', 't', 's', 'r', '!'],	# 3
	['!', 'Y', 'Z', '@', '!', '!', '!', '!', '=', '=', '=', '!'],	# 4
	['!', 'V', 'W', 'X', '!', '!', '!', '!', '!', '!', '!', '!'],	# 5
	['!', 'S', 'T', 'U', '!', '!', '!', '!', '!', '!', '!', '!'],	# 6
	['!', 'P', 'Q', 'R', '!', '!', '!', '!', '!', '!', '!', '!'],	# 7
	['!', 'M', 'N', 'O', 'F', 'C', 'z', 'w', '=', '!', '!', '!'],	# 8
	['!', 'J', 'K', 'L', 'E', 'B', 'y', 'v', '=', '!', '!', '!'],	# 9
	['!', 'G', 'H', 'I', 'D', 'A', 'x', 'u', '=', '!', '!', '!'],	# 10
	['!', '!', '!', '!', '!', '!', '!', '!', '!', '!', '!', '!'],	# 11
]

class Position
	def initialize()
		@x = 1
		@y = 1
		@dx = 1
		@dy = 0
		@path = "0"
	end

	# 方向を変える
	def _changeDirection(nxt)
		if nxt == 'R' then
			if    (@dx == 1)  && (@dy == 0)  then
				@dx = 0; @dy = 1
			elsif (@dx == 0)  && (@dy == 1)  then
				@dx = -1; @dy = 0
			elsif (@dx == -1) && (@dy == 0)  then
				@dx = 0; @dy = -1
			elsif (@dx == 0)  && (@dy == -1) then
				@dx = 1; @dy = 0
			end
		elsif nxt == 'L' then
			if    (@dx == 1)  && (@dy == 0)  then
				@dx = 0; @dy = -1
			elsif (@dx == 0)  && (@dy == 1)  then
				@dx = 1; @dy = 0
			elsif (@dx == -1) && (@dy == 0)  then
				@dx = 0; @dy = 1
			elsif (@dx == 0)  && (@dy == -1) then
				@dx = -1; @dy = 0
			end
		else
		end
	end

	# 1歩進む
	# 進んだ位置の文字を返す
	# t,s,r、w,v,uからの移動は特殊なパターンがあるのでそれを処理する
	# t,s,rからyの増加方向に進んだ場合、w,v,uに接続する
	# w,v,uからxの増加方向に進んだ場合、t,s,rに接続する
	# この際、方向も90度変更する
	def _toNext()
		nx = @x + @dx
		ny = @y + @dy

		pos = MAP[ny][nx]
		if pos == '=' then
			case MAP[@y][@x]
			when 't'
				nx = 7; ny = 8; @dx = -1; @dy = 0
			when 's'
				nx = 7; ny = 9; @dx = -1; @dy = 0
			when 'r'
				nx = 7; ny = 10; @dx = -1; @dy = 0
			when 'w'
				nx = 8; ny = 3; @dx = 0; @dy = -1
			when 'v'
				nx = 9; ny = 3; @dx = 0; @dy = -1
			when 'u'
				nx = 10; ny = 3; @dx = 0; @dy = -1
			end
		end
		@x = nx; @y = ny
		return MAP[@y][@x]
	end

	# 指定された数字だけ進む
	# 奈落に落ちたらfalse、そうでなければtrueを返す
	def _forward(nxt)
		for _ in 0...nxt.to_i
			ret = _toNext()
			@path += ret
			if ret == '!' then return false end
		end
		return true
	end

	# 入力値に従って移動する
	# 通ったパスの文字列を返す
	# 奈落に落ちたら終わる
	# input R,L,1~9
	def move(input)
		for nxt in input.chars
			if (nxt == 'R') || (nxt == 'L') then
				_changeDirection(nxt)
			else
				ret = _forward(nxt)
				if ret == false then break end
			end
		end
		return @path
	end

	attr_accessor :x
	attr_accessor :y
end

while line = gets
	line.strip!
	if line.empty? then
		next
	end

	pos = Position.new
	ret = pos.move(line)
	puts ret
end

解説

問題の難易度は★★★ですが、アルゴリズム的に難しいところはありません。
やればできると言うタイプの問題です。

考え方

一番の問題は地図をどうするかということです。
組み込みのデータ型でうまく表現する方法は(多分どんな言語でも)ないでしょう。
私は二次元配列にしておいて別の場所につながるところまできたらそれを例外的に処理するという方法にしました。この方法は単純で解りやすいです。

MAP

地図の定義です。先に書いた通り2次元配列で外周1マス分余分に確保しています。
それぞれの座標の値はマップ上の文字(領域外は'!')ですが、三角形の右下部分は'='を設定し、この文字の場所に来たら対応する場所に座標を書き換え、方向を調整することで三角形のマップに一致させます。

Positionクラス

移動と位置情報を管理するクラスです。
次のメンバを持ちます。

[変数]
x, y: 現在の座標です。
dx, dy: 現在x,yのどちらを向いているかで、それぞれ±1(例外的に初期値として0)をとります。
path: 経路情報です。回答はこれを印字します。

[関数]
move(): 入力をパースして移動を処理します。
_changeDirection(): 入力がRかLの時方向を変えます。
_forward(): 入力が数値の時、数字の回数だけ位置を変更します。
_toNext(): 1歩分の移動を処理します。

move()

入力値を1文字ずつ取り出してRかLなら_changeDirection()、それ以外なら_forward()を呼びます。_forward()は領域外に出た場合falseを返すのでfalseが帰ってきた場合は終了します。
ループが終わったらpathを返します。

_changeDirection()

方向をR、Lに従って時計回り、反時計回りにdx、dyを変更します。

_forward()

引数の数値回_toNext()を呼び出します。
_toNext()は移動するごとにその座標の文字を返すのでそれをpathに加えます。領域外に出た場合、'!'を返すので'!'が帰ってきた場合はfalseを返してループをやめます。
ループが最後までいったらtrueを返します。

_toNext()

現在の座標(x,y)から現在の方向(dx,dy)に1回だけ進め、その座標の文字を返します。
ただし、座標の文字が"="の時は三角形の右下で別の場所につながるのでその処理をします(68行目〜83行目)。これは1対1対応するので固定的に記述してしまっています。

雑感

個人的に好きなタイプの問題です。
私の方法は単純ですが、マップの形が5角形以上になると対応できません。マップが5角形以上になった場合、片ごとにマップを構築するか、1マスを表すクラスを作り、それぞれのマスのつながりを表現する必要があります。