mirror of
https://github.com/mfocko/blog.git
synced 2024-11-22 04:53:47 +01:00
Solution to the backtracking with Karel (#10)
This commit is contained in:
commit
155fa9c741
2 changed files with 389 additions and 1 deletions
387
algorithms/04-recursion/2022-11-29-karel/2023-12-24-solution.md
Normal file
387
algorithms/04-recursion/2022-11-29-karel/2023-12-24-solution.md
Normal 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
|
|
@ -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.
|
Loading…
Reference in a new issue