From acc0efc8e0ea1fe56132e102eec669fb2af7ba56 Mon Sep 17 00:00:00 2001 From: Matej Focko Date: Sun, 24 Dec 2023 00:09:50 +0100 Subject: [PATCH] algorithms: add solution for Karel Signed-off-by: Matej Focko --- .../2022-11-29-karel/2023-12-24-solution.md | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 algorithms/04-recursion/2022-11-29-karel/2023-12-24-solution.md diff --git a/algorithms/04-recursion/2022-11-29-karel/2023-12-24-solution.md b/algorithms/04-recursion/2022-11-29-karel/2023-12-24-solution.md new file mode 100644 index 0000000..d7abfc8 --- /dev/null +++ b/algorithms/04-recursion/2022-11-29-karel/2023-12-24-solution.md @@ -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 \ No newline at end of file