187 lines
5.4 KiB
Python
187 lines
5.4 KiB
Python
import random
|
|
|
|
|
|
class IDAStar:
|
|
def __init__(self, h, neighbours):
|
|
""" Iterative-deepening A* search.
|
|
|
|
h(n) is the heuristic that gives the cost between node n and the goal node. It must be admissable, meaning that h(n) MUST NEVER OVERSTIMATE the true cost. Underestimating is fine.
|
|
|
|
neighbours(n) is an iterable giving a pair (cost, node, descr) for each node neighbouring n
|
|
IN ASCENDING ORDER OF COST. descr is not used in the computation but can be used to
|
|
efficiently store information about the path edges (e.g. up/left/right/down for grids).
|
|
"""
|
|
|
|
self.h = h
|
|
self.neighbours = neighbours
|
|
self.FOUND = object()
|
|
|
|
|
|
def solve(self, root, is_goal, max_cost=None):
|
|
""" Returns the shortest path between the root and a given goal, as well as the total cost.
|
|
If the cost exceeds a given max_cost, the function returns None. If you do not give a
|
|
maximum cost the solver will never return for unsolvable instances."""
|
|
|
|
self.is_goal = is_goal
|
|
self.path = [root]
|
|
self.is_in_path = {root}
|
|
self.path_descrs = []
|
|
self.nodes_evaluated = 0
|
|
|
|
bound = self.h(root)
|
|
|
|
while True:
|
|
t = self._search(0, bound)
|
|
if t is self.FOUND: return self.path, self.path_descrs, bound, self.nodes_evaluated
|
|
if t is None: return None
|
|
bound = t
|
|
|
|
def _search(self, g, bound):
|
|
self.nodes_evaluated += 1
|
|
|
|
node = self.path[-1]
|
|
f = g + self.h(node)
|
|
if f > bound: return f
|
|
if self.is_goal(node): return self.FOUND
|
|
|
|
m = None # Lower bound on cost.
|
|
for cost, n, descr in self.neighbours(node):
|
|
if n in self.is_in_path: continue
|
|
|
|
self.path.append(n)
|
|
self.is_in_path.add(n)
|
|
self.path_descrs.append(descr)
|
|
t = self._search(g + cost, bound)
|
|
|
|
if t == self.FOUND: return self.FOUND
|
|
if m is None or (t is not None and t < m): m = t
|
|
|
|
self.path.pop()
|
|
self.path_descrs.pop()
|
|
self.is_in_path.remove(n)
|
|
|
|
return m
|
|
|
|
|
|
def slide_solved_state(n):
|
|
return tuple(i % (n*n) for i in range(1, n*n+1))
|
|
|
|
def slide_randomize(p, neighbours):
|
|
for _ in range(len(p) ** 2):
|
|
_, p, _ = random.choice(list(neighbours(p)))
|
|
return p
|
|
|
|
def slide_neighbours(n):
|
|
movelist = []
|
|
for gap in range(n*n):
|
|
x, y = gap % n, gap // n
|
|
moves = []
|
|
if x > 0: moves.append(-1) # Move the gap left.
|
|
if x < n-1: moves.append(+1) # Move the gap right.
|
|
if y > 0: moves.append(-n) # Move the gap up.
|
|
if y < n-1: moves.append(+n) # Move the gap down.
|
|
movelist.append(moves)
|
|
|
|
def neighbours(p):
|
|
gap = p.index(0)
|
|
l = list(p)
|
|
|
|
for m in movelist[gap]:
|
|
l[gap] = l[gap + m]
|
|
l[gap + m] = 0
|
|
yield (1, tuple(l), (l[gap], m))
|
|
l[gap + m] = l[gap]
|
|
l[gap] = 0
|
|
|
|
return neighbours
|
|
|
|
def slide_print(p):
|
|
n = int(round(len(p) ** 0.5))
|
|
l = len(str(n*n))
|
|
for i in range(0, len(p), n):
|
|
print(" ".join("{:>{}}".format(x, l) for x in p[i:i+n]))
|
|
|
|
def encode_cfg(cfg, n):
|
|
r = 0
|
|
b = n.bit_length()
|
|
for i in range(len(cfg)):
|
|
r |= cfg[i] << (b*i)
|
|
return r
|
|
|
|
|
|
def gen_wd_table(n):
|
|
goal = [[0] * i + [n] + [0] * (n - 1 - i) for i in range(n)]
|
|
goal[-1][-1] = n - 1
|
|
goal = tuple(sum(goal, []))
|
|
|
|
table = {}
|
|
to_visit = [(goal, 0, n-1)]
|
|
while to_visit:
|
|
cfg, cost, e = to_visit.pop(0)
|
|
enccfg = encode_cfg(cfg, n)
|
|
if enccfg in table: continue
|
|
table[enccfg] = cost
|
|
|
|
for d in [-1, 1]:
|
|
if 0 <= e + d < n:
|
|
for c in range(n):
|
|
if cfg[n*(e+d) + c] > 0:
|
|
ncfg = list(cfg)
|
|
ncfg[n*(e+d) + c] -= 1
|
|
ncfg[n*e + c] += 1
|
|
to_visit.append((tuple(ncfg), cost + 1, e+d))
|
|
|
|
return table
|
|
|
|
def slide_wd(n, goal):
|
|
wd = gen_wd_table(n)
|
|
goals = {i : goal.index(i) for i in goal}
|
|
b = n.bit_length()
|
|
|
|
def h(p):
|
|
ht = 0 # Walking distance between rows.
|
|
vt = 0 # Walking distance between columns.
|
|
d = 0
|
|
for i, c in enumerate(p):
|
|
if c == 0: continue
|
|
g = goals[c]
|
|
xi, yi = i % n, i // n
|
|
xg, yg = g % n, g // n
|
|
ht += 1 << (b*(n*yi+yg))
|
|
vt += 1 << (b*(n*xi+xg))
|
|
|
|
if yg == yi:
|
|
for k in range(i + 1, i - i%n + n): # Until end of row.
|
|
if p[k] and goals[p[k]] // n == yi and goals[p[k]] < g:
|
|
d += 2
|
|
|
|
if xg == xi:
|
|
for k in range(i + n, n * n, n): # Until end of column.
|
|
if p[k] and goals[p[k]] % n == xi and goals[p[k]] < g:
|
|
d += 2
|
|
|
|
d += wd[ht] + wd[vt]
|
|
|
|
return d
|
|
return h
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
solved_state = slide_solved_state(4)
|
|
neighbours = slide_neighbours(4)
|
|
is_goal = lambda p: p == solved_state
|
|
|
|
tests = [
|
|
(15, 14, 1, 6, 9, 11, 4, 12, 0, 10, 7, 3, 13, 8, 5, 2),
|
|
]
|
|
|
|
slide_solver = IDAStar(slide_wd(4, solved_state), neighbours)
|
|
|
|
for p in tests:
|
|
path, moves, cost, num_eval = slide_solver.solve(p, is_goal, 80)
|
|
slide_print(p)
|
|
print(", ".join({-1: "Left", 1: "Right", -4: "Up", 4: "Down"}[move[1]] for move in moves))
|
|
print(cost, num_eval)
|