在课程给的Template的基础上完成的,通过所有test, 100/100. Challenge题移动步数长度为200.
教训回顾:
感觉算法并不像之前几周的课的那么有趣,比较像魔方的那种算法,写的过程比较痛苦。过程中因为对各个invariant的输入输出理解有偏差,导致最后代码大片地需要修改。(没有听老师在video里说的,先用OwlTest测试invariant通过之后再implement解题的methods。结果果然吃了大亏!写的Test Suite也都没用了,后面自己测试也白白花了很多时间)assert 语句在整个过程中发挥比较大作用,通过assert的条件判断,可以很快发现出问题的位置,减少错误检查的时间。
其他同学的优质Implementation:
http://www.codeskulptor.org/#user41_WfdDiserIYgcq0y.py by Yoshimi Kuruma
改进:
1. left-right, right-left, up-down, down-up的组和可以精简,进而减少步数。Kuruma是递归地去除字符串中的这些组和(因为每次替代后有可能又产生新的可精简组和)。
def move_str_trimmer(self, move_str):
"""
Trim redundant move
"""
move_str2 = move_str
move_str = move_str.replace('ud', '').replace('du', '').replace('lr', '').replace('rl', '')
if move_str == move_str2:
return move_str2
else:
return self.move_str_trimmer(move_str)
2. 各阶段的solve方法, 可以统一用一个函数,生成移动的字符串组和。
def position_tile(self, target_row, target_col, current_row, current_col):
"""
helper function based on Homework Q2 and Q8
return: move string
"""
move_str = ''
druld = 'druld'
row_diff = target_row - current_row
col_diff = target_col - current_col
move_str += row_diff * 'u'
if col_diff == 0:
move_str += 'ld' + (row_diff - 1) * druld
elif col_diff > 0:
move_str += col_diff * 'l'
if current_row == 0:
move_str += (abs(col_diff) - 1) * 'drrul'
else:
move_str += (abs(col_diff) - 1) * 'urrdl'
move_str += row_diff * druld
elif col_diff < 0:
move_str += (abs(col_diff) - 1) * 'r'
if current_row == 0:
move_str += abs(col_diff) * 'rdllu'
else:
move_str += abs(col_diff) * 'rulld'
move_str += row_diff * druld
move_str = self.move_str_trimmer(move_str)
return move_str
我的Implementation:
"""
Loyd's Fifteen puzzle - solver and visualizer
Note that solved configuration has the blank (zero) tile in upper left
Use the arrows key to swap this tile with its neighbors
"""
# import poc_fifteen_gui
class Puzzle:
"""
Class representation for the Fifteen puzzle
"""
def __init__(self, puzzle_height, puzzle_width, initial_grid=None):
"""
Initialize puzzle with default height and width
Returns a Puzzle object
"""
self._height = puzzle_height
self._width = puzzle_width
self._grid = [[col + puzzle_width * row
for col in range(self._width)]
for row in range(self._height)]
if initial_grid != None:
for row in range(puzzle_height):
for col in range(puzzle_width):
self._grid[row][col] = initial_grid[row][col]
def __str__(self):
"""
Generate string representaion for puzzle
Returns a string
"""
ans = ""
for row in range(self._height):
ans += str(self._grid[row])
ans += "\n"
return ans
#####################################
# GUI methods
def get_height(self):
"""
Getter for puzzle height
Returns an integer
"""
return self._height
def get_width(self):
"""
Getter for puzzle width
Returns an integer
"""
return self._width
def get_number(self, row, col):
"""
Getter for the number at tile position pos
Returns an integer
"""
return self._grid[row][col]
def set_number(self, row, col, value):
"""
Setter for the number at tile position pos
"""
self._grid[row][col] = value
def clone(self):
"""
Make a copy of the puzzle to update during solving
Returns a Puzzle object
"""
new_puzzle = Puzzle(self._height, self._width, self._grid)
return new_puzzle
########################################################
# Core puzzle methods
def current_position(self, solved_row, solved_col):
"""
Locate the current position of the tile that will be at
position (solved_row, solved_col) when the puzzle is solved
Returns a tuple of two integers
"""
solved_value = (solved_col + self._width * solved_row)
for row in range(self._height):
for col in range(self._width):
if self._grid[row][col] == solved_value:
return (row, col)
assert False, "Value " + str(solved_value) + " not found"
def update_puzzle(self, move_string):
"""
Updates the puzzle state based on the provided move string
"""
zero_row, zero_col = self.current_position(0, 0)
for direction in move_string:
if direction == "l":
assert zero_col > 0, "move off grid: " + direction
self._grid[zero_row][zero_col] = self._grid[zero_row][zero_col - 1]
self._grid[zero_row][zero_col - 1] = 0
zero_col -= 1
elif direction == "r":
assert zero_col < self._width - 1, "move off grid: " + direction
self._grid[zero_row][zero_col] = self._grid[zero_row][zero_col + 1]
self._grid[zero_row][zero_col + 1] = 0
zero_col += 1
elif direction == "u":
assert zero_row > 0, "move off grid: " + direction
self._grid[zero_row][zero_col] = self._grid[zero_row - 1][zero_col]
self._grid[zero_row - 1][zero_col] = 0
zero_row -= 1
elif direction == "d":
assert zero_row < self._height - 1, "move off grid: " + direction
self._grid[zero_row][zero_col] = self._grid[zero_row + 1][zero_col]
self._grid[zero_row + 1][zero_col] = 0
zero_row += 1
else:
assert False, "invalid direction: " + direction
##################################################################
# Phase one methods
def lower_row_invariant(self, target_row, target_col):
"""
Check whether the puzzle satisfies the specified invariant
at the given position in the bottom rows of the puzzle (target_row > 1)
Returns a boolean
"""
# Check if all cell on the right satisfied the conditions
for col in range(target_col + 1, self.get_width()):
if self.current_position(target_row, col) != (target_row, col):
return False
# Check if all cell below the target row satisfy the conditions
for row in range(target_row + 1, self.get_height()):
for col in range(self.get_width()):
if self.current_position(row, col) != (row, col):
return False
# Check if tile 0, satisfies the condition
if self.current_position(0, 0) != (target_row, target_col):
return False
# if all cells satisfy the conditions
return True
def solve_interior_tile(self, target_row, target_col):
"""
Place correct tile at target position
Updates puzzle and returns a move string
"""
# Assume Target Cell in (s, t)
cur_row, cur_col = self.current_position(target_row, target_col)
steps_h = abs(target_col - cur_col)
steps_v = abs(target_row - cur_row)
move = ""
# Case 1: Directly Above (i,j) -> Move target cell Down
# (s, t = j) -> (i, t = j)
if steps_h == 0 and steps_v != 0:
# Move up and across the target tile
move += 'u' * steps_v
steps_v -= 1
# Move it down cyclically
move += 'lddru' * steps_v
steps_v -= steps_v
# Move 0 tile to the initial position of next solve
move += 'ld'
# Case 2: Above (i, j) but not in same column
if steps_h != 0 and steps_v != 0:
# Move tile 0 to the current postion of target tile, UP -> Left/Right
# 1: Move tile 0 across target tile(horizontally)
# 2: Move target tile to the position above target position
move += 'u' * steps_v
if target_col > cur_col: # Target tile : rightward
move += 'l' * steps_h
steps_h -= 1
# Move target tile to the position directly above
# if target tile is in row 0, move it down 1 step
if cur_row == 0:
move += 'druld'
steps_v -= 1
# move target tile right cyclically
move += steps_h * 'urrdl'
steps_h -= steps_h
move += 'dru'
steps_v -= 1
assert steps_h == 0
else: # Target tile : leftward
move += 'r' * steps_h
steps_h -= 1
if cur_row == 0:
move += 'dlurd'
steps_v -= 1
move += steps_h * 'ulldr'
steps_h -= steps_h
# tile 0 cannot move into the solved tiles
if steps_v == 1:
move += 'ullddru'
else:
move += 'dlu'
steps_v -= 1
assert steps_h == 0
# 3: Move it downward to target cell cyclically
move += 'lddru' * steps_v
steps_v -= steps_v
# 4: Move 0 tile to the initial position of next solve
move += 'ld'
# Case 3: On the left of (i, j) -> Move left
# (s, t = j) -> (i, t = j)
if steps_h != 0 and steps_v == 0:
# 1: move leftward across target tile
move += 'l' * steps_h
steps_h -= 1
# 2: move target tile rightward,cyclically
move += 'urrdl' * steps_h
steps_h -= steps_h
# Check and return
assert steps_h == 0
assert steps_v == 0
self.update_puzzle(move)
return move
def solve_col0_tile(self, target_row):
"""
Solve tile in column zero on specified row (> 1)
Updates puzzle and returns a move string
"""
# print 'Enter Method'
move = ""
cur_row, cur_col = self.current_position(target_row, 0)
steps_h = cur_col
steps_v = abs(target_row - cur_row)
# Case 0 A: target tile is 1 step above
if steps_h == 0 and steps_v == 1:
move += "u" + (self.get_width() - 1) * 'r'
self.update_puzzle(move)
return move
# Case 0 B: if target tile is right and up of target position, move the target tile rightward for 1 step
if steps_h == 1 and steps_v == 1:
move += 'uurrdlld'
steps_h += 1
# Case 1: Target cell: Right and above
if steps_h != 0 and steps_v != 0:
# 1: move A cell leftward & move across target cell
move += 'rul' + 'u' * (steps_v - 1) + steps_h * 'r'
steps_h -= 1
# 2 A&B: move target cell leftward to column 0
if cur_row == 0:
move += 'dllur' * steps_h
else:
move += 'ulldr' * steps_h
steps_h -= steps_h
# 3: move vertically down, cyclically
if steps_v == 1:
pass
else:
move += 'dlu' + (steps_v - 2) * 'rddlu' + 'rd'
steps_v -= steps_v - 1
# Case 2: Target cell: Above directly by > 1 steps
elif steps_h == 0 and steps_h == 0: # Case 2: Target cell: Above
# 1: move A cell leftward & move across target cell
move += 'rul' + 'u' * (steps_v - 1)
steps_v -= 1
# 2: move vertically down, cyclically
move += (steps_v - 1) * 'rddlu' + 'rd'
steps_v -= steps_v - 1
else:
raise NotImplementedError, "Case Not Implemented!"
# 4: move target cell & its next cell within this 2x2 field
move += 'dlu'
steps_v -= 1
# move tile 0 to inital postion of next step
move += (self.get_width() - 1) * 'r'
assert steps_h == 0
assert steps_v == 0, steps_v
self.update_puzzle(move)
return move
#############################################################
# Phase two methods
def row0_invariant(self, target_col):
"""
Check whether the puzzle satisfies the row zero invariant
at the given column (col > 1)
Returns a boolean
target_col: column of target position of the tile to be solved
"""
# Check All cells on the right
for col in range(target_col + 1, self.get_width()):
for row in [0, 1]:
if self.current_position(row, col) != (row, col):
print '0'
return False
# Check the cell (1, target_col)
if self.current_position(1, target_col) != (1, target_col):
print '1'
return False
# Check Tile 0
if self.current_position(0, 0) != (0, target_col):
print '2'
return False
# Check tiles in lower rows
for row in range(2, self.get_height()):
for col in range(self.get_width()):
if self.current_position(row, col) != (row, col):
print '3'
return False
# Otherwise
return True
def row1_invariant(self, target_col):
"""
Check whether the puzzle satisfies the row one invariant
at the given column (col > 1)
Returns a boolean
"""
# Check cells in the cells on the right
for col in range(target_col + 1, self.get_width()):
for row in [0, 1]:
if self.current_position(row, col) != (row, col):
return False
# Check Tile 0 at (1, target_col)
if self.current_position(0, 0) != (1, target_col):
return False
# Check tiles in lower rows
for row in range(2, self.get_height()):
for col in range(self.get_width()):
if self.current_position(row, col) != (row, col):
return False
# Otherwise
return True
def solve_row0_tile(self, target_col):
"""
Solve the tile in row zero at the specified column
Updates puzzle and returns a move string
target_col: column of target position of the tile to be solved
"""
cur_row, cur_col = self.current_position(0, target_col)
steps_h = target_col - cur_col
steps_v = cur_row - 0
move = ""
# print 'cur', cur_row, cur_col
# print 'target', target_col
# print steps_h, steps_v
# Case 0: 1 step left
if steps_h == 1 and steps_v == 0:
move += 'ld'
steps_h -= 1
# Case 1: Left, >1 steps
elif steps_v == 0 and steps_h != 0:
# 1: Move 7 upward
move += 'd'
# Across to the current postion of target cell
move += 'lu' + 'l' * (steps_h - 1)
steps_h -= 1
# Move rightwards
move += 'drrul' * (steps_h - 1)
steps_h -= steps_h - 1
# Move within 2x2 cells
move += 'drrul'
steps_h -= 1
# Tile 0 to final position
move += 'd'
# Case 2: Row 1
elif steps_v != 0 and steps_h > 1:
# 1: Move 7 upward
move += 'd'
# Across to the current postion of target cell
move += 'l' * steps_h
steps_h -= 1
# Move rightwards
move += 'urrdl' * (steps_h - 1)
steps_h -= steps_h - 1
# Move up
move += 'urd'
steps_v -= 1
# Move within 2x2 cells
move += 'rul'
steps_h -= 1
# Tile 0 to final position
move += 'd'
elif steps_v != 0 and steps_h == 1: # Case 3: Row 1, diagonal to tile 0
# Move target tile leftward for 1 step and move tile 0 back to original position
move += 'lldrur'
steps_h += 1
# Now it's case 2 - 2 step
game_clone = self.clone()
game_clone.update_puzzle(move)
move += game_clone.solve_row0_tile(target_col)
steps_h -= steps_h
steps_v -= 1
else:
raise NotImplementedError('Case not implemented')
assert steps_h == 0, steps_h
assert steps_v == 0, steps_v
self.update_puzzle(move)
return move
def solve_row1_tile(self, target_col):
"""
Solve the tile in row one at the specified column
Updates puzzle and returns a move string
"""
cur_row, cur_col = self.current_position(1, target_col)
steps_h = target_col - cur_col
steps_v = 1 - cur_row
move = ""
# Case 1: Directly Above
if steps_h == 0:
# 1: Move Across the currernt postion of target cell. Then, it's done.
move += "u" * steps_v
steps_v -= 1
# Case 2: On the left of target position
elif steps_v == 0:
# 1: Move Across the current position of target cell.
move += 'l' * steps_h
steps_h -= 1
# 2: Move the target tile rightward
move += 'urrdl' * steps_h
steps_h -= steps_h
# 3: Move the tile 0 to the next position to solve
move += 'ur'
# Case 3: In row 0 and on the left
elif steps_v == 1 and steps_h != 0:
# 1: Move Across the current position of target cell.
move += 'u' * steps_v + 'l' * steps_h
steps_h -= 1
# 2: Move target cell rightward
move += 'drrul' * steps_h
steps_h -= steps_h
# 3: Move 1 step down to target postion. Then, it's done.
move += 'dru'
steps_v -= 1
else:
raise NotImplementedError('No such case has been implemented in Phase 2!')
assert steps_v == 0, steps_v
assert steps_h == 0, steps_h
self.update_puzzle(move)
return move
###########################################################
# Phase 3 methods
def solve_2x2(self):
"""
Solve the upper left 2x2 part of the puzzle
Updates the puzzle and returns a move string
"""
# Move tile 0 clockwise (cyclically) in the 2x2 cells of top left corner
game_clone = self.clone()
game_clone.update_puzzle('ul')
move = 'ul'
for dummy_idx in range(3):
if game_clone.row0_invariant(0):
break
game_clone.update_puzzle('rdlu')
move += 'rdlu'
self.update_puzzle(move)
return move
def solve_puzzle(self):
"""
Generate a solution string for a puzzle
Updates the puzzle and returns a move string
"""
move = ""
game = self.clone()
# Initialization: move tile 0 to the bottom right corner
steps_h = game.get_width() - 1 - game.current_position(0, 0)[1]
steps_v = game.get_height() - 1 - game.current_position(0, 0)[0]
move += 'r' * steps_h + steps_v * 'd'
game.update_puzzle(move)
# Phase 1
assert game.lower_row_invariant(game.get_height() - 1, game.get_width() - 1)
for row in range(game.get_height() - 1, 1, -1):
for col in range(game.get_width() - 1, 0, -1):
move += game.solve_interior_tile(row, col)
move += game.solve_col0_tile(row)
assert game.lower_row_invariant(game.current_position(0, 0)[0], game.current_position(0, 0)[1])
# Phase 2
for col in range(game.get_width() - 1, 1, -1):
move += game.solve_row1_tile(col) # Solve row 1
move += game.solve_row0_tile(col) # Solve row 0
assert game.row1_invariant(1)
# Phase 3
move += game.solve_2x2()
assert game.row0_invariant(0)
self.update_puzzle(move)
return move
# Start interactive simulation
# poc_fifteen_gui.FifteenGUI(Puzzle(4, 4))
obj = Puzzle(4, 4, [[15, 11, 8, 12], [14, 10, 9, 13], [2, 6, 1, 4], [3, 7, 5, 0]])
# print obj
move = obj.solve_puzzle()