9

I have a 2D array, arr, where each cell in it has a value 1, 2 or 3, for example, arr[0][0] = 3, arr[2][1] = 2, and arr[0][4] = 1.

I want to know the shortest path from a given certain cell, for example, arr[5][5] to the closest cell which has value 2 where the path shouldn't contain any cells that have the value 1. How can I do this?

Below is a script for the BFS, but how can I make it accept a 2D array as a graph and starting point as a certain cell location in the array and then go to the nearest two from this cell avoiding cells with 1s, so that it looks like bfs(2darray, starting location, 2)?

def bfs(graph, start, end):
    # Maintain a queue of paths
    queue = []

    # Push the first path into the queue
    queue.append([start])
    while queue:

        # Get the first path from the queue
        path = queue.pop(0)

        # Get the last node from the path
        node = path[-1]

        # Path found
        if node == end:
            return path

        # Enumerate all adjacent nodes, construct a new path and push it into the queue
        for adjacent in graph.get(node, []):
            new_path = list(path)
            new_path.append(adjacent)
            queue.append(new_path)

print bfs(graph, '1', '11')

Enter image description here

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tak
  • 3,536
  • 11
  • 51
  • 93
  • Do the values in the cells influence the "cost" of the path to that cell, or in the length of the path always the manhattan distance? – tobias_k Dec 19 '17 at 23:05
  • @tobias_k yes they influence it, if it has the value 1 then this cell can't be part of the path. I'll update my question – Tak Dec 19 '17 at 23:07
  • Use Breadth First Search over all adjacent cells that have a `2` until you find a `3`. – tobias_k Dec 19 '17 at 23:08
  • @tobias_k thanks for trying to help. Could you please explain how this can be done please? As bfs takes a graph along with a start and a goal points, and in my case I have a 2d array not a graph and I want to search values not by location. – Tak Dec 19 '17 at 23:15
  • @Tak A grid/2D array is a lot like a graph, if you choose to think of it that way. Think of adjacent points as having an edge between them, and you're half way there. – mypetlion Dec 19 '17 at 23:21
  • @mypetlion but how I can adjust the posted code to accept inputs as the 2d array, starting cell, and value searching for? – Tak Dec 20 '17 at 00:38
  • @tobias_k I'd really appreciate it if you could have a look at it whenever you can. Thanks in advance – Tak Dec 20 '17 at 00:38

2 Answers2

33

You can use a simple breadth first search for this. Basically, each cell in your grid corresponds to a node in the graph, with edges between adjacent cells. Start at the starting position, and keep expanding passable cells until you find a goal cell.

def bfs(grid, start):
    queue = collections.deque([[start]])
    seen = set([start])
    while queue:
        path = queue.popleft()
        x, y = path[-1]
        if grid[y][x] == goal:
            return path
        for x2, y2 in ((x+1,y), (x-1,y), (x,y+1), (x,y-1)):
            if 0 <= x2 < width and 0 <= y2 < height and grid[y2][x2] != wall and (x2, y2) not in seen:
                queue.append(path + [(x2, y2)])
                seen.add((x2, y2))

Grid setup and results: (Note that I'm using symbols instead of numbers, simply for the reason that it's easier to visually parse the grid this way and to verify the solution.)

wall, clear, goal = "#", ".", "*"
width, height = 10, 5
grid = ["..........",
        "..*#...##.",
        "..##...#*.",
        ".....###..",
        "......*..."]
path = bfs(grid, (5, 2))
# [(5, 2), (4, 2), (4, 3), (4, 4), (5, 4), (6, 4)]
tobias_k
  • 81,265
  • 12
  • 120
  • 179
  • Thank you very much, answer upvoted. Can you try it when grid has values here http://pasteall.org/729272 ? it seems very slow to me sometimes – Tak Dec 20 '17 at 23:08
  • @Tak I changed the position of the `(x, y) in seen` check. It should be much faster now, as a lot fewer elements are needlessly added to the queue. – tobias_k Dec 20 '17 at 23:24
  • Thank you very much. And I noticed something. In the example here http://pasteall.org/729289 I find that usually the result comes back with only 1 point which is the point of origin where the search started for the nearest 7 but there is no path. Any idea what is causing this to happen? – Tak Dec 21 '17 at 00:18
  • I'd really appreciate it if you could please advise – Tak Dec 21 '17 at 05:15
  • @Tak There was a copy-paste error in my updated code, please try again. It it does not work, please say for which starting position you are getting the problem. – tobias_k Dec 21 '17 at 09:02
  • Im sorry to open the thread again but, I was implementing this code and I dont quite understand why the inversion of y and x in `if grid[y][x] == goal:` . Why not use `if grid[x][y] == goal:` ? Thanks! – Blue Ross Jun 05 '21 at 03:43
  • 1
    @BlueRoss The `grid` is a list of strings, where each string is one "row" in the grid. So with `grid[a][b]`, `a` selects the string (the row), and then `b` select the character within this string (the column). Hence, it has to be `grid[y][x]` since `x` is the column and `y` the row. – tobias_k Jun 05 '21 at 11:02
  • This works perfect, thanks! Also, easy to understand and very elegant. – ProfessorPorcupine Jul 11 '21 at 16:55
  • Add 'import collections' at the beginning. – salehinejad Jun 29 '22 at 15:49
4

If the list is not too big, the easiest solution I find is using the where function of the NumPy library to find the cells which have the value you are looking for. So, you will need to convert your list into a NumPy array.

The code below might be simplified to make it shorter and more efficient, but in this way it will be clearer. By the way, you can compute two kind of distances: the typical Euclidean and the Manhattan.

If there is more than one target cell at the same distance of the origin cell, min_coords corresponds to the first cell found (first by rows, then by columns).

import numpy as np

# The list needs to be transformed into an array in order to use the np.where method
# arr = np.random.randint(5, size=(6, 6))
arr = np.array([[0, 0, 0, 1, 1, 3],
                [0, 0, 2, 1, 1, 0],
                [0, 0, 1, 1, 1, 1],
                [3, 0, 3, 1, 1, 1], ])

# Origin cell to make the search
x0, y0 = (1, 1)
targetValue = 3

# This is the keypoint of the problem: find the positions of the cells containing the searched value
positions = np.where(arr == targetValue)
x, y = positions

dx = abs(x0 - x)  # Horizontal distance
dy = abs(y0 - y)  # Vertical distance

# There are different criteria to compute distances
euclidean_distance = np.sqrt(dx ** 2 + dy ** 2)
manhattan_distance = abs(dx + dy)
my_distance = euclidean_distance  # Criterion choice
min_dist = min(my_distance)
print(min_dist)

min_pos = np.argmin(my_distance)  # This method will only return the first occurrence (!)
min_coords = x[min_pos], y[min_pos]
print(min_coords)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
elthaniel
  • 41
  • 3
  • Nice, but this does not take into account that there could be `1`s blocking the path to the nearest `3`. – tobias_k Dec 20 '17 at 08:41