8.8 KiB
id | slug | title | description | tags | last_update | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
solution | /recursion/karel/solution | Solution to the problem | Solving the problem introduced in the previous post. |
|
|
Solving the maze problem
We will go through the given problem the same way as I have suggested in the previous post.
Summary of the problem
We have a robot in some kind of a maze and we have to find our way out that is marked with a so-called “beeper”. We've been given a restriction not to use any variables, we can use just backtracking and recursion.
Brainstorming the idea
Let's start with some brainstorming of the solution.
- How will I know what I've checked without any variables?
- answer: recursion will need to take care of that, cause I'm not allowed anything else
- How will I pass around the fact I've found the exit?
- answer: I can return values from helper functions, so I should be able to indicate found/not found
- How is the exit marked?
- answer: there is one “beeper” as a mark
- Can I reduce my problem somehow?
- answer: I could check each possible direction as a reduced search space
»Rough« pseudocode
We should be able to construct a skeleton of our solution at least. Pseudocode follows:
def find_exit
if found the exit then
signal others
terminate
end
check left
check front
check right
end
As you can see, we only mention what we want to do very roughly, technical details are left out, except for the early return (which is the base of our recursive function).
»Proper« pseudocode
In the proper pseudocode we will need to dive into the technical details like the way we check for exit, move around, etc.
We can start by cleaning up and decomposing the function written above:
def find_exit
# BASE: found exit
if found_exit() then
return true
end
# check left
if left_is_clear() then
turn_left()
step()
if find_exit() then
return true
end
turn_around()
step()
turn_left()
end
# check front
if front_is_clear() then
step()
if find_exit() then
return true
end
turn_around()
step()
turn_around()
end
# check right
if right_is_clear() then
turn_right()
step()
if find_exit() then
return true
end
turn_around()
step()
turn_right()
end
return false
end
We are missing few of the functions that we use in our pseudocode above:
found_exit()
turn_around()
turn_right()
We can implement those easily:
def found_exit
if not beepers_present() then
return false
end
pick_beeper()
if beepers_present() then
put_beeper()
return false
end
put_beeper()
return true
end
def turn_around
turn_left()
turn_left()
end
def turn_right
turn_around()
turn_left()
end
Now we have everything ready for implementing it in Python.
Actual implementation
It's just a matter of rewriting the pseudocode into Python1:
class SuperKarel(Karel):
# you can define your own helper functions on Karel here, if you wish to
def found_exit(self) -> bool:
if not self.beepers_present():
return False
self.pick_beeper()
if self.beepers_present():
self.put_beeper()
return False
self.put_beeper()
return True
def turn_around(self):
for _ in range(2):
self.turn_left()
def turn_right(self):
for _ in range(3):
self.turn_left()
def find_exit(self) -> bool:
if self.found_exit():
return True
# check left
if self.left_is_clear():
self.turn_left()
self.step()
if self.find_exit():
return True
self.turn_around()
self.step()
self.turn_left()
# check front
if self.front_is_clear():
self.step()
if self.find_exit():
return True
self.turn_around()
self.step()
self.turn_around()
# check right
if self.right_is_clear():
self.turn_right()
self.step()
if self.find_exit():
return True
self.turn_around()
self.step()
self.turn_right()
return False
def run(self):
self.find_exit()
We have relatively repetitive code for checking each of the directions, I would propose to refactor a bit, in a fashion of checkin just forward, so it's more readable:
def find_exit(self) -> bool:
if self.found_exit():
return True
self.turn_left()
for _ in range(3):
if self.front_is_blocked():
self.turn_right()
continue
self.step()
if self.find_exit():
return True
self.step()
self.turn_around()
self.turn_right()
return False
We can also notice that turning around takes 2 left turns and turning to right
does 3. We get 5 left turns in total when we turn around and right afterwards…
Taking 4 left turns just rotates us back to our initial direction, therefore it
is sufficient to do just one left turn (5 % 4 == 1
). That way we get:
def find_exit(self) -> bool:
if self.found_exit():
return True
self.turn_left()
for _ in range(3):
if self.front_is_blocked():
self.turn_right()
continue
self.step()
if self.find_exit():
return True
self.step()
# turning around and right is same as one turn to the left
self.turn_left()
return False
Testing
In the skeleton, with the previous post, I have included multiple mazes that can be tested. I have tried this solution with all of the given mazes, and it was successful in finding the exit. However there is one precondition of our solution that we haven't spoken about.
We are silently expecting the maze not to have any loops. Example of such
maze can be the maze666.kw
:
┌─────────┬─┐
│. . . . .│.│
│ ┌─────┐ │ │
│.│. . .│.│.│
│ │ │ │ │ │
│.│.│. . .│.│
│ │ │ └─┤
│.│.│. . . 1│
│ │ │ │ ┌─┤
│.│. . .│.│.│
│ └─────┘ │ │
│. > . . .│.│
└─────────┴─┘
If you try running our solution on this map, Karel just loops and never finds the solution. Let's have a look at the loop he gets stuck in:
┌─────────┬─┐
│* * * * *│.│
│ ┌─────┐ │ │
│*│* * *│*│.│
│ │ │ │ │ │
│*│*│. * *│.│
│ │ │ └─┤
│*│*│. * * 1│
│ │ │ │ ┌─┤
│*│* * *│*│.│
│ └─────┘ │ │
│* * * * *│.│
└─────────┴─┘
He walks past the exit, but can't see it, cause there's always a feasible path that is worth trying.
:::tip Algorithm
The algorithm we have written to find the exit is a depth-first search (DFS). However, as opposed to the usual implementation, we have no notion of paths that are being (or have already been) explored.
:::
Fixing the issue
Since we are not allowed to use variables, the only way to resolve this issue is to mark the “cells” that we have tried. We can easily use beepers for this, but we need to be careful not to confuse the exit with already visited cell.
To do that we'll use 2 beepers instead of the one. Implementation follows:
def visited(self) -> bool:
if not self.beepers_present():
return False
self.pick_beeper()
if not self.beepers_present():
self.put_beeper()
return False
self.pick_beeper()
if self.beepers_present():
assert False, "no cell shall be marked with 3 beepers"
self.put_beeper()
self.put_beeper()
return True
def find_exit(self) -> bool:
# BASE: already tried
if self.visited():
self.turn_around()
return False
# BASE
if self.found_exit():
return True
# mark the cell as visited
for _ in range(2):
self.put_beeper()
self.turn_left()
for _ in range(3):
if self.front_is_blocked():
self.turn_right()
continue
self.step()
if self.find_exit():
return True
self.step()
# turning around and right is same as one turn to the left
self.turn_left()
return False
Now our solution works also for mazes that have loops.
-
which is usually very easy matter ↩︎