4

I am given a problem to generate binary mazes of dimensions r x c (0/false for blocked cell and 1/true for free cell). Each maze is supposed to be solvable.

One can move from (i, j) to either (i + 1, j)(down) or (i, j + 1)(right). The solver is expected to reach (r - 1, c - 1)(last cell) starting from (0, 0)(first cell).

Below is my algorithm (modified BFS) to check if a maze is solvable. It runs in O(r*c) time complexity. I am trying to get a solution in better time complexity. Can anyone suggest me some other algorithm?? I don't want the path, I just want to check.

#include <iostream>
#include <queue>
#include <vector>

const int r = 5, c = 5;

bool isSolvable(std::vector<std::vector<bool>> &m) {
    if (m[0][0]) {
        std::queue<std::pair<int, int>> q;
        q.push({0, 0});
        while (!q.empty()) {
            auto p = q.front();
            q.pop();
            if (p.first == r - 1 and p.second == c - 1)
                return true;
            if (p.first + 1 < r and m[p.first + 1][p.second])
                q.push({p.first + 1, p.second});
            if (p.second +1 < c and m[p.first][p.second + 1])
                q.push({p.first, p.second + 1});
        }
    }
    return false;
}

int main() {
    char ch;

    std::vector<std::vector<bool>> maze(r, std::vector<bool>(c));
    for (auto &&row : maze)
        for (auto &&ele : row) {
            std::cin >> ch;
            ele = (ch == '1');
        }

    std::cout << isSolvable(maze) << std::endl;
    return 0;
}

Recursive Solution:

bool exploreMaze(std::vector<std::vector<bool>> &m, std::vector<std::vector<bool>> &dp, int x = 0, int y = 0) {
    if (x + 1 > r or y + 1 > c) return false;
    if (not m[x][y]) return false;
    if (x == r - 1 and y == c - 1) return true;
    if (dp[x][y + 1] and exploreMaze(m, dp, x, y + 1)) return true;
    if (dp[x + 1][y] and exploreMaze(m, dp, x + 1, y)) return true;
    return dp[x][y] = false;
}

bool isSolvable(std::vector<std::vector<bool>> &m) {
    std::vector<std::vector<bool>> dp(r + 1, std::vector<bool>(c + 1, true));
    return exploreMaze(m, dp);
}

Specific need:

I aim to use this function many times in my code: changing certain point of the grid, and then rechecking if that changes the result. Is there any possibility of memoization so that the results generated in a run can be re-used? That could give me better average time complexity.

brc-dd
  • 10,788
  • 3
  • 47
  • 67
  • 1
    `m[p.first + 1][p.second] and p.first + 1 < r` should be `p.first + 1 < r and m[p.first + 1][p.second]`, otherwise you can run into segmentation fault (trying to read out of bounds memory). Similary for the other condition. – Nelfeal Jun 24 '20 at 11:13
  • This is much easier if what you really want is to start with empty and then add blocks while avoiding any additions that would make it unsolvable. Even easier if you can start with full and then remove blocks until it becomes solvable. – Matt Timmermans Jun 24 '20 at 12:43
  • 1
    ummmm. Why is checking for solution needed at all? Since you can just carve paths only in allowed directions? – Sopel Jun 24 '20 at 12:58

3 Answers3

2

If calling this function many times with low changes there's a data structure called Link-Cut tree which supports the following operations in O(log n) time:

  1. Link (Links 2 graph nodes)
  2. Cut (Cuts given edge from a graph)
  3. Is Connected? (checks if 2 nodes are connected by some edges)

Given that a grid is an implicit graph we can first build Link-Cut tree, in O(n*m*log(n*m)) time

Then all updates (adding some node/deleting some node) can be done by just deleting/adding neighboring edges which will only take O(log(n*m)) time


Though I suggest optimizing maze generation algorithm instead of using this complicated data structure. Maze generation can be done with DFS quite easily

Photon
  • 2,717
  • 1
  • 18
  • 22
  • I aim to use this function many times in my code: changing certain point of the grid, and then recheck if that changes the result. Is there any possibility of memoization so that the results generated in a run can be re-used? That could give me better average time complexity. – brc-dd Jun 24 '20 at 11:12
  • @brc-dd Photon's answer is correct. You should edit your question to include that need. – Nelfeal Jun 24 '20 at 11:15
  • @Nelfeal Added that in a section named *specific need* :) – brc-dd Jun 24 '20 at 11:21
1

The problem you are looking at is known as Dynamic Connectivity and as @Photon said, as you have an acyclic graph one solution is to use Link-cut tree. Another one is based on another representation as Euler tour.

One Lyner
  • 1,964
  • 1
  • 6
  • 8
1

You cannot go below O(r*c) in the general case because, with any pathfinding strategy, there is always a special case of a maze where you need to traverse a rectangular subregion of dimensions proportional to r and c before finding the correct path.

As for memoization: there is something you can do, but it might not help that much. You can build a copy of the maze but only keeping the valid paths, and putting in each cell the direction towards the previous and next cells, as well as the number of paths that traverse it. Let me illustrate.

Take the following maze, and the corresponding three valid paths:

1 1 1 0 1        1 1 1 0 0    1 1 0 0 0    1 1 0 0 0
0 1 1 1 1        0 0 1 1 0    0 1 1 1 0    0 1 0 0 0
0 1 0 1 0        0 0 0 1 0    0 0 0 1 0    0 1 0 0 0
1 1 0 1 0        0 0 0 1 0    0 0 0 1 0    0 1 0 0 0
0 1 1 1 1        0 0 0 1 1    0 0 0 1 1    0 1 1 1 1

You can build what I'll call the forward direction grid (FDG), the backward direction grid (BDG), and the valuation grid:

R B D N N    B L L N N    3 3 1 0 0
N B R D N    N U B L N    0 2 2 2 0
N D N D N    N U N U N    0 1 0 2 0
N D N D N    N U N U N    0 1 0 2 0
N R R R B    N U L B L    0 1 1 3 3

R = right, D = down, L = left, U = up, B = both, and N = none. The FDG tells you, in each cell, in what direction is the next cell on a valid path (or if both are). The BDG is the same thing in reverse. The valuation grid tells you how many valid paths contain each cell.

For convenience, I'm putting a B at the destination in the direction grids. You can see it as if the goal was to exit the maze, and to do so, you can go in either direction from the final cell. Note that there are always the same number of B cells, and that it's exactly the number of valid paths.

The easiest way to get these grids is to build them during a depth-first search. In fact, you can even use the BDG for the depth-first search since it contains backtracking information.

Now that you have these, you can block or free a cell and update the three grids accordingly. If you keep the number of valid paths separately as well, you can update it at the same time and the condition "the maze is solvable" becomes "the number of valid paths is not zero". Also note that you can combine both direction grids, but I find them easier to grasp separately.

To update the grids and the number of valid paths, there are three cases:

  • (A) you blocked a cell that was marked N; you don't need to do anything.
  • (B) you blocked a cell that was not marked N, so previously part of at least one valid path; decrement the number of valid paths by the cell's value in the valuation grid, and update all three grids accordingly.
  • (C) you freed a cell (that was necessarily marked N); update all three grids first and then increment the number of valid paths by the cell's new value in the valuation grid.

Updating the grids is a bit tricky, but the point is that you do not need to update every cell.

In case (B), if the number of valid paths hits zero, you can reset all three grids. Otherwise, you can use the FDG to update the correct cells forward until you hit the bottom-right, and the BDG to update the correct ones backward until you hit the top-left.

In case (C), you can update the direction grids first by doing a depth-first search, both forward and backward, and backtrack as soon as you hit a cell that isn't marked N (you need to update this cell as well). Then, you can make two sums of the values, in the valuation grid, of the cells you hit: one going forward and one going backward. The number of paths going through the new cell is the product of these two sums. Next, you can update the rest of the valuation grid with the help of the updated direction grids.

I would imagine this technique having an effect on performance with very large mazes, if the updates to the maze itself do not create or break too many paths every time.

Nelfeal
  • 12,593
  • 1
  • 20
  • 39