Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 84 additions & 78 deletions src/main/java/com/thealgorithms/backtracking/MazeRecursion.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,122 +4,128 @@
* This class contains methods to solve a maze using recursive backtracking.
* The maze is represented as a 2D array where walls, paths, and visited/dead
* ends are marked with different integers.
*
* The goal is to find a path from a starting position to the target position
* (map[6][5]) while navigating through the maze.
*/
public final class MazeRecursion {
private static final int[][] FIRST_STRATEGY_DIRECTIONS = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
private static final int[][] SECOND_STRATEGY_DIRECTIONS = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};

private MazeRecursion() {
}

/**
* This method solves the maze using the "down -> right -> up -> left"
* movement strategy.
* Solves the maze using the "down -> right -> up -> left" movement strategy,
* starting at (1, 1) and targeting (6, 5).
*
* @param map The 2D array representing the maze (walls, paths, etc.)
* @return The solved maze with paths marked, or null if no solution exists.
* @param map the 2D array representing the maze (walls, paths, etc.)
* @return the solved maze with paths marked, or null if no solution exists or a fixed endpoint is outside the maze
* @throws IllegalArgumentException if the maze is null, empty, or not rectangular
*/
public static int[][] solveMazeUsingFirstStrategy(int[][] map) {
if (setWay(map, 1, 1)) {
validateMaze(map);
if (!isInBounds(map, 1, 1) || !isInBounds(map, 6, 5)) {
return null;
}
if (map[6][5] == 2) {
return map;
}
return null;
return solve(map, 1, 1, 6, 5, FIRST_STRATEGY_DIRECTIONS) ? map : null;
}

/**
* This method solves the maze using the "up -> right -> down -> left"
* movement strategy.
* Solves the maze using the "up -> right -> down -> left" movement strategy,
* starting at (1, 1) and targeting (6, 5).
*
* @param map The 2D array representing the maze (walls, paths, etc.)
* @return The solved maze with paths marked, or null if no solution exists.
* @param map the 2D array representing the maze (walls, paths, etc.)
* @return the solved maze with paths marked, or null if no solution exists or a fixed endpoint is outside the maze
* @throws IllegalArgumentException if the maze is null, empty, or not rectangular
*/
public static int[][] solveMazeUsingSecondStrategy(int[][] map) {
if (setWay2(map, 1, 1)) {
validateMaze(map);
if (!isInBounds(map, 1, 1) || !isInBounds(map, 6, 5)) {
return null;
}
if (map[6][5] == 2) {
return map;
}
return null;
return solve(map, 1, 1, 6, 5, SECOND_STRATEGY_DIRECTIONS) ? map : null;
}

/**
* Attempts to find a path through the maze using a "down -> right -> up -> left"
* movement strategy. The path is marked with '2' for valid paths and '3' for dead ends.
* Solves the maze using the "down -> right -> up -> left" movement strategy.
*
* @param map The 2D array representing the maze (walls, paths, etc.)
* @param i The current x-coordinate of the ball (row index)
* @param j The current y-coordinate of the ball (column index)
* @return True if a path is found to (6,5), otherwise false
* @param map the 2D array representing the maze (0 = open, non-zero = blocked)
* @param startRow the starting row
* @param startColumn the starting column
* @param goalRow the goal row
* @param goalColumn the goal column
* @return the solved maze with paths marked, or null if no solution exists
* @throws IllegalArgumentException if the maze is null, empty, not rectangular,
* or a coordinate is outside the maze
*/
private static boolean setWay(int[][] map, int i, int j) {
if (map[6][5] == 2) {
return true;
public static int[][] solveMazeUsingFirstStrategy(int[][] map, int startRow, int startColumn, int goalRow, int goalColumn) {
validateCoordinates(map, startRow, startColumn, goalRow, goalColumn);
return solve(map, startRow, startColumn, goalRow, goalColumn, FIRST_STRATEGY_DIRECTIONS) ? map : null;
}

/**
* Solves the maze using the "up -> right -> down -> left" movement strategy.
*
* @param map the 2D array representing the maze (0 = open, non-zero = blocked)
* @param startRow the starting row
* @param startColumn the starting column
* @param goalRow the goal row
* @param goalColumn the goal column
* @return the solved maze with paths marked, or null if no solution exists
* @throws IllegalArgumentException if the maze is null, empty, not rectangular,
* or a coordinate is outside the maze
*/
public static int[][] solveMazeUsingSecondStrategy(int[][] map, int startRow, int startColumn, int goalRow, int goalColumn) {
validateCoordinates(map, startRow, startColumn, goalRow, goalColumn);
return solve(map, startRow, startColumn, goalRow, goalColumn, SECOND_STRATEGY_DIRECTIONS) ? map : null;
}

private static boolean solve(int[][] map, int row, int column, int goalRow, int goalColumn, int[][] directions) {
if (!isInBounds(map, row, column) || map[row][column] != 0) {
return false;
}

// If the current position is unvisited (0), explore it
if (map[i][j] == 0) {
// Mark the current position as '2'
map[i][j] = 2;
map[row][column] = 2;
if (row == goalRow && column == goalColumn) {
return true;
}

// Move down
if (setWay(map, i + 1, j)) {
return true;
}
// Move right
else if (setWay(map, i, j + 1)) {
return true;
}
// Move up
else if (setWay(map, i - 1, j)) {
return true;
}
// Move left
else if (setWay(map, i, j - 1)) {
for (int[] direction : directions) {
if (solve(map, row + direction[0], column + direction[1], goalRow, goalColumn, directions)) {
return true;
}

map[i][j] = 3; // Mark as dead end (3) if no direction worked
return false;
}

map[row][column] = 3;
return false;
}

/**
* Attempts to find a path through the maze using an alternative movement
* strategy "up -> right -> down -> left".
*
* @param map The 2D array representing the maze (walls, paths, etc.)
* @param i The current x-coordinate of the ball (row index)
* @param j The current y-coordinate of the ball (column index)
* @return True if a path is found to (6,5), otherwise false
*/
private static boolean setWay2(int[][] map, int i, int j) {
if (map[6][5] == 2) {
return true;
private static int validateMaze(int[][] map) {
if (map == null || map.length == 0 || map[0] == null || map[0].length == 0) {
throw new IllegalArgumentException("Maze must not be null or empty.");
}

if (map[i][j] == 0) {
map[i][j] = 2;

// Move up
if (setWay2(map, i - 1, j)) {
return true;
}
// Move right
else if (setWay2(map, i, j + 1)) {
return true;
}
// Move down
else if (setWay2(map, i + 1, j)) {
return true;
}
// Move left
else if (setWay2(map, i, j - 1)) {
return true;
int columnCount = map[0].length;
for (int[] row : map) {
if (row == null || row.length != columnCount) {
throw new IllegalArgumentException("Maze must be a non-empty rectangular matrix.");
}
}
return columnCount;
}

map[i][j] = 3; // Mark as dead end (3) if no direction worked
return false;
private static void validateCoordinates(int[][] map, int startRow, int startColumn, int goalRow, int goalColumn) {
int columnCount = validateMaze(map);
if (startRow < 0 || startRow >= map.length || startColumn < 0 || startColumn >= columnCount || goalRow < 0 || goalRow >= map.length || goalColumn < 0 || goalColumn >= columnCount) {
throw new IllegalArgumentException("Start and goal coordinates must be inside the maze.");
}
return false;
}

private static boolean isInBounds(int[][] map, int row, int column) {
return row >= 0 && row < map.length && column >= 0 && column < map[row].length;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.thealgorithms.backtracking;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

Expand All @@ -10,6 +13,102 @@
*/
public class MazeRecursionTest {

@Test
public void testSmallMazeWithExplicitCoordinates() {
int[][] firstStrategyMaze = {{0, 0}, {0, 0}};
int[][] secondStrategyMaze = {{0, 0}, {0, 0}};

assertArrayEquals(new int[][] {{2, 0}, {2, 2}}, MazeRecursion.solveMazeUsingFirstStrategy(firstStrategyMaze, 0, 0, 1, 1));
assertArrayEquals(new int[][] {{2, 2}, {0, 2}}, MazeRecursion.solveMazeUsingSecondStrategy(secondStrategyMaze, 0, 0, 1, 1));
}

@Test
public void testLegacyMethodsReturnNullWhenFixedGoalIsOutsideMaze() {
assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[1][1]));
assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[1][1]));
assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[2][2]));
assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[2][2]));
}

@Test
public void testLegacyMethodsReturnNullWhenFixedGoalRowOrColumnIsOutsideMaze() {
assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[7][5]));
assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[6][6]));
}

@Test
public void testLegacyMethodsReturnNullWhenNoPathExists() {
int[][] firstStrategyMaze = new int[7][6];
int[][] secondStrategyMaze = new int[7][6];
firstStrategyMaze[1][1] = 1;
secondStrategyMaze[1][1] = 1;

assertNull(MazeRecursion.solveMazeUsingFirstStrategy(firstStrategyMaze));
assertNull(MazeRecursion.solveMazeUsingSecondStrategy(secondStrategyMaze));
}

@Test
public void testLegacyMethodsAcceptAlreadyMarkedGoal() {
int[][] firstStrategyMaze = new int[7][6];
int[][] secondStrategyMaze = new int[7][6];
firstStrategyMaze[6][5] = 2;
secondStrategyMaze[6][5] = 2;

assertSame(firstStrategyMaze, MazeRecursion.solveMazeUsingFirstStrategy(firstStrategyMaze));
assertSame(secondStrategyMaze, MazeRecursion.solveMazeUsingSecondStrategy(secondStrategyMaze));
}

@Test
public void testNoPathExistsWithExplicitCoordinates() {
int[][] maze = {{0, 1}, {1, 0}};

assertNull(MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 1, 1));
}

@Test
public void testExplicitCoordinatesMustBeInsideMaze() {
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[2][2], 0, 0, 2, 1));
}

@Test
public void testEachStartAndGoalCoordinateMustBeInsideMaze() {
int[][] maze = new int[2][2];

assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, -1, 0, 1, 1));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 2, 0, 1, 1));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, -1, 1, 1));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 2, 1, 1));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, -1, 1));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 2, 1));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 1, -1));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(maze, 0, 0, 1, 2));
}

@Test
public void testMazeMustNotBeNullOrEmpty() {
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(null));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[0][0]));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {null}));
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {new int[0]}));
}

@Test
public void testMazeMustBeRectangular() {
assertThrows(IllegalArgumentException.class, () -> MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {{0, 0}, {0}}, 0, 0, 1, 0));
}

@Test
public void testBlockedStartAndGoalHaveNoSolution() {
assertNull(MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {{1, 0}, {0, 0}}, 0, 0, 1, 1));
assertNull(MazeRecursion.solveMazeUsingSecondStrategy(new int[][] {{0, 0}, {0, 1}}, 0, 0, 1, 1));
}

@Test
public void testOpenSingleCellMazeIsAlreadySolved() {
assertArrayEquals(new int[][] {{2}}, MazeRecursion.solveMazeUsingFirstStrategy(new int[][] {{0}}, 0, 0, 0, 0));
assertArrayEquals(new int[][] {{2}}, MazeRecursion.solveMazeUsingSecondStrategy(new int[][] {{0}}, 0, 0, 0, 0));
}

@Test
public void testSolveMazeUsingFirstAndSecondStrategy() {
int[][] map = new int[8][7];
Expand Down
Loading