3

As a school project I have to find the solution path in a maze using the backtracking method recursively, I usually have no problem solving algorithms with recursion, on linear problems, however when it comes of having multiple choices/paths to follow I don't know how to find only 1 solution.

Problem parameters:

  • A maze represented in a matrix which has multiple ways of getting to the same end point
  • The starting point

Language used:

  • F#

Output of the code:

████████████████████
█     █           ██
█ ███ ███ █████ █ ██
█   █   █   █   █ ██
█ █ █ █ █ █ █ ███ ██
█     █Sxxxx  █   ██
█ █████ █ █x███ █ ██
█     █   █xxx█   ██
█ █ █ ███████x██████
█ █ █   █   █x    ██
█ █ ███ █ █ █x███ ██
█   █   █ █  x█   ██
█ ███ ███ █ █x█ █ ██
█            x█   ██
███ █████████x███ ██
███ █     █xxx█ █ ██
███ █ █ █ █x███ █ ██
███   █ █xxx█     ██
█████████x██████████
█████████E██████████

#: Walls
 : Paths
E: End Point
S: Start Point

Portion of the code:

let rec dfs(x,y,path,visited) =
        let rec checkVisited point visited = 
            match visited with
            | [] -> false
            | (x,y)::xs -> if point = (x,y) then true else checkVisited point xs
        let adjacents = [(x,y+1);(x+1,y);(x,y-1);(x-1,y)]
        for point in adjacents do
            if point = this.endPoint then
                this.solutionPath <- path
            else
                if checkVisited point visited = false && this.checkPoint point && this.isWall point = false then
                    dfs(fst(point),snd(point),(path@[point]),(visited@[(x,y)]))

This is another way (mooore optimized) of searching the solution in a maze

let rec dfs(x,y,path) =
            // setting the point in the matrix visited (setting it to 'false')
            matrix.[y].[x] <- (fst(matrix.[y].[x]),false)
            // getting the adjacents of the point
            let adjacents = [(x,y+1);(x+1,y);(x,y-1);(x-1,y)]
            // iterate the adjacents
            for (x1,y1) in adjacents do
                // if the adjacent is the end point set the soultion path
                if (x1,y1) = this.endPoint then
                    this.solutionPath <- path
                else
                    // else check if the point is in the matrix and is not yet visited
                    if this.checkPoint(x1,y1) && snd(matrix.[y1].[x1]) <> false && this.isWall(x1,y1) = false then
                        // execute recursively the method in the point and add the current poisition to the path
                        dfs(x1,y1,((x1,y1)::path))
        dfs(x,y,[])

I have made it! if you have any troubles doing this i will help you (even in other languages)!

  • 1
    Basically you are trying to find the solution which will give you shortest path to the final point right. Because there are multiple paths to the final point. So assume you are trying to solve for the shortest path to the E. – zenwraight Jan 16 '20 at 18:39
  • yes, thanks for the reply, in reality it doesn't really have to be the shortest, it has to be one of the few ways of getting me to the end point. –  Jan 16 '20 at 18:40
  • 1
    But what are you trying to solve, like you just want to figure out any particular way that takes you to E, or you want to find all the possible ways to reach E. Just trying to understand. – zenwraight Jan 16 '20 at 18:46
  • @zenwraight i have to search 1 way (it doesn't matter which one) that takes me to the endpoint (using the backtracking recursive method) –  Jan 16 '20 at 18:49
  • 2
    Got it, this is easy, I am not fluent in F#, if I provide logic, will it help ? – zenwraight Jan 16 '20 at 18:50
  • 1
    Which other language are you comfortable in ? – zenwraight Jan 16 '20 at 18:51
  • @zenwraight yes it really would, if you want it you can write also the code in java/python –  Jan 16 '20 at 18:51
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/206091/discussion-between-zenwraight-and-stecco). – zenwraight Jan 16 '20 at 19:04
  • Where are you stuck? When you find any solution, you return success. Usually, the reverse problem is the hard part: how to accumulate multiple solutions. – Prune Jan 16 '20 at 20:32
  • @Prune Infact the problem is to accumulate multiple solutions and then return only one, could you help me? –  Jan 17 '20 at 11:45
  • 2
    Returning all paths is also a well-documented approach. First research solving a maze; make a valid attempt; post your results if you're still failing. You've made a good start already. – Prune Jan 17 '20 at 17:29
  • @Prune i have updated the code i am explicitly using dfs right now as you can see, but it is not working, can you take a look at the code and see what's not working please? –  Jan 18 '20 at 10:16
  • try not to use mutable variables when writing functional code in f#. `seen`, instead of being a mutable variable, can be a value passed into the recursive function. – Chechy Levas Jan 21 '20 at 16:29
  • @ChechyLevas yeah thanks, but i dont think that is the problem, can you please help me? –  Jan 23 '20 at 14:50

1 Answers1

6

Your current approach looks mostly okay. But because you're doing a depth first search the main issue you have is that there's nothing preventing you from getting stuck trying an infinitely long paths like [(1,1);(1,2);(1,1);...] instead of getting to more productive paths. To avoid this you can either scan the path to see if the proposed next point is already in it (which takes time at most linear in the length of the list which may be fine for small problem sizes), or pass the set of visited points as an extra argument to the recursive function (which should allow faster membership queries).

The other issue you have is that you don't have any way of combining the results of the different branches you could take. One simple approach is to change the return type of your inner function to be an option type, and to return Some(path) from the top if and rewrite the else to something more like

[x, y+1
 x, y-1
 x+1, y
 x-1, y]
|> List.tryPick (fun (x',y') -> if this.checkPoint(x',y') then 
                                    sol(x', y', (x,y)::path)
                                else None)

This is recursively attempting each possible direction in turn and returning whatever the first successful one is. This will not necessarily return the shortest path because it's a depth-first search. You could also easily create a variant that returns a list of all possible paths instead of using an option (the biggest change would be using List.collect instead of List.tryPick), in which case you could choose the shortest solution from the list if you wanted to, although this would do a lot of intermediate computation.

A more involved change would be to switch to a breadth-first search instead of depth-first, which would let you return a shortest path very easily. Conceptually, here's how one approach to that would be to keep track of a shortest path to all "seen" points (starting with just [S,[]], along with a set of points whose children have not yet been explored (again starting with just [S]). Then, as long as there are points to explore, collect all of their distinct children; for each one that doesn't yet have a known path, add the path and put it in the next set of children to explore.

kvb
  • 54,864
  • 2
  • 91
  • 133
  • So i have tried the DFS and updated the answer, the problem now is that it gives me Stackoverflow, probably because i don't check if i have already been in the same point, as you suggested i searched the BFS which should solve the problem, but i only found it in iterative mode and i have to solve the problem recursively, could you help me on this? thanks for the reply anyway –  Jan 17 '20 at 11:41
  • @Stecco My last paragraph indicates how to do a breadth first search recursively, in concept. You can attempt it and post an update if you run into issues. But I think fixing your depth first search to check whether the point already appears in the path will be much easier given what you've already got. – kvb Jan 17 '20 at 18:24
  • I have updated the question right now with the new code that checks if the point is already seen, but it doesn't work when it returns the solution it just gives an empty array –  Jan 17 '20 at 18:28
  • @Stecco - you've still got the issue I mention in the second paragraph; you aren't combining the results of the calls to `dfs`, you're just running the first three but ignoring the results aand returning the results of the last call. – kvb Jan 19 '20 at 14:55
  • Yes, but i really dont know how to solve the problem, i am not really into the logics of recursion on tree like structures, could you please correct the code so that i can understand? I would really appreciate it –  Jan 19 '20 at 16:09
  • @Stecco - I laid out one approach in my second paragraph, but let me explain again. First, you need to modify what you return, because depending on the path so far there may not be any way to get to the destination without revisiting a seen node. You could choose to return an option, in which case a value of None means no path found and a value of Some(path) means you found a path, or you could return a list, in which case you could return all (0 or more) paths. – kvb Jan 22 '20 at 15:35
  • Then, instead of just calling `dfs` 4 times and ignoring the returns of the first 3, you need to combine the results of each call; I gave you the code for one way to do this when using an option, but you could instead write this some other way if you prefer (e.g. `let opt1=dfs ...` and so on, and then some code to combine the values to pick out the first non-None value). – kvb Jan 22 '20 at 15:35
  • i have updated the code, but it still does not work, can you please correct it? i will give you the bounty if you do so –  Jan 23 '20 at 14:48
  • @Steco - Your code is now very close to correct if your goal is to return all possible paths. However, you have the following problems: 1. You need to return `[path]` instead of just `path` (that is, the list of possibilities you've found in this branch is just a one-element list). 2. Get rid of the mutable `visited` variable; instead pass an additional `visited` parameter into both `checkPoints` (and your recursive calls to it) and `dfs` (and your recursive calls to it; instead of updating the value of `visited` inside this function you can just bind to a new variable shadowing the old one). – kvb Jan 23 '20 at 22:44
  • a question, in the visisted list, do i add all the points that i am going to visit, or only the current point where i am? –  Jan 24 '20 at 08:43
  • Just the point where you are currently (as in your existing code). Basically just change `visited <- (x,y)::visited` to `let visited = (x,y)::visited`, which will create a new binding that hides the old one for the remainder of the function. – kvb Jan 24 '20 at 15:16
  • i have updated the code with your suggestions (i dont know if i did correctly) but it still doesn't work, it returns an empty list right now –  Jan 24 '20 at 15:26
  • @Stecco That looks fine to me; how are you implementing `this.endPoint`, `this.checkPoint`, etc.? – kvb Jan 24 '20 at 15:36
  • right now i have added the implementation of this.endPoint and this.checkPoint on the code –  Jan 24 '20 at 15:57
  • You're still missing a bunch of code; maybe try using a fixed maze like the one in your initial comment rather than a randomly generated one to test the dfs portion of your code; it's possible that your issue lies in the generation instead. – kvb Jan 24 '20 at 19:03
  • i dont think so, cause in any way it should return every kind of solution of the maze and the algorithm that generates the maze always generates a maze with at least 1 solution, can you help me writing the last part of the code? Or at least tell me what i am missing please? –  Jan 24 '20 at 19:20
  • @Stecco - no, because the dfs code looks fine to me, and running a barely modified variant of it works on my machine (including with the new maze in your updated question). But I don't have time (or, frankly, the inclination) to debug all of the extra parts of your code (not all of which are even in your question); on the other hand it should be straightforward for you to run your own code with a specific maze, and that would potentially help you track down any issues. – kvb Jan 24 '20 at 19:47
  • Can you please share the "barely variant of it" that works on your machine, that could save my life –  Jan 24 '20 at 19:48
  • @Stecco - Literally the only difference is that I do not use `(x,y) = this.endPoint` because I nested `dfs` inside of another function definition that wasn't inside of a class (I used `arr.[x,y] = 'E'` instead, where `arr` is a 2D character array representing the maze). – kvb Jan 24 '20 at 21:02
  • Hi i have BIG news, the algorithm now works, but it is tooo slow, can you help me optimizing it? –  Jan 25 '20 at 20:49
  • @Stecco - If performance is an issue (even with your modified version), I'd recommend switching to a breadth-first search. – kvb Jan 27 '20 at 15:03