3

I have a 3 x 3 grid with randomly placed obstacles in which there is a random starting point but no endpoint. The endpoint is created when there are no more cells to occupy. Movement can occur up, down, left or right.

How can I see all possible unique paths within the grid?

Example:

  • Once a cell is used when looking for a path, it cannot be used again (1 becomes a 0).
  • If there are no more neighbouring cells to move into the path has ended. Regardless of weather all the cells have been visited.
# bottom left is (0, 0) and top right is (2, 2)...
# start is (1, 1) and obstacle is (2, 1)
    
[1] [1] [1]
[1] [S] [0]
[1] [1] [1]
    
S = starting point
0 = obstacle
1 = open cell

With the above example there would be 6 unique paths.

path_1 = (1, 2), (2, 2) # up, right
path_2 = (1, 0), (2, 0) # down, right
path_3 = (0, 1), (0, 2), (1, 2), (2, 2) # left, up, right, right
path_4 = (0, 1), (0, 0), (1, 0), (2, 0) # left, down, right, right
path_5 = (1, 2), (0, 2), (0, 1), (0, 0), (1, 0), (2, 0) # up, left, down, down, right, right
path_6 = (1, 0), (0, 0), (0, 1), (0, 2) (1, 2), (2, 2) # down, left, up, up, right, right
Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
walker2238
  • 33
  • 1
  • 1
  • 8
  • I suppose it is forbidden to revisit a cell. Are you familiar with depth-first search? – Beta Mar 18 '22 at 15:49
  • That's correct. Each cell can only be used once per unique path. And thanks for the suggestion. I'll look it up. – walker2238 Mar 18 '22 at 16:41
  • I suggest providing a smaller example (with a 3x3 grid instead of a 5x7 grid, or with more 0 and fewer 1), so that you can tell us what the answer would be on the example. There's a bit of an ambiguity here; how do you count unique paths? In particular, where do the paths end? Do they have to keep going until they run out of non-visited non-obstacle neighbours? Is the empty path counted? All those questions would be answered if you could list all the unique paths on one small example. – Stef Mar 18 '22 at 17:34
  • I took your advice. Does the new example provide more clarity? – walker2238 Mar 18 '22 at 19:46
  • The question is perfectly clear now with the example, thanks. One thing to note is the number of paths can grow exponentially. Breadth first search and depth first search both solve this problem. I'd recommend depth-first search, since it allows you to generate paths one at a time. It may also be helpful to know how you came to this question. If your actual goal is to find a path with particular properties (or generate random paths), it may be possible to do so much faster than the exponential-time DFS. – kcsquared Mar 21 '22 at 08:07
  • I'm not sure I understand DFS correctly. Using my example, say my first unique path is (1, 2), (2, 2). When I look for the second unique path am I using the first path and working backwards looking for un-visited neighbouring cells that would make the path unique? And reset visited cells for each pass? – walker2238 Mar 21 '22 at 15:48
  • Essentially yes; a recursive DFS would have a fixed priority ordering of directions to try (say, Up -> Right -> Down -> Left), and would explore the grid as much as possible in that order until it gets stuck, for example in (1, 2), (2, 2). Then it [backtracks](https://en.wikipedia.org/wiki/Backtracking) to just (1, 2), and extends that with (1, 2), (0, 2) and so forth until it gets stuck again, then backtracking until it finds a new direction to try (in this example, back to the very start) and trying the 'down' path from (1, 0), etc. – kcsquared Mar 21 '22 at 23:23
  • You might be interested to know what you describe is actually what is known as a self avoiding walk: https://en.wikipedia.org/wiki/Self-avoiding_walk which is pretty much the gold standard to model polymers in physics/chemistry. The self-avoiding part makes it difficult to enumerate all possible walks on a large grid (exponential time required to be exact) but if you are interested in some property of these walks that property can be estimated much more efficiently. – ldog Mar 23 '22 at 06:58

3 Answers3

3

To get all the paths, you can use DFS or BFS but each path needs to have a unique visited set to keep track that you:

  1. do not go back to the same coordinate twice in a single path
  2. allow different paths to go on the same coordinate

I wrote a DFS implementation for a grid here and the solution will rely on this example.

Solution

To do graph search one would need to define the states, which are the coordinates in this case, but for this problem we will keep track of two parameters, for convenience:

  1. The path taken will be documented via Crack code (right=0, down=1, left=2, up=3) which is a form of chain code
  2. the visited set for each path will de documented for the reasons noted above

The implementation in Python is as follows (in my case the top left matches coordinate (0,0) and lower right matches (n-1,n-1) for nXn grid)

import collections
def find_paths(grid, start_coord):
    paths       = set()  # paths will be added here
    state_queue = collections.deque([]) # Pending states which have not been explored yet
    # State is a tuple consists of:
    # 1. current coordinate
    # 2. crack code path
    # 3. set of visited coordinates in that path
    state       = [start_coord, [], {start_coord}]  # Starting state
    while True:
        # Getting all possible neighboring states
        # Crack code (right=0, down=1, left=2, up=3)
        state_right = [(state[0][0],state[0][1]+1), state[1] + [0], state[2].copy()] if state[0][1]+1 < len(grid[state[0][0]]) else None
        state_down  = [(state[0][0]+1,state[0][1]), state[1] + [1], state[2].copy()] if state[0][0]+1 < len(grid) else None
        state_left  = [(state[0][0],state[0][1]-1), state[1] + [2], state[2].copy()] if state[0][1]-1 >= 0 else None
        state_up    = [(state[0][0]-1,state[0][1]), state[1] + [3], state[2].copy()] if state[0][0]-1 >= 0 else None
        # Adding to the queue all the unvisited states, as well as to the visited to avoid returning to states
        blocked_counter = 0
        for next_state in [state_right, state_down, state_left, state_up]:
            if next_state is None:
                blocked_counter += 1
            elif next_state[0] in state[2] or grid[next_state[0][0]][next_state[0][1]] == 0:
                blocked_counter += 1
            else:
                next_state[2].add(next_state[0])
                state_queue.append(next_state)
                
        # After checking all directions' if reached a 'dead end', adding this path to the path set
        if blocked_counter == 4:
            paths.add(tuple(state[1]))
        # Popping next state from the queue and updating the path if needed
        try:
            state = state_queue.pop()
        except IndexError:
            break
    return paths

Explanation

  1. At the beginning we create the initial state, as well as the initial path which is an empty list and the visited set, containing only the start coordinate

  2. For each state we are in we do the following: 2.1. create the four neighboring states (advancing right, up, left or down)

    2.2. checking if we can advance in each direction by checking if the path we are in already visited this coordinate and if the coordinate is valid

    2.3. if we can advance in the said direction, add this next_state to the state_queue as well as to the visited set of the path

    2.4. if we can not advance in any of the four directions, we reached a 'dead end' and we add the path we are in to the paths set

    2.5. pop the next state from state_queue which is kind of a bad name since it is a stack (due to the appending and popping from the same side of the deque()

  3. When the state_queue is empty, we finished the search and we can return all the found paths

When running this with the given example, i.e.

start_coord = (1,1)
grid = [[1, 1, 1],
        [1, 1, 0],
        [1, 1, 1]]

We get:

find_paths(grid, start_coord)
# {(2, 3, 0, 0), (3, 2, 1, 1, 0, 0), (3, 0), (2, 1, 0, 0), (1, 0), (1, 2, 3, 3, 0, 0)}
Tomer Geva
  • 1,764
  • 4
  • 16
1

This is actually not as difficult as it seems. Since it seems to me that you are learning, I avoid giving you code and will focus on the ideas.

The solution

Since you need to find all solutions, this is a classical backtracking problem. The idea of backtracking is to try out every possibility and store all the solutions. Particularities about your problem:

  • grid
  • movement
  • obstacles
  • unique solutions

How to check everything?

You need to loop your grid and for each point and for each point, starting from a depth of 0, you repeat the following:

  • if depth < available points map the possible moves (no obstacles, not visited) (1)
  • for each point:
    • increase depth with 1
    • mark the current point as visited
    • check whether there was a solution and handle it if so
    • with the current status, jump to (1)
    • unmark the current point from being visited
    • decrease depth by 1

Handling the solution

You need to have a unique, predictable signature for your solution, like the points you have moved to in their order and store all solutions you had so far. Before you enter your new solution, check whether it's already among the solutions and only append it if it's not there.

Pruning

You can prune your findings based on earlier findings, if the problem-space is big-enough to make this a performance optimization, but you should only consider that when you already have a solution.

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
0

I was looking to solve this with dynamic programming to avoid the exponential time complexity but failed to do so. However, here is a dense version in Javascript which uses DFS (which given this problem is the same as brute-forcing since we are interested in all possible paths) and recursion in a functional style with es6 features. We also make use of a single-dimension array to represent the board with the directions method accounting for which positions are reachable from a given position. The single-dimension array contains the rows of the board laid out sequentially from top to bottom.

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

The algorithm works for any m by n grid and so we must specify the width as an input parameter.

const directions = (pos, board, width) => {
    const [n, s, w, e] = [pos - width, pos + width, pos - 1, pos + 1]
    return [
        n >= 0 && board[n] ? n : null, 
        s < board.length && board[s] ? s : null, 
        pos % width !== 0 && board[w] ? w : null, 
        e % width !== 0 && board[e] ? e : null
    ]
        .filter(pos => pos !== null)
}

const solve = (pos, board, width, path) => {
    const next = directions(pos, board, width)
    return next.length === 0 ? 
        [[...path, pos]] // have a complete path
        :
        next
            .map(nextPos => solve(nextPos, [...board.slice(0, pos), 0, ...board.slice(pos + 1)], width, [...path, pos]))
            .flat()
}

const oneDim2TwoDim = (oneDimCoord, width) => 
    [Math.floor(oneDimCoord / width), oneDimCoord % width]

const res = solve(4, [1, 1, 1, 1, 1, 0, 1, 1, 1], 3, [])
console.log(res.map(path => path.map(step => oneDim2TwoDim(step, 3))))

At each step, we check which directions are possible to go in. If no direction is available, we return the current path, otherwise we make a recursive call in each of the directions.

Use it like

solve(4, [1, 1, 1, 1, 1, 0, 1, 1, 1], 3, [])
/*
[
  [ 4, 1, 0, 3, 6, 7, 8],
  [ 4, 1, 2 ],
  [ 4, 7, 6, 3, 0, 1, 2],
  [ 4, 7, 8 ],
  [ 4, 3, 0, 1, 2 ],
  [ 4, 3, 6, 7, 8 ]
]
*/

Each path starts with the starting position and you can easily convert it back to (x, y) coordinate style with

const oneDim2TwoDim = (oneDimCoord, width) => 
    [Math.floor(oneDimCoord / width), oneDimCoord % width]

const res = solve(4, [1, 1, 1, 1, 1, 0, 1, 1, 1], 3, [])
    .map(path => path.map(step => oneDim2TwoDim(step, 3)))
/*
[
  [ [ 1, 1 ], [ 0, 1 ], [ 0, 0 ], [ 1, 0 ], [ 2, 0 ], [ 2, 1 ], [ 2, 2 ] ],
  [ [ 1, 1 ], [ 0, 1 ], [ 0, 2 ] ],
  [ [ 1, 1 ], [ 2, 1 ], [ 2, 0 ], [ 1, 0 ], [ 0, 0 ], [ 0, 1 ], [ 0, 2 ] ],
  [ [ 1, 1 ], [ 2, 1 ], [ 2, 2 ] ],
  [ [ 1, 1 ], [ 1, 0 ], [ 0, 0 ], [ 0, 1 ], [ 0, 2 ] ],
  [ [ 1, 1 ], [ 1, 0 ], [ 2, 0 ], [ 2, 1 ], [ 2, 2 ] ]
]
*/
fast-reflexes
  • 4,891
  • 4
  • 31
  • 44