Solution to the backtracking with Karel (#10)

This commit is contained in:
Matej Focko 2023-12-24 00:12:50 +01:00 committed by GitHub
commit 155fa9c741
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 389 additions and 1 deletions

View file

@ -0,0 +1,387 @@
---
id: solution
slug: /recursion/karel/solution
title: Solution to the problem
description: |
Solving the problem introduced in the previous post.
tags:
- python
- karel
- recursion
- backtracking
- solution
last_update:
date: 2023-12-24
---
# 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:
```ruby
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:
```ruby
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:
```ruby
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 Python[^1]:
```py
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:
```py
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:
```py
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:
```py
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.
[^1]: which is usually very easy matter

View file

@ -1,5 +1,6 @@
---
id: karel-1
id: karel
slug: /recursion/karel
title: Recursion and backtracking with Robot Karel
description: |
A problem with too many restrictions.