私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「アタック25に挑戦!」(CodeIQ)を参照してください。
Rubyで解答しています。
#!/usr/bin/ruby
require 'set'
# パネル(周囲1マスを余分に作る)
Pannel = Array.new(7).map{Array.new(7,nil)}
# 現在所有しているパネル
Possession = Hash.new
# 周囲の座標計算用
Around = [[-1,-1], [-1,0], [-1,+1], [0,-1], [0,+1], [+1,-1], [+1,0], [+1,+1]]
# パネル番号を座標に変換する
def pannel2Pos(pannel)
pannel -= 1 # 0始まりにする
pos = [0,0]
pos[0] = (pannel/5).floor + 1 # Y座標
pos[1] = pannel % 5 + 1 # X座標
return pos
end
# targetからdistの方向に反転可能なパネルの数を探索する
def getReverse(color, target, dist)
ret = [] # 間のパネル
pos = pannel2Pos(target)
for i in 1..4
n = [pos[0]+dist[0]*i, pos[1]+dist[1]*i]
# 範囲外チェック
if (n[0] <= 0) || (n[0] > 5) || (n[1] <= 0) || (n[1] > 5) then break end
# 同じ色のパネルまでチェック
if Pannel[n[0]][n[1]] == color then return ret.length # 同じ色
elsif Pannel[n[0]][n[1]] != nil then ret.push(Pannel[n[0]][n[1]]) # 違う色
else return 0 # 空きマス
end
end
# ライン上に同じ色がなかった
return 0
end
# 配置可能な場所を探す
# color 色
# cand 配置候補パネル番号
def searchReverse(color, cand)
ret = []
# 1か所も所有していない場合はどこにでも置ける
# TODO 間違っている(次にはさめる場所にしなければならない)
if Possession[color].length == 0 then return ret end
# 自分以外の色がない場合もどこにでも置ける
others = 0
Possession.each{|key, val|
if key == color then next
else others += val.length
end
}
if others == 0 then return ret end
# 反転可能なパネルがある場所のみを列挙
for c in cand
for a in Around
if getReverse(color, c, a) != 0 then ret.push(c) end
end
end
return ret
end
# 次に反転可能な場所華道家をチェックする
def checkReverseNext(color, target, dist)
pos = pannel2Pos(target)
for i in 1..4
n = [pos[0]+dist[0]*i, pos[1]+dist[1]*i]
# 範囲外チェック
if (n[0] <= 0) || (n[0] > 5) || (n[1] <= 0) || (n[1] > 5) then break end
# 同じ色のパネルまでチェック
if Pannel[n[0]][n[1]] == nil then # 空きマス
# 空きマスで隣り合っている場合はダメ
if i > 1 then return true
else return false
end
elsif Pannel[n[0]][n[1]] == color then # 同じ色
return false
end
end
# ライン上に空いているマスがなかった(おけない)
return false
end
# 次に反転可能な場所を探す
def canReverseNext(color, cand)
ret = Set.new
for c in cand
for a in Around
if checkReverseNext(color, c, a) then ret.add(c) end
end
end
return ret.to_a
end
# 配置可能なパネルを特定する
def searchPosition(color, cand)
# 引っ繰り返せる場所を探す
ret = searchReverse(color, cand)
if !ret.empty? then return ret end
# 引っ繰り返せる場所がない場合、次に引っ繰り返せる場所
ret = canReverseNext(color, cand)
if !ret.empty? then return ret end
# それもなかったら隣接場所のどれでも良い
return cand
end
# 色を設定する
def setColor(color, pannel)
pos = pannel2Pos(pannel)
if Pannel[pos[0]][pos[1]] != nil then
return
else
Pannel[pos[0]][pos[1]] = color
end
end
# 配置可能な場所を抽出する
def search2Set()
ret = Set.new
for pannel in 1..25
pos = pannel2Pos(pannel)
if Pannel[pos[0]][pos[1]] != nil then next end
# 色のついたパネルに隣接していたらセット
for a in Around
if (Pannel[pos[0]+a[0]][pos[1]+a[1]] != nil) then
ret.add(pannel)
break
end
end
end
return ret.to_a
end
def isFirst()
Possession.each_value{|val| if val.length != 0 then return false end}
return true
end
#main
while line = gets
line.strip!
if line.empty? then
next
end
input = line.split(",", 2)
if input.length == 2 then
Possession[input[0]] = input[1].split(",").map {|i| i.to_i}
for i in Possession[input[0]]
setColor(input[0], i.to_i)
end
else
Possession[input[0]] = []
next
end
end
if isFirst() then
Possession.each_key{|color| puts color + ",13"}
else
cand = search2Set()
Possession.each_key {|color|
ret = searchPosition(color, cand)
puts color + "," + ret.join(",")
}
end
この問題は同じ出題者の他の問題と比べて少し変わっています。この出題者の問題は数学的な思考が要求されますがプログラム自体は短くて済むという問題が多いのですが、この問題に関してはルール通りにプログラムを実装すれば解けます。
私の数学能力ではわからないだけで計算だけでできる方法があるのかもしれませんが。
空いているパネルを順番にチェックしてルールに従って取れるパネルの番号を列挙するだけです。結構面倒臭いですが。
入力値は毎の取得済みパネルをPosessionに設定し、setColor()でPanelにも色を設定します。
Panelは7×7で外周をnilにセットし、それ以外の5×5がゲームで取得可能な場所になっています。外周をnilにしているのは領域外のチェックを簡単にするためです。Panelのようそのうち各色で占められている場所には'R'、'B'、'W'、'G'が入ります。
これは最初(つまり誰もパネルを取っていない)状態を判断します。
最初に取得できる場所は13しかないので、最初の場合は「色,13」を出力して終わります。
この関数は1〜25番のパネルを順次チェックし、空きパネル(nilがセットされている)だったら、周囲のパネルをチェックします。そして周囲のパネルのいずれかに色がセットされていたら誰かが次に取れるパネルの候補とします。
最後までチェックしたら候補の集合を返します。
この関数内で使っているnannel2Pos()は1〜25番をPannelの座標に変換する関数です。
引数に色とsearch2Set()で作成したパネル取得候補の番号リストを取り、配置可能な場所を探します。取得可能なパネルは次の優先順位で決定します。
1. 挟んで引っ繰り返せる場所
2. 引っ繰り返せる場所がないなら次に正解した時引っ繰り返せる場所
3. それもなければ取得済みのパネルに隣接する場所
3は引数candそのものです。
1と2はそれぞれsearchReverse()とcanReverseNext()で探します。
まず、探索対象の色が他に1か所もパネルを取っていない場合、この条件で取れるパネルはありません(51行目)。
次に探索対象がすでに取得されているパネルの色全てを独占している場合もこの条件で取れるパネルはありません(54〜60行目)。
そうでないなら候補の場所から上下左右、各斜め方向に探索して自分の色ではさめるかをチェックします。このチェックを行うのがgetReverse()でgetReverse()が1以上を返したパネルの番号を結果として収集して返します。
引数に色(color)、候補のパネル番号(target)、探索方向(dist)をとり、distの方向に1つずつ移動しながら同じ色が出るかをチェックします。distは定数Aroundのどれかになります。
ここで気をつける必要があるのは間に空欄がある場合です。例えば現在の色がRで右方向に探索するとします。「(R),B,_,R,_」のようになっているとした場合(R)の場所には置けません(R,Bはそれぞれの色のパネルで_は空き)。この辺りのチェックを33〜35行目で行っています。
なお、実際の処理ではretにパネルの色を集めていますが、本来は必要ありません。これはデバッグしやすくするためです。
searchReverse()で置ける場所が見つからなかった時(=はさめる場所がなかった)、次もう一度正解したらはさめる場所を探します。 この関数では候補の場所に対してループするだけで探索はcheckReverseNext()で行います。checkReverseNext()でtrueになったパネルを対象として結果に収集します。
基本的にgetReverse()と同じような処理ですが条件が異なります。
引数targetの番号から引数distの方向に探索しますが、探すのが空きパネルになります。そしてtargetの位置から空きパネルまでの間に1つ以上他の色のパネルがなければなりません。この判断を82〜89行目で行っています(81行目のコメントが間違っています)。
CodeIQの問題の中ではかなり長いプログラムになりました。
★★★の問題で相応に難しくはありますが、ルール通りにプログラムすればできるし、ルール通りにやって計算量が大変なことになるわけでもなかったので、どうしようもないという感じは無くやればできるという感じの比較的気楽な問題でした。