0

I have designed a algorithm which will solve the 8 square problem using BFS or DFS.The current problem is that it is running for infinitely long time.If I let it run for 30 minutes or so.It ends with my RAM getting full.What's the problem in my implementation.I am unsuccessful to debug this code. Thanks in advance.

import copy
import time
import pprint
def get_empty_board():
    return [
        [7,4,2],
        [8,3,0],
        [1,5,6]
        ]
end_state = [
    [1,2,3],
    [8,0,4],
    [7,6,5]
    ]
def solve_squares(board):
    states_explored = list()
    states = [ (neighbor,[board] + [neighbor]) for neighbor in get_neighbors(board) ]
    while states:
        print(len(states))
        current_state = states.pop(0)
        if (current_state[0]) in states_explored:
            continue
        # if len(states) > 300:
            # states =[ states[0] ]
        # pprint.pprint(current_state)
        # pprint.pprint(current_state)
        if current_state[0] == end_state:
            return True,current_state[1]
        neighbors = get_neighbors(current_state[0])
        states_explored.append(current_state[0])
        for neighbor in neighbors:
            if (neighbor) not in states_explored:
                states.append((neighbor,current_state[1] + [neighbor]))
                states_explored.append((neighbor[0]))
    return False,None
def get_neighbors(board):
    x = None
    y = None
    neighbors = list()
    for i in range(len(board)):
        for j in range(len(board[0])):
            if board[i][j] == 0:
                x = i
                y = j
                break
    # print(x,y)
    for i in range(len(board)):
        for j in range(len(board[0])):
            if abs(i-x) <= 1 and abs(j-y) <= 1:
                if abs(i-x) != abs(j-y):
                    # print(i,j)
                    new_state = copy.deepcopy(board)
                    new_state[x][y] = new_state[i][j]
                    new_state[i][j] = 0
                    # pprint.pprint(new_state)
                    # time.sleep(5)
                    neighbors.append(new_state)
    return neighbors
def main():
    result,path = solve_squares(get_empty_board())
    print(result)
    print(path)
main()

3 Answers3

1

There are couple of improvements required in your solution:

  1. You are using a python list (e.g., states_explored) to track the board configurations that you already been visited. Now, list's average case complexity for x in s is: O(n). You need to apply some efficient data structure (e.g., set) for this purpose. You can check this stack-overflow answer for details discussion about this optimization.
  2. Your RAM is getting full because you are storing the full paths of every discovered board configurations in the queue (e.g., states). This is quite unnecessary and memory inefficient. To solve this, you can use an efficient data-structure (e.g., map) to store the parent-state of a given state. After discovering the target, you need to construct the path by backtracking the map.

We can visualize the second optimization by the following example. Let's say, you are mapping <state, parent-state> as the key-value:

<initial-state, NaN>
<state-1, initial-state>
<state-2, state-1>
<state-3, state-2>
...
...
<final-state, state-n>

Now after discovering final-state, we can query what is the parent of final-state in the map. Then, recursively made this query until we reach to the initial-state.

If you apply this two optimizations, you will get huge improvement in your run-time, as well as in the memory consumption.

biqarboy
  • 852
  • 6
  • 15
1

The issue is indeed performance. To get an idea of the speed, put the following print in your code (and only that one):

    if len(states_explored) % 1000 == 0:
        print(len(states_explored))

You'll see how it slows down as it progresses. I wrote a more efficient implementation, and found that the algorithm would need to visit over 100,000 states to find the solution. At the rate you see the above line output lines, you can imagine that it will take a very long time to finish. I didn't have the patience to wait for it.

Note that there was one error I spotted in your code, but it does not harm the algorithm:

states_explored.append((neighbor[0]))

This statement is wrong for two reasons:

  • neighbor is a board, so taking index [0] from it, produces the first row of that board, and that is useless for this algorithm.
  • If you would correct it to just neighbor, it would become significant, but would stop the algorithm in its tracks, because then the search will stop when these neighbors are popped from the queue.

So this line should just be omitted.

Here are some ways to improve the efficiency of your algorithm:

  • Use a primitive value to represent the board, not a list. For instance, a string of 9 characters would do the job. Python can work much faster with strings than with 2-dimensional lists. This also means you don't need deep_copy.
  • Don't use a list for tracking which states have been visited. Use a set -- or to also cover for the next point -- a dictionary. Lookup in a set/dictionary is more efficient than in a list.
  • Don't store the whole path of boards that leads to a certain state. It is enough to track the previous state. You can use a dictionary to both indicate that a state was visited, and from which state it came. This will represent a path as a linked list. And so you can reconstruct a path from it once you have found the target.
  • When looking for neighbors, don't iterate each cell of the board. It is quite clear at which indexes these neighbors are at, and there are at most 4 of them. Just locate these four coordinates and check whether they are in range. Here the string-representation of the board will also come in handy: you can locate the 0-cell with board.index("0").
  • Don't use .pop(0) on a list: it is not efficient. You can use a deque instead. Or -- what I prefer in this case -- don't pop at all. Instead, use two lists. Iterate the one, and populate the second. Then assign the second list to the first and repeat the process with an empty second list.

Here is the code I would suggest. It has the same print I suggested at the start of this answer, and finds the solution in a few seconds.

def get_empty_board():
    return "742830156"

end_state = "123804765"

def print_board(board):
    print(board[:3])
    print(board[3:6])
    print(board[6:])

def solve_squares(board):
    states = [(board, None)]
    came_from = {}
    while states:
        frontier = []
        for state in states:
            board, prev = state
            if board in came_from:
                continue
            came_from[board] = prev
            if len(came_from) % 1000 == 0:
                print(len(came_from))
            if board == end_state:  # Found! Reconstruct path
                path = []
                while board:
                    path += [board]
                    board = came_from[board]
                path.reverse()
                return path
            frontier += [(neighbor, board) for neighbor in get_neighbors(board) if neighbor not in came_from]
        states = frontier

def get_neighbors(board):
    neighbors = list()
    x = board.index("0")
    if x >= 3:  # Up
        neighbors.append(board[0:x-3] + "0" + board[x-2:x] + board[x-3] + board[x+1:])
    if x % 3:  # Left
        neighbors.append(board[0:x-1] + "0" + board[x-1] + board[x+1:])
    if x % 3 < 2:  # Right
        neighbors.append(board[0:x] + board[x+1] + "0" + board[x+2:])
    if x < 6:  # Down
        neighbors.append(board[0:x] + board[x+3] + board[x+1:x+3] + "0" + board[x+4:])
    return neighbors

path = solve_squares(get_empty_board())
print("solution:")
for board in path:
    print_board(board)
    print()
trincot
  • 317,000
  • 35
  • 244
  • 286
0

Flattening the board and using a dictionary containing pre-mapped moves will simplify and accelerate the logic considerably. A BFS approach is recommended in order to obtain the smallest number of moves. To keep track of visited positions, the flattened board can be stored as a tuple which will allow direct use of a set to efficiently track and verify previous states:

# move mapping (based on position of the zero/empty block)
moves = { 0: [1,3],
          1: [0,2,4],
          2: [1,5],
          3: [0,4,6],
          4: [1,3,5,7],
          5: [2,4,8],
          6: [3,7],
          7: [4,6,8],
          8: [5,7] }

from collections import deque               
def solve(board,target=(1,2,3,4,5,6,7,8,0)):
    if isinstance(board[0],list):  # flatten board
        board  = tuple(p for r in board for p in r)
    if isinstance(target[0],list): # flatten target
        target = tuple(p for r in target for p in r)
    seen = set()
    stack = deque([(board,[])])         # BFS stack with board/path
    while stack:
        board,path = stack.popleft()    # consume stack breadth first
        z = board.index(0)              # position of empty block
        for m in moves[z]:              # possible moves
            played = list(board)                       
            played[z],played[m] = played[m],played[z] # execute move
            played = tuple(played)                    # board as tuple
            if played in seen: continue               # skip visited layouts
            if played == target: return path + [m]    # check target
            seen.add(played)                          
            stack.append((played,path+[m]))           # stack move result

output:

initial = [ [7,4,2],
            [8,3,0],
            [1,5,6]
          ]
target  = [ [1,2,3],
            [8,0,4],
            [7,6,5]
          ]

solution = solve(initial,target) # runs in 0.19 sec.

# solution = (flat) positions of block to move to the zero/empty spot
[4, 1, 0, 3, 6, 7, 4, 1, 0, 3, 4, 1, 2, 5, 8, 7, 6, 3, 4, 5, 8, 7, 4]


board = [p for r in initial for p in r]
print(*(board[i:i+3] for i in range(0,9,3)),sep="\n")
for m in solution:
    print(f"move {board[m]}:")
    z = board.index(0)
    board[z],board[m] = board[m],board[z]
    print(*(board[i:i+3] for i in range(0,9,3)),sep="\n")

[7, 4, 2]
[8, 3, 0]
[1, 5, 6]
move 3:
[7, 4, 2]
[8, 0, 3]
[1, 5, 6]
move 4:
[7, 0, 2]
[8, 4, 3]
[1, 5, 6]
move 7:
[0, 7, 2]
[8, 4, 3]
[1, 5, 6]
move 8:
[8, 7, 2]
[0, 4, 3]
[1, 5, 6]
move 1:
[8, 7, 2]
[1, 4, 3]
[0, 5, 6]
move 5:
[8, 7, 2]
[1, 4, 3]
[5, 0, 6]
move 4:
[8, 7, 2]
[1, 0, 3]
[5, 4, 6]
move 7:
[8, 0, 2]
[1, 7, 3]
[5, 4, 6]
move 8:
[0, 8, 2]
[1, 7, 3]
[5, 4, 6]
move 1:
[1, 8, 2]
[0, 7, 3]
[5, 4, 6]
move 7:
[1, 8, 2]
[7, 0, 3]
[5, 4, 6]
move 8:
[1, 0, 2]
[7, 8, 3]
[5, 4, 6]
move 2:
[1, 2, 0]
[7, 8, 3]
[5, 4, 6]
move 3:
[1, 2, 3]
[7, 8, 0]
[5, 4, 6]
move 6:
[1, 2, 3]
[7, 8, 6]
[5, 4, 0]
move 4:
[1, 2, 3]
[7, 8, 6]
[5, 0, 4]
move 5:
[1, 2, 3]
[7, 8, 6]
[0, 5, 4]
move 7:
[1, 2, 3]
[0, 8, 6]
[7, 5, 4]
move 8:
[1, 2, 3]
[8, 0, 6]
[7, 5, 4]
move 6:
[1, 2, 3]
[8, 6, 0]
[7, 5, 4]
move 4:
[1, 2, 3]
[8, 6, 4]
[7, 5, 0]
move 5:
[1, 2, 3]
[8, 6, 4]
[7, 0, 5]
move 6:
[1, 2, 3]
[8, 0, 4]
[7, 6, 5]
Alain T.
  • 40,517
  • 4
  • 31
  • 51