私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「魔方陣で最大値?」(CodeIQ)を参照してください。
4×4の魔方陣は1~16までの数字を1度ずつ使ったもので、以下の左図のようなものがあります。
魔方陣は縦、横、斜めのすべての列について、その和が等しいという特徴があります。
魔方陣において、左上のマスからスタートして、右か下の隣り合うマスへの移動を繰り返して最短距離で右下のマスまで移動します。
このとき、経由したマスの数の和が最大になるような移動経路を考えます。
上図の魔方陣の場合、上図右のように移動すると和は 67 になります。
標準入力から整数 n が与えられたとき、4×4のすべての魔方陣に対してこのような移動経路を求め、その和が n になる魔方陣の個数を求め、標準出力に出力してください。
4×4の魔方陣は全部で7,040個存在することが知られています。
その中で、n=54のときは以下の2パターンに回転や鏡像を含めたものが全部で8通りありますので、以下のような入出力になります。
【入出力サンプル】
標準入力
54
標準出力
8
Rubyで解答しています。
#!/usr/bin/ruby
def setUsed(used, arr, val)
for a in arr
used[a] = val
end
return used
end
def isDup(x, used, others)
if used[x] then return true end
if others.include?(x) then return true end
return false
end
# 次のサイトから移植
# http://www.daido-it.ac.jp/~oishi/TH5/ms4/ms4prg.html
def makeMagicSquare()
ret = []
used = Array.new(17,false)
for a in 1..13 # aのループ
used[a]=true
for d in (a+1)..15 # dのループ
used[d]=true
for m in (d+1)..16 # mのループ
q=34-a-d-m # qを計算
if q<a then break end # 回転禁止条件より
if q>16 then next end # 16以下であること
if isDup(q, used, [m]) then next end # 重複チェック
used=setUsed(used, [m,q], true) # 使用済みのフラグを立てる
for f in 1..16 # fのループ
if used[f] then next end # fが既に使用されていないか
k=34-a-f-q # kを計算
if k<1 then break end # 1以上であること(kはこの先減る)
if k>16 then next end # 16以下であること
if isDup(k, used, [f]) then next end # 重複チェック
used=setUsed(used, [f,k], true) # 使用済みのフラグを立てる
for b in 1..16 # bのループ
if used[b] then next end
c=34-a-b-d
if c<1 then break end
if c>16 then next end
if isDup(c, used, [b]) then next end
used=setUsed(used, [b,c], true)
for g in 1..16 # gのループ
if used[g] then next end
j=34-d-g-m
if j<1 then break end
if j>16 then next end
if isDup(j, used, [g]) then next end
o=34-c-g-k
if o<1 then break end
if o>16 then next end
if isDup(o, used, [j,g]) then next end
n=34-b-f-j
if n>16 then break end
if n<1 then next end
if isDup(n, used, [j,g,o]) then next end
used=setUsed(used, [g,j,o,n], true)
for e in 1..16 # eのループ
if used[e] then next end
h=34-e-f-g
if h<1 then break end
if h>16 then next end
if isDup(h, used, [e]) then next end
i=34-a-e-m
if i<1 then break end
if i>16 then next end
if isDup(i, used, [e,h]) then next end
l=34-d-h-q
if l>16 then break end
if l<1 then next end
if isDup(l, used, [e,h,i]) then next end
ret << [[a,b,c,d],[e,f,g,h],[i,j,k,l],[m,n,o,q]]
end
used=setUsed(used, [g,j,o,n], false) # フラグを降ろす
end
used=setUsed(used, [b,c], false) # フラグを降ろす
end
used=setUsed(used, [f,k], false) # フラグを降ろす
end
used=setUsed(used, [m,q], false) # フラグを降ろす
end
used[d]=false
end
used[a]=false
end
return ret
end
# ダイクストラ法で経路探索
def getMaxPath(map)
memo = Array.new(4){Array.new(4, 0)}
directions = [[0,1], [1,0]]
queue = [[0,0]]
memo[0][0] = map[0][0]
while !queue.empty?
cur = queue.shift
v = memo[cur[0]][cur[1]]
for d in directions
nxt = [cur[0]+d[0], cur[1]+d[1]]
if (nxt[0]<0) || (nxt[1]<0) || (4<=nxt[0]) || (4<=nxt[1]) then next end
nv = v + map[nxt[0]][nxt[1]]
if nv > memo[nxt[0]][nxt[1]] then
memo[nxt[0]][nxt[1]] = nv
queue << nxt
end
end
queue.uniq!
end
return memo[3][3]
end
# mapを回転する
def rotateMap(map)
nxt = Array.new(4){Array.new(4, 0)}
for i in 0...4
for j in 0...4
nxt[j][3-i] = map[i][j]
end
end
return nxt
end
# mapを上下鏡像にする
def mirrorMap(map)
nxt = Array.new(4){Array.new(4, 0)}
for i in 0...4
for j in 0...4
nxt[3-i][j] = map[i][j]
end
end
return nxt
end
# 魔方陣の全パターンの最長経路を求める
def makeCountList()
ret = Hash.new(0)
maps = makeMagicSquare()
for map in maps
valiants = [map]
for _ in 0...3
valiants << rotateMap(valiants.last)
end
valiants << mirrorMap(map)
for _ in 0...3
valiants << rotateMap(valiants.last)
end
for v in valiants
c = getMaxPath(v)
ret[c] += 1
end
end
return ret
end
# main
counts = makeCountList()
while line = gets
line.strip!
if line.empty? then next end
p counts[line.to_i]
end
私のプログラムは愚直にやっていますが、もっと効率の良い方法があるのでしょうか?
考え方は非常に単純で、魔方陣の全てのパターンを求めてからダイクストラ法で経路を計算して入力値の値になるものを数えるだけです。
問題は魔方陣の求め方です。
私はWebで検索して見つけたプログラムをRubyで書き直しただけですが、この方法の考え方は難しくありません。
4隅と中央の4マス、縦横と斜めは合計値が34になるという性質を利用して適当に値を設定し、計算できるところは計算するという方法でやっています。
プログラムの説明の方で詳しくやります。
makeCountList()で全ての魔方陣の経路を求め、経路の値毎に何パターンあるかをcountsに保持します。
countsは連想配列なので入力値の値の数を印字します。
全ての魔方陣の経路を求め、経路の値毎に何パターンあるかを記録した廉造配列を返します。
makeMagicSquare()で回転や鏡像を排除した魔方陣のパターンを生成します。
その1つずつについてrotateMap()で90〜270度回転したもの、鏡像、鏡像を90〜270度回転したものを生成し、その全てについてgetMaxCount()で経路の値を求めます。
経路の値をキーとし、その値になる経路の数を廉造配列に記録し、返却します。
| a | b | c | d |
| e | f | g | h |
| i | j | k | l |
| m | n | o | q |
| a | b | c | d |
| e | f | g | h |
| i | j | k | l |
| m | n | o | q |
| a | b | c | d |
| e | f | g | h |
| i | j | k | l |
| m | n | o | q |
| a | b | c | d |
| e | f | g | h |
| i | j | k | l |
| m | n | o | q |
| a | b | c | d |
| e | f | g | h |
| i | j | k | l |
| m | n | o | q |
| a | b | c | d |
| e | f | g | h |
| i | j | k | l |
| m | n | o | q |
プログラム中ではa〜qの変数で各マスの値、usedですでに使用済みの値を管理しています。候補値が確定する毎にusedの添え字番号の値をtrueにし、ループを抜ける毎に当該箇所をfalseに戻します。
setUsed()はusedの添え字番号に一致する値をtrueかfalseにセットする関数です。
isDup()は重複チェックを行う関数で、xが候補値、usedが使用済みの値管理テーブル、othersはusedに記録されていないけれど、計算などで求まった候補値です。
この2つの関数は単純なので解説しません。
rotateMap()は2次元配列を90度回転したものに、mirrorMap()は上下方向の鏡像に変換します。いずれも単純な座標の入れ替えなのでコードを見てください。
引数mapに基づいて問題通りに最大の値になる経路を探索します。
ダイクストラ法で右か下方向に値を辿り、そのマスに到達した時に最も大きな値になるものだけを保持して、最終的に右下の値を返すだけです。
私のやり方は完全な総当たりですがもっと効率の良い方法がある気はします。
その上、魔方陣を解く関数のネストがヒドく、ちょっとどうかと思います(人のコードを移植しておいてアレですが)。
私は魔方陣を回転、鏡像を除いて列挙した上で回転、鏡像を作成しています。多分、一気に全て列挙するよりもこちらの方が速いと思うのですがどうでしょう?