Coursera - POC Part2 - Fifteen Puzzle

在课程给的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()




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值