Skip to content

Commit 78616fc

Browse files
committed
Rewrote "Rat in a maze" algorithm
It's based on the previous implementation but offers a better API and is now properly testable, too
1 parent 15835ed commit 78616fc

File tree

2 files changed

+202
-49
lines changed

2 files changed

+202
-49
lines changed

Backtracking/RatInAMaze.js

Lines changed: 118 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,136 @@
11
/*
22
* Problem Statement:
3-
* - Given a NxN grid, find whether rat in cell (0,0) can reach target(N-1,N-1);
4-
* - In grid "0" represent blocked cell and "1" represent empty cell
3+
* - Given a NxN grid, find whether rat in cell [0, 0] can reach the target in cell [N-1, N-1]
4+
* - The grid is represented as an array of rows. Each row is represented as an array of 0 or 1 values.
5+
* - A cell with value 0 can not be moved through. Value 1 means the rat can move here.
6+
* - The rat can not move diagonally.
57
*
6-
* Reference for this problem:
7-
* - https://www.geeksforgeeks.org/rat-in-a-maze-backtracking-2/
8+
* Reference for this problem: https://www.geeksforgeeks.org/rat-in-a-maze-backtracking-2/
9+
*/
10+
11+
/**
12+
* Checks if the given grid is valid.
813
*
14+
* A grid needs to satisfy these conditions:
15+
* - must not be empty
16+
* - must be a square
17+
* - must not contain values other than {@code 0} and {@code 1}
18+
*
19+
* @param grid The grid to check.
20+
* @throws TypeError When the given grid is invalid.
921
*/
22+
function validateGrid (grid) {
23+
if (!Array.isArray(grid) || grid.length === 0) throw new TypeError('Grid must be a non-empty array')
1024

11-
// Helper function to find if current cell is out of boundary
12-
const outOfBoundary = (grid, currentRow, currentColumn) => {
13-
if (currentRow < 0 || currentColumn < 0 || currentRow >= grid.length || currentColumn >= grid[0].length) {
14-
return true
15-
} else {
16-
return false
17-
}
25+
const allRowsHaveCorrectLength = grid.every(row => row.length === grid.length)
26+
if (!allRowsHaveCorrectLength) throw new TypeError('Grid must be a square')
27+
28+
const allCellsHaveValidValues = grid.every(row => {
29+
return row.every(cell => cell === 0 || cell === 1)
30+
})
31+
if (!allCellsHaveValidValues) throw new TypeError('Grid must only contain 0s and 1s')
1832
}
1933

20-
const printPath = (grid, currentRow, currentColumn, path) => {
21-
// If cell is out of boundary, we can't proceed
22-
if (outOfBoundary(grid, currentRow, currentColumn)) return false
34+
function isSafe (grid, x, y) {
35+
const n = grid.length
36+
return x >= 0 && x < n && y >= 0 && y < n && grid[y][x] === 1
37+
}
2338

24-
// If cell is blocked then you can't go ahead
25-
if (grid[currentRow][currentColumn] === 0) return false
39+
/**
40+
* Attempts to calculate the remaining path to the target.
41+
*
42+
* @param grid The full grid.
43+
* @param x The current X coordinate.
44+
* @param y The current Y coordinate.
45+
* @param solution The current solution matrix.
46+
* @param path The path we took to get from the source cell to the current location.
47+
* @returns {string|boolean} Either the path to the target cell or false.
48+
*/
49+
function getPathPart (grid, x, y, solution, path) {
50+
const n = grid.length
2651

27-
// If we reached target cell, then print path
28-
if (currentRow === targetRow && currentColumn === targetColumn) {
29-
console.log(path)
30-
return true
52+
// are we there yet?
53+
if (x === n - 1 && y === n - 1 && grid[y][x] === 1) {
54+
solution[y][x] = 1
55+
return path
3156
}
3257

33-
// R,L,D,U are directions `Right, Left, Down, Up`
34-
const directions = [
35-
[1, 0, 'D'],
36-
[-1, 0, 'U'],
37-
[0, 1, 'R'],
38-
[0, -1, 'L']
39-
]
40-
41-
for (let i = 0; i < directions.length; i++) {
42-
const nextRow = currentRow + directions[i][0]
43-
const nextColumn = currentColumn + directions[i][1]
44-
const updatedPath = path + directions[i][2]
45-
46-
grid[currentRow][currentColumn] = 0
47-
if (printPath(grid, nextRow, nextColumn, updatedPath)) return true
48-
grid[currentRow][currentColumn] = 1
49-
}
58+
// did we step on a 0 cell or outside the grid?
59+
if (!isSafe(grid, x, y)) return false
60+
61+
// are we walking onto an already-marked solution coordinate?
62+
if (solution[y][x] === 1) return false
63+
64+
// none of the above? let's dig deeper!
65+
66+
// mark the current coordinates on the solution matrix
67+
solution[y][x] = 1
68+
69+
// attempt to move right
70+
const right = getPathPart(grid, x + 1, y, solution, path + 'R')
71+
if (right) return right
72+
73+
// right didn't work: attempt to move down
74+
const down = getPathPart(grid, x, y + 1, solution, path + 'D')
75+
if (down) return down
76+
77+
// down didn't work: attempt to move up
78+
const up = getPathPart(grid, x, y - 1, solution, path + 'U')
79+
if (up) return up
80+
81+
// up didn't work: attempt to move left
82+
const left = getPathPart(grid, x - 1, y, solution, path + 'L')
83+
if (left) return left
84+
85+
// no direction was successful: remove this cell from the solution matrix and backtrack
86+
solution[y][x] = 0
5087
return false
5188
}
5289

53-
// Driver Code
90+
function getPath (grid) {
91+
// grid dimensions
92+
const n = grid.length
93+
94+
// prepare solution matrix
95+
const solution = []
96+
for (let i = 0; i < n; i++) {
97+
const row = Array(n)
98+
row.fill(0)
99+
solution[i] = row
100+
}
101+
102+
return getPathPart(grid, 0, 0, solution, '')
103+
}
104+
105+
/**
106+
* Creates an instance of the "rat in a maze" based on a given grid (maze).
107+
*/
108+
export class RatInAMaze {
109+
110+
/** Path from the source [0, 0] to the target [N-1, N-1]. */
111+
#_path = ''
54112

55-
const grid = [
56-
[1, 1, 1, 1],
57-
[1, 0, 0, 1],
58-
[0, 0, 1, 1],
59-
[1, 1, 0, 1]
60-
]
113+
#_solved = false
61114

62-
const targetRow = grid.length - 1
63-
const targetColumn = grid[0].length - 1
115+
constructor (grid) {
116+
// first, let's do some error checking on the input
117+
validateGrid(grid)
64118

65-
// Variation 2 : print a possible path to reach from (0, 0) to (N-1, N-1)
66-
// If there is no path possible then it will print "Not Possible"
67-
!printPath(grid, 0, 0, '') && console.log('Not Possible')
119+
// attempt to solve the maze now - all public methods only query the result state later
120+
const solution = getPath(grid)
121+
122+
if (solution !== false) {
123+
this.#_path = solution
124+
this.#_solved = true
125+
}
126+
}
127+
128+
get solved () {
129+
return this.#_solved
130+
}
131+
132+
get path () {
133+
return this.#_path
134+
}
135+
136+
}

Backtracking/tests/RatInAMaze.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { RatInAMaze } from '../RatInAMaze'
2+
3+
describe('RatInAMaze', () => {
4+
it('should fail for non-arrays', () => {
5+
const values = [undefined, null, {}, 42, 'hello, world']
6+
7+
for (const value of values) {
8+
expect(() => {new RatInAMaze(value)}).toThrow()
9+
}
10+
})
11+
12+
it('should fail for an empty array', () => {
13+
expect(() => {new RatInAMaze([])}).toThrow()
14+
})
15+
16+
it('should fail for a non-square array', () => {
17+
const array = [
18+
[0, 0, 0],
19+
[0, 0]
20+
]
21+
22+
expect(() => {new RatInAMaze(array)}).toThrow()
23+
})
24+
25+
it('should fail for arrays containing invalid values', () => {
26+
const values = [[[2]], [['a']]]
27+
28+
for (const value of values) {
29+
expect(() => {new RatInAMaze(value)}).toThrow()
30+
}
31+
})
32+
33+
it('should work for a single-cell maze', () => {
34+
const maze = new RatInAMaze([[1]])
35+
expect(maze.solved).toBe(true)
36+
expect(maze.path).toBe('')
37+
})
38+
39+
it('should work for a single-cell maze that can not be solved', () => {
40+
const maze = new RatInAMaze([[0]])
41+
expect(maze.solved).toBe(false)
42+
expect(maze.path).toBe('')
43+
})
44+
45+
it('should work for a simple 3x3 maze', () => {
46+
const maze = new RatInAMaze([[1, 1, 0], [0, 1, 0], [0, 1, 1]])
47+
expect(maze.solved).toBe(true)
48+
expect(maze.path).toBe('RDDR')
49+
})
50+
51+
it('should work for a simple 2x2 that can not be solved', () => {
52+
const maze = new RatInAMaze([[1, 0], [0, 1]])
53+
expect(maze.solved).toBe(false)
54+
expect(maze.path).toBe('')
55+
})
56+
57+
it('should work for a more complex maze', () => {
58+
const maze = new RatInAMaze([
59+
[1, 1, 1, 1, 1, 0, 0],
60+
[0, 0, 0, 0, 1, 0, 0],
61+
[1, 1, 1, 0, 1, 0, 0],
62+
[1, 0, 1, 0, 1, 0, 0],
63+
[1, 0, 1, 1, 1, 0, 0],
64+
[1, 0, 0, 0, 0, 0, 0],
65+
[1, 1, 1, 1, 1, 1, 1]
66+
])
67+
expect(maze.solved).toBe(true)
68+
expect(maze.path).toBe('RRRRDDDDLLUULLDDDDRRRRRR')
69+
})
70+
71+
it('should work for a more complex maze that can not be solved', () => {
72+
const maze = new RatInAMaze([
73+
[1, 1, 1, 1, 1, 0, 0],
74+
[0, 0, 0, 0, 1, 0, 0],
75+
[1, 1, 1, 0, 1, 0, 0],
76+
[1, 0, 1, 0, 1, 0, 0],
77+
[1, 0, 1, 0, 1, 0, 0],
78+
[1, 0, 0, 0, 0, 0, 0],
79+
[1, 1, 1, 1, 1, 1, 1]
80+
])
81+
expect(maze.solved).toBe(false)
82+
expect(maze.path).toBe('')
83+
})
84+
})

0 commit comments

Comments
 (0)