204 lines
6.1 KiB
Lua
204 lines
6.1 KiB
Lua
----------
|
|
-- SOLVER
|
|
----------
|
|
|
|
local table_concat = table.concat -- local alias
|
|
|
|
local function Solver(root, h, successors)
|
|
local pathlist, pathhash, iters
|
|
-- it is required that "h(node)" returns:
|
|
-- 0 when "is_goal(node)==true"
|
|
-- >0 when "is_goal(node)==false"
|
|
-- (because it allows for some simplification herein)
|
|
local FOUND = 0 -- ie: "is_goal(node)==true"
|
|
local NOT_FOUND = 1e9 -- some number larger than largest possible f
|
|
|
|
local function hash(node)
|
|
return table_concat(node,",")
|
|
end
|
|
|
|
local function search(g, bound)
|
|
iters = iters + 1
|
|
--if ((iters % 1000000) == 0) then print("iterations:", iters) end
|
|
local node = pathlist[#pathlist]
|
|
local h = h(node)
|
|
local f = g + h
|
|
if (f > bound) then return f end
|
|
if (h == FOUND) then return FOUND end
|
|
local min = NOT_FOUND
|
|
for succ, cost in successors(node) do
|
|
local succhash = hash(succ)
|
|
if (not pathhash[succhash]) then
|
|
pathlist[#pathlist+1], pathhash[succhash] = succ, true
|
|
local t = search(g+cost, bound)
|
|
if (t == FOUND) then return FOUND end
|
|
if (t < min) then min = t end
|
|
pathlist[#pathlist], pathhash[succhash] = nil, nil
|
|
end
|
|
end
|
|
return min
|
|
end
|
|
|
|
return {
|
|
solve = function()
|
|
pathlist = { root }
|
|
pathhash = { [hash(root)] = true }
|
|
iters = 0
|
|
local bound = h(root)
|
|
while true do
|
|
bound = search(0, bound)
|
|
if (bound == FOUND) then return bound, iters, pathlist end
|
|
if (bound == NOT_FOUND) then return bound, iters, nil end
|
|
end
|
|
end
|
|
}
|
|
end
|
|
|
|
------------------
|
|
-- DOMAIN SUPPORT
|
|
------------------
|
|
|
|
local i2c = { [0]=0, 1,2,3,4, 1,2,3,4, 1,2,3,4, 1,2,3,4 } -- convert index to column
|
|
local i2r = { [0]=0, 1,1,1,1, 2,2,2,2, 3,3,3,3, 4,4,4,4 } -- convert index to row
|
|
local R, U, L, D = 1, -4, -1, 4 -- move indexing values
|
|
local movenames = { -- move names
|
|
[0]="", [R]="r", [U]="u", [L]="l", [D]="d"
|
|
}
|
|
local succmoves = { -- successor directions
|
|
{R,D}, {R,L,D}, {R,L,D}, {L,D},
|
|
{R,U,D}, {R,U,L,D}, {R,U,L,D}, {U,L,D},
|
|
{R,U,D}, {R,U,L,D}, {R,U,L,D}, {U,L,D},
|
|
{R,U}, {R,U,L}, {R,U,L}, {U,L}
|
|
}
|
|
local manhdists = { -- manhattan distances
|
|
{ [0]=0, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6 },
|
|
{ [0]=0, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4, 4, 3, 4, 5 },
|
|
{ [0]=0, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3, 5, 4, 3, 4 },
|
|
{ [0]=0, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2, 6, 5, 4, 3 },
|
|
{ [0]=0, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5 },
|
|
{ [0]=0, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3, 3, 2, 3, 4 },
|
|
{ [0]=0, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2, 4, 3, 2, 3 },
|
|
{ [0]=0, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1, 5, 4, 3, 2 },
|
|
{ [0]=0, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3, 1, 2, 3, 4 },
|
|
{ [0]=0, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2, 2, 1, 2, 3 },
|
|
{ [0]=0, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1, 3, 2, 1, 2 },
|
|
{ [0]=0, 5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0, 4, 3, 2, 1 },
|
|
{ [0]=0, 3, 4, 5, 6, 2, 3, 4, 5, 1, 2, 3, 4, 0, 1, 2, 3 },
|
|
{ [0]=0, 4, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3, 1, 0, 1, 2 },
|
|
{ [0]=0, 5, 4, 3, 4, 4, 3, 2, 3, 3, 2, 1, 2, 2, 1, 0, 1 },
|
|
{ [0]=0, 6, 5, 4, 3, 5, 4, 3, 2, 4, 3, 2, 1, 3, 2, 1, 0 },
|
|
}
|
|
|
|
--- create a state from a pattern, optionally applying a move
|
|
local function state(patt, move)
|
|
local node = {}
|
|
for k,v in pairs(patt) do node[k] = v end
|
|
if (move) then
|
|
local e = node.e
|
|
local ep = e + move
|
|
node[e], node[ep] = node[ep], 0
|
|
node.e, node.m = ep, move
|
|
end
|
|
return node
|
|
end
|
|
|
|
--- iterator for successors of node
|
|
local function successors(node)
|
|
local moves = succmoves[node.e]
|
|
local i, n = 0, #moves
|
|
return function()
|
|
i = i + 1
|
|
if (i <= n) then
|
|
return state(node, moves[i]), 1
|
|
end
|
|
end
|
|
end
|
|
|
|
--- hueristic estimate of travel cost from node to goal
|
|
local function h(node)
|
|
local sum, ijx, jix, t = 0, 1, 1
|
|
for i = 1, 4 do
|
|
local colmax, rowmax = 0, 0
|
|
for j = 1, 4 do
|
|
t = node[ijx]
|
|
sum = sum + manhdists[ijx][t] -- manhattan
|
|
if (i2r[t] == i) then -- row conflicts
|
|
if (t > rowmax) then rowmax=t else sum=sum+2 end
|
|
end
|
|
t = node[jix]
|
|
if (i2c[t] == i) then -- col conflicts
|
|
if (t > colmax) then colmax=t else sum=sum+2 end
|
|
end
|
|
ijx, jix = ijx+1, jix+4
|
|
end
|
|
jix = jix - 15
|
|
end
|
|
return sum
|
|
end
|
|
|
|
------------------
|
|
-- PRINT SUPPORT:
|
|
------------------
|
|
|
|
local function printnode(node)
|
|
print("+--+--+--+--+")
|
|
for i = 0, 12, 4 do
|
|
print( string.format("|%2d|%2d|%2d|%2d|", node[i+1], node[i+2], node[i+3], node[i+4]) )
|
|
print("+--+--+--+--+")
|
|
end
|
|
end
|
|
|
|
local function printpath(path)
|
|
-- note that #path is 1 longer than solution due to root node at path[1]
|
|
-- concatenated result will be correct length since movenames[root.m]==""
|
|
local t = {}
|
|
for i, node in ipairs(path) do
|
|
t[i] = movenames[node.m]
|
|
end
|
|
local pathstr = table_concat(t)
|
|
print("SOLUTION: " .. pathstr .. " (length: " .. #pathstr .. ")")
|
|
end
|
|
|
|
---------
|
|
-- TASKS:
|
|
---------
|
|
|
|
-- goal is implied by h(), never actually used (see solver's notes)
|
|
-- local goal = state({ 1,2,3,4, 5,6,7,8, 9,10,11,12, 13,14,15,0, e=16, m=0 })
|
|
|
|
do
|
|
print("PRIMARY TASK (OPTIMALLY)")
|
|
local sclock = os.clock()
|
|
local root = state({ 15,14,1,6, 9,11,4,12, 0,10,7,3, 13,8,5,2, e=9, m=0 })
|
|
printnode(root)
|
|
local solver = Solver(root, h, successors)
|
|
local bound, iters, path = solver:solve()
|
|
printpath(path)
|
|
printnode(path[#path])
|
|
print("ITERATIONS: " .. iters)
|
|
print("ELAPSED: " .. (os.clock()-sclock) .. "s")
|
|
end
|
|
|
|
print()
|
|
|
|
do
|
|
print("EXTRA CREDIT TASK (APPROXIMATELY, NON-OPTIMALLY)")
|
|
-- only primary task specifies "fewest possible moves"
|
|
-- extra credit task only specifies "solve", so..
|
|
local sclock = os.clock()
|
|
local root = state({ 0,12,9,13, 15,11,10,14, 3,7,2,5, 4,8,6,1, e=1, m=0 })
|
|
printnode(root)
|
|
local function hec(node)
|
|
-- overweighting h makes it not admissible,
|
|
-- causing solver to favor g when minimizing,
|
|
-- leading to non-optimal (but much easier to find!) solutions
|
|
return h(node)*1.5
|
|
end
|
|
local solver = Solver(root, hec, successors)
|
|
local bound, iters, path = solver:solve()
|
|
printpath(path) --> 86, optimal solution is known to be 80
|
|
printnode(path[#path])
|
|
print("ITERATIONS: " .. iters)
|
|
print("ELAPSED: " .. (os.clock()-sclock) .. "s")
|
|
end
|