0

I am trying to get the shortest path for a maze with a ball: the ball is rolling until it hits a wall. I use Dijkstra's algorithm using heapq for priority queue. However, I get a non-optimal path as result.

Here is my code with sample input:

maze = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
        [0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0],
        [0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0],
        [0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0],
        [1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
        [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1],
        [1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
        [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0],
        [0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0],
        [0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

start = (0, 0)
end = (22, 22)

def shortestDistance(maze: List[List[int]], start: List[int], destination: List[int]):
    start, destination = tuple(start), tuple(destination)
    row, col = len(maze), len(maze[0])
    moves = [(-1, 0), (0, 1), (0, -1), (1, 0)]
    dstr = ['u', 'r', 'l', 'd']

    class Point:
        def __init__(self, distance, coordinates, directions):
            self.distance = distance
            self.coordinates = coordinates
            self.directions = directions

        def __eq__(self, p):
            if self.distance == p.distance:
                return self.__lt__(self, p)

            return self.distance - p.distance

        def __lt__(self, p):
            return len(self.directions) - len(p.directions)


    heap = [(Point(0, start, ""))]
    visited = set()
    while heap:
        point = heapq.heappop(heap)
        dist = point.distance
        node = point.coordinates
        directions = point.directions

        if node in visited: continue
        if node == destination:
            return directions

        visited.add(node)

        for idx, move in enumerate(moves):
            dx, dy = move
            newX = node[0]
            newY = node[1]
            distance = dist
            newDirections = directions
            while 0 <= newX + dx < row and 0 <= newY + dy < col and maze[newX + dx][newY + dy] == 0:
                newX += dx
                newY += dy
                distance += 1
                if (newX, newY) == destination:
                    break

            if (newX, newY) not in visited:
                heapq.heappush(heap, Point(distance, (newX, newY), newDirections + dstr[idx]))

    return "Impossible"


path = shortestDistance(maze, start, end)
print(path)

The idea is to compare the distance and if it is equal, pick the path with fewer changes of direction.

I am currently getting rdrludlrdrudludldldr (i.e. right-down-right-left-...) as an output, but the sequence "rl" found at index 2 doesn't make sense: "Right" should not be followed by "Left" nor should "Up" be followed by "Down" and vice versa. Such a sequence is evidently not optimal as the first of those two moves could just be omitted to get the ball at the same location and travelling shorter distance.

The expected output for this maze is drururdrdrurdrd.

Why am I not getting the shortest path?

trincot
  • 317,000
  • 35
  • 244
  • 286
feedy
  • 1,071
  • 5
  • 17
  • Try to create a as small as possible failing example and than start debugging. – MrSmith42 Apr 16 '21 at 11:11
  • While your suggestion can be used for testing, the output clearly shows something else going wrong, Unfortunately. I can't get my head around the logic behind the algorithm to fix it and I am sure it's a small mistake somewhere. – feedy Apr 16 '21 at 11:13
  • Like in your previous question, you have undefined variables. Please, can we ask you to diligently check your code for these obvious errors before posting on Stack Overflow? – trincot Apr 16 '21 at 11:17
  • What are the undefined variables good sir?? I appreciate you following me on Stack Overflow to try and close my questions, I only wish someone with such dedication towards actual algorithms showed up here. – feedy Apr 16 '21 at 11:18
  • x, y are nowhere defined. I already mentioned this at your previous question. – trincot Apr 16 '21 at 11:20
  • BTW: I am not following you, nor is my aim to close your questions. I am looking at questions that interest me, and try to help you to post better quality questions. If you prefer I do not do that anymore, I will happily leave you with the rest of the community. – trincot Apr 16 '21 at 11:23
  • For some reason, my code ran with x and y and I never even got an error they are undefined, not sure why, perhaps Pycharm's cache. I've changed them to newX and newY now and the output is exactly the same. Do you have any input on the algortihm itself :)? – feedy Apr 16 '21 at 11:25
  • @trincot Also could you please explain how is my question, in its current form, against SO's guidelines, as you both downvoted it and also suggest closing it? – feedy Apr 16 '21 at 11:37
  • I did neither of these things. Why do you say that? Your question is on topic, but, like I said it is problematic that your previous question (which I think you deleted now) had so many problems that could be prevented if you would just had taken a bit more care before posting. And it was not encouraging that you repeated the same mistake here (x, y undefined). – trincot Apr 16 '21 at 11:44

1 Answers1

2

The problem is that the __lt__ function is not doing what it should.

It should return a boolean which is true when self is to be considered less than p. As you currently return an integer result, which often is non-zero you get into the situation where a pair (p, q) of points would have both p < q and q < p as true... which leads to erratic behaviour.

Here is how you could define it:

def __lt__(self, p):
    return ((self.distance, len(self.directions), self.directions) < 
          < (p.distance, len(p.directions), p.directions)) 

With this change the returned path is

rdrdldldrdr

Simplification

Instead of creating the class Point, you could use named tuples, which makes everything easier (and faster). You would just need to change the order of the "properties" so that these points compare in the desired way, i.e. directions should come before coordinates and the length of the directions string should get its own property:

from collections import namedtuple

# change order of properties so comparison works as intended
Point = namedtuple("Point", "distance, length, directions, coordinates")

And then make the appropriate change where you call Point:

heap = [Point(0, 0, "", start)]
# ...
heapq.heappush(heap, Point(distance, len(newDirections) + 1, newDirections + dstr[idx], (newX, newY)))
trincot
  • 317,000
  • 35
  • 244
  • 286
  • That's excellent, thank you so much (again)! I know I might be extending your hospitality here, but could you tell me (feel free not to), why would I not be getting a correct result for another example array. It is a bit hard ot put it in a comment so I uploaded it here. https://pastebin.com/8uyBgmp7 – feedy Apr 16 '21 at 12:31
  • I am getting `drururdrdrurdrd` as an output - and while the first 7 or 8 directions are fine, the next ones essentially lead to a dead end. I believe it should start as `drururd` but then it goes up rather than down (at least I think it should go down, by trying to find the best path by hand). Same starting and end points. – feedy Apr 16 '21 at 12:32
  • If you think the solution is not optimal, then what is the optimal solution? It looks fine to me. – trincot Apr 16 '21 at 12:39
  • I made an update after reading that shorter directions (fewer turns) should get precedence when the distance is equal. It doesn't change the outcome for this case though. – trincot Apr 16 '21 at 12:55
  • Nevermind, it is absolutely optimal. Thank you so much, if you are into cryptocurrencies or have a paypal, please send me your address in a private chat so I can at least buy you a beer, I really mean it. You saved me a lot of time – feedy Apr 16 '21 at 13:12
  • 1
    Nice to hear. If you ever have another question I can help with, just ping me (with @trincot in a comment), and I'll have a look. No need for compensation. An up vote once in a while is already nice (but don't bulk vote). – trincot Apr 16 '21 at 13:26