5
\$\begingroup\$

I have made a simple python algorithm that calculates the amount of moves (Right, Left, Up, Down) to get to all other points on a grid from a certain starting point. However, the grid includes obstacles (*). The points that haven't been explored yet have the value -1.

def floodfill(x,y):
    # The default grid
    grid =[[-1  ,-1  , '*', '*'],
           ['*' ,-1  ,-1  , '*'],
           ['*' ,'*' ,-1  , '*'],
           [-1  ,-1  ,-1  ,  -1]]

    # Set the starting point to 0, 0 amount of moves needed
    grid[x][y] = 0

    # Set the starting number, the algorithm will search for this number to continue its flood
    number = 0

    while True:
        found = False
        for y in range(len(grid)):
            for x in range(len(grid[0])):
                # If 'number' is found, continue the flood from there
                if grid[x][y] == number:
                    # x+1 has to be IN the grid, there must be NO obstacle, and the point must be UNEXPLORED
                    if(x+1 < len(grid[0]) and grid[x+1][y] != '*' and grid[x+1][y] == -1):
                        grid[x+1][y] = number + 1 #Right
                        found = True
                    if(x-1 >= 0 and grid[x-1][y] != '*' and grid[x-1][y] == -1):
                        grid[x-1][y] = number + 1 #Left
                        found = True
                    if(y+1 < len(grid) and grid[x][y+1] != '*' and grid[x][y+1] == -1):
                        grid[x][y+1] = number + 1 #Down
                        found = True
                    if(y-1 >= 0 and grid[x][y-1] != '*' and grid[x][y-1] == -1):
                        grid[x][y-1] = number + 1 #Up
                        found = True

        # Break loop if max amount of steps is needed              
        if found == False:
            break
        # Increment 'number', next loop the algorithm will continue from this point
        number += 1

    return grid

So for example, running floodfill(0,0) will return:

[  0,   1, '*', '*']
['*',   2,   3, '*']
['*', '*',   4, '*']
[  7,   6,   5,   6]

Are there ways I can improve the 4 if statetements that check if you can move from one point to another?

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

Generalities

Since you are performing pretty much the same operation within each of your 4 ifs, you can simplify the code using an helper function to generate the 4 neighbours and apply the same operation to each of them. For better performances, this helper can be a generator:

def neighbours(x, y):
    for dx in [-1, 1]:
        yield x + dx, y
    for dy in [-1, 1]:
        yield x, y + dy


def floodfill(grid, x, y):
    # Set the starting point to 0, 0 amount of moves needed
    grid[x][y] = 0

    # Set the starting number, the algorithm will search for this number to continue its flood
    number = 0

    while True:
        found = False
        for y in range(len(grid)):
            for x in range(len(grid[0])):
                # If 'number' is found, continue the flood from there
                if grid[x][y] == number:
                    for dx, dy in neighbours(x, y):
                        if 0 <= dx < len(grid[0]) and 0 <= dy < len(grid) and grid[dx][dy] == -1:
                            grid[dx][dy] = number + 1
                            found = True

        # Break loop if max amount of steps is needed              
        if found == False:
            break
        # Increment 'number', next loop the algorithm will continue from this point
        number += 1

    return grid


if __name__ == '__main__':
    floodfill([
        [-1  ,-1  , '*', '*'],
        ['*' ,-1  ,-1  , '*'],
        ['*' ,'*' ,-1  , '*'],
        [-1  ,-1  ,-1  ,  -1],
    ], 0, 0)

Now, from there:

  • I changed the way to handle the grid variable and made it a parameter, it allows for better reusability;
  • floodfill now modifies a parameter in-place and returns it; this is unnecessary and we need to figure out wether returning a fresh list or modifying the parameter in-place is the best option;
  • You spend a lot of time iterating over the elements of the grid only to learn that you can't do anything with them (yet): if only you kept track of the previously modified coordinates so you can check only their neighbours…
  • You compute len(grid[0]) and len(grid) twice for each point that is not a wall, better store them in a variable.
  • You use x to access a row of the grid (ordinate) and y to access an element in that row (abscissa) but check that x is lower than the width and y is lower than the height: you should probably use grid[y][x] to avoid errors when the grid is not square.
  • You don't check if the starting point is in a wall and blindly replace it with 0.

Data structure

I don't know your use case, so I don't know the need you may have for reusability of your initial grid (like trying floodfill on the same grid from different starting points), but:

  • If you expect the grid you pass to the function to hold the result of the computation, you can modify it in place and, thus, not return it;
  • If you expect the function to return the result of the computation, then you should not modify the input.

In any case, to get better type consistency in the output, I would expect walls to be None rather than a string. If you want pretty characters for your walls, write a function that will print them out of the output of this one.

Using None in the output also means using them in the input in case you chose to modify the list in-place.

Otherwise, if you chose to return a completely new list, you can change the input a bit, as the user does not have to be aware of the inner of you function. You can choose to accept a list of booleans where truthy values reprensent the path and falsey values represent walls. Note that, this wording also means that, albeit we would favor input such as:

        [[True, True, False, False],
        [False, True, True, False],
        [False, False, True, False],
        [True, True, True, True]]

your original grid (with walls marked as None) of

        [[-1  ,-1  , None, None],
        [None ,-1  ,-1  , None],
        [None ,None ,-1  , None],
        [-1  ,-1  ,-1  ,  -1]]

would work just the same, granted we use implicit boolean conversion.

Workload

We also need to keep track of modified values so we don't end up iterating over and over again over walls and values we already computed. Doing nothing but wasting time.

One solution is using a list/set of the current coordinates we need to take care of and a second one to store the coordinates we modify during the iteration of the first one. At the end of the iteration, we swap the two lists/sets and start over. Until there is no more coordinates added to the second one.

It works but feels a bit hackish. Instead, a better alternative is to use a FIFO queue: only one queue is used, we append elements to it as we modify them and we pop elements out of it one by one to take care of it (or its neighbours, actually). The FIFO queue guaranties that the poped element is always the oldest, so we are assured that the number associated to the coordinates of that element is less (or equal) than the number associated to the other elements of the queue.

You can use append() and pop(0) on a list as your FIFO queue, but it is not efficient, as pop(0) is \$O(n)\$. Instead, you should use append() and popleft() on a collections.deque

Proposed improvements

from collections import deque


def neighbours(x, y):
    for dx in [-1, 1]:
        yield x + dx, y
    for dy in [-1, 1]:
        yield x, y + dy


def floodfill(is_path, x, y):
    height = len(is_path)
    width = len(is_path[0])

    grid = [[None] * width for _ in is_path]
    if not is_path[y][x]:
        # If we started in a wall, we can't reach anything
        return grid
    grid[y][x] = 0

    modified = deque([(x, y)])

    while modified:
        x, y = modified.popleft()
        number = grid[y][x]
        for dx, dy in neighbours(x, y):
            if 0 <= dx < width and 0 <= dy < height and is_path[dy][dx] and grid[dy][dx] is None:
                grid[dy][dx] = number + 1
                modified.append((x, y))

    return grid


if __name__ == '__main__':
    floodfill([
        [True, True, False, False],
        [False, True, True, False],
        [False, False, True, False],
        [True, True, True, True],
    ], 0, 0)

Note that if you want to use the version that modifies the parameter in-place, you just need to remove the grid building and return as well as and is_path[dx][dy] from the if; and changing the comparison back to grid[dy][dx] == -1.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.