The Algorithms - Python Python TheAlgorithms/Python

Backtracking Algorithms in TheAlgorithms/Python

Seven constraint-satisfaction problems solved by the same pattern: try a placement, recurse, and undo it when the branch fails.

7 stops ~14 min Verified 2026-05-05
What you will learn
  • How is_safe checks column, left diagonal, and right diagonal threats for the n-queens problem
  • How sudoku finds the next empty cell and tries digits 1-9, backtracking when no digit satisfies the constraints
  • How the knight tour helper marks a cell, recurses, and sets it back to 0 when the branch fails
  • How rat_in_maze marks a path cell as zero, recurses toward the destination, and unmarks on failure
  • How all_permutations uses an index_used boolean array to track which elements appear in the current prefix
  • How word_search uses a visited-points set keyed by a position hash to avoid revisiting cells
  • How hamiltonian_cycle validates two conditions before extending the path and backtracks by writing -1
Prerequisites
  • Comfort reading recursive Python functions and 2D list indexing
  • Understanding of what a constraint-satisfaction problem is
1 / 7

N-queens: is_safe checks column and both diagonals before placing

backtracking/n_queens.py:16

is_safe returns True only if no queen exists on the same column, left diagonal, or right diagonal above the target row.

The n-queens problem asks for every arrangement of N queens on an N-by-N board where no queen threatens another. Because queens are placed row by row, the only threats can come from rows above the current row. Three all() expressions check the three attack directions. The column check zips range(row) against [column] * row, scanning every cell in the same column from row 0 up to row minus 1. The two diagonal checks walk backward from the row above the target, incrementing or decrementing the column index by 1 per step. All three use generator expressions so evaluation stops on the first conflict found. The solve function calls is_safe before each placement: if it returns True, the queen is placed by setting the cell to 1 and the recursion continues; if the branch fails, the cell is reset to 0.

Key takeaway

is_safe checks three directions with three all() generators: same column above, left diagonal, and right diagonal. All three must pass before a queen is placed.

def is_safe(board: list[list[int]], row: int, column: int) -> bool:
    """
    This function returns a boolean value True if it is safe to place a queen there
    considering the current state of the board.

    Parameters:
    board (2D matrix): The chessboard
    row, column: Coordinates of the cell on the board

    Returns:
    Boolean Value

    >>> is_safe([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1)
    True
    >>> is_safe([[0, 1, 0], [0, 0, 0], [0, 0, 0]], 1, 1)
    False
    >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1)
    False
    >>> is_safe([[0, 0, 1], [0, 0, 0], [0, 0, 0]], 1, 1)
    False
    >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 2)
    True
    >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 2, 1)
    True
    >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 0, 2)
    True
    >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 2, 2)
    True
    """

    n = len(board)  # Size of the board

    # Check if there is any queen in the same upper column,
    # left upper diagonal and right upper diagonal
    return (
        all(board[i][j] != 1 for i, j in zip(range(row), [column] * row))
        and all(
            board[i][j] != 1
            for i, j in zip(range(row - 1, -1, -1), range(column - 1, -1, -1))
2 / 7

Sudoku: find empty cell, try digits 1-9, backtrack on failure

backtracking/sudoku.py:75

sudoku() places each digit in the first empty cell, recurses, and resets the cell to 0 when the recursive call cannot find a solution.

Sudoku uses a walrus operator to combine finding and unpacking the first empty cell in one expression. When find_empty_location returns None, every cell is filled and the grid is returned as the solved result. Otherwise the algorithm enters the trial loop: for each digit 1 through 9, is_safe checks that no duplicate exists in the same row, column, or 3-by-3 box. If the digit is safe, it is written into the grid and the function recurses. A non-None return from the recursive call means the rest of the puzzle was solved, so the result propagates up. A None return means no digit completed the puzzle from this cell, so the cell is reset to 0 and the loop tries the next digit. If no digit works, the function returns None, signaling failure to the caller.

Key takeaway

Sudoku's backtracking is three lines: place a digit if safe, recurse, reset to 0 if the recursion fails. The walrus operator := makes the base case a one-liner.

def sudoku(grid: Matrix) -> Matrix | None:
    """
    Takes a partially filled-in grid and attempts to assign values to
    all unassigned locations in such a way to meet the requirements
    for Sudoku solution (non-duplication across rows, columns, and boxes)

    >>> sudoku(initial_grid)  # doctest: +NORMALIZE_WHITESPACE
    [[3, 1, 6, 5, 7, 8, 4, 9, 2],
     [5, 2, 9, 1, 3, 4, 7, 6, 8],
     [4, 8, 7, 6, 2, 9, 5, 3, 1],
     [2, 6, 3, 4, 1, 5, 9, 8, 7],
     [9, 7, 4, 8, 6, 3, 1, 2, 5],
     [8, 5, 1, 7, 9, 2, 6, 4, 3],
     [1, 3, 8, 9, 4, 7, 2, 5, 6],
     [6, 9, 2, 3, 5, 1, 8, 7, 4],
     [7, 4, 5, 2, 8, 6, 3, 1, 9]]
     >>> sudoku(no_solution) is None
     True
    """
    if location := find_empty_location(grid):
        row, column = location
    else:
        # If the location is ``None``, then the grid is solved.
        return grid

    for digit in range(1, 10):
        if is_safe(grid, row, column, digit):
            grid[row][column] = digit

            if sudoku(grid) is not None:
                return grid

            grid[row][column] = 0

    return None
3 / 7

Knight tour: mark the cell, recurse, reset to 0 on backtrack

backtracking/knight_tour.py:49

The knight tour helper marks each candidate cell with the next move number, recurses, and resets to 0 when the branch exhausts all positions without completing the tour.

The knight tour requires visiting every square on an n-by-n board exactly once using only legal knight moves. The helper function follows the standard backtracking template. The base case checks whether all cells are non-zero; if so, the tour is complete and the function returns True. For each valid knight position reachable from the current cell, the cell is written with the current move number curr + 1. If the recursive call returns True, the solution has been found and True propagates back up. If the recursion fails, the cell is reset to 0, erasing the move, and the loop tries the next reachable position. get_valid_pos filters the eight possible knight offsets against the board boundaries. The outer open_knight_tour function tries every starting cell until one finds a complete tour.

Key takeaway

The knight tour helper is the backtracking template in four lines: check completion, try each valid move, recurse, reset to 0 on failure.

def open_knight_tour_helper(
    board: list[list[int]], pos: tuple[int, int], curr: int
) -> bool:
    """
    Helper function to solve knight tour problem.
    """

    if is_complete(board):
        return True

    for position in get_valid_pos(pos, len(board)):
        y, x = position

        if board[y][x] == 0:
            board[y][x] = curr + 1
            if open_knight_tour_helper(board, position, curr + 1):
                return True
            board[y][x] = 0

    return False
4 / 7

Rat in maze: mark path cell as zero, recurse toward destination, unmark on failure

backtracking/rat_in_maze.py:139

run_maze marks each visited cell with 0 in the solution matrix, recurses in four directions, and backtracks by restoring the marker when the branch fails.

Rat in maze navigates a grid where 0 cells are open paths and 1 cells are walls. The solutions matrix starts as all 1s; each cell on the successful path is set to 0 to mark it. The base case at line 159 checks whether the current cell is the destination and is open; if so, it is marked in solutions and the function returns True. The rest of the function (not shown in this slice) checks boundary conditions, verifies the current cell is accessible, marks it in solutions, and recurses in four directions: down, right, up, left. If all four recursive calls fail, the cell is restored to 1 and the function returns False. Unlike knight tour, this problem separates the input maze (read-only) from the solutions matrix (write-only), making it clear which grid represents the problem and which represents the solution path being built.

Key takeaway

Rat in maze reads maze to check walls and writes solutions to mark the path; backtracking restores the solutions cell to 1.

def run_maze(
    maze: list[list[int]],
    i: int,
    j: int,
    destination_row: int,
    destination_column: int,
    solutions: list[list[int]],
) -> bool:
    """
    This method is recursive starting from (i, j) and going in one of four directions:
    up, down, left, right.
    If a path is found to destination it returns True otherwise it returns False.
    Parameters
        maze: A two dimensional matrix of zeros and ones.
        i, j : coordinates of matrix
        solutions: A two dimensional matrix of solutions.
    Returns:
        Boolean if path is found True, Otherwise False.
    """
    size = len(maze)
    # Final check point.
    if i == destination_row and j == destination_column and maze[i][j] == 0:
        solutions[i][j] = 0
        return True
5 / 7

All permutations: an index_used array tracks which elements are in the current prefix

backtracking/all_permutations.py:16

A boolean index_used array marks which sequence positions have been selected into the current permutation prefix, preventing reuse without sorting.

Generating all permutations is a classic backtracking problem because each prefix constrains which elements can extend it. The index_used list at line 20 of the caller starts as all zeros (0), used as falsy values, one entry per sequence position. Inside the loop, only positions where index_used[i] is False are eligible. Choosing position i sets index_used[i] = True, appends sequence[i] to current_sequence, and recurses with index + 1. When the recursion returns, current_sequence.pop() removes the last element and index_used[i] = False makes position i available again. This structure produces the n! permutations in lexicographic order if the input is sorted, and in the input order otherwise. The doctest confirms six permutations of [1, 2, 3].

Key takeaway

The index_used boolean array is the constraint: set it to True before recursing to prevent reuse, reset it to False after to make the element available to other branches.

def create_state_space_tree(
    sequence: list[int | str],
    current_sequence: list[int | str],
    index: int,
    index_used: list[int],
) -> None:
    """
    Creates a state space tree to iterate through each branch using DFS.
    We know that each state has exactly len(sequence) - index children.
    It terminates when it reaches the end of the given sequence.

    :param sequence: The input sequence for which permutations are generated.
    :param current_sequence: The current permutation being built.
    :param index: The current index in the sequence.
    :param index_used: list to track which elements are used in permutation.

    Example 1:
    >>> sequence = [1, 2, 3]
    >>> current_sequence = []
    >>> index_used = [False, False, False]
    >>> create_state_space_tree(sequence, current_sequence, 0, index_used)
    [1, 2, 3]
    [1, 3, 2]
    [2, 1, 3]
    [2, 3, 1]
    [3, 1, 2]
    [3, 2, 1]

    Example 2:
    >>> sequence = ["A", "B", "C"]
    >>> current_sequence = []
    >>> index_used = [False, False, False]
    >>> create_state_space_tree(sequence, current_sequence, 0, index_used)
    ['A', 'B', 'C']
    ['A', 'C', 'B']
    ['B', 'A', 'C']
    ['B', 'C', 'A']
    ['C', 'A', 'B']
    ['C', 'B', 'A']
6 / 7

Word search: a visited-points set keyed by position hash prevents cell reuse

backtracking/word_search.py:47

A set of position hashes tracks which cells are on the current path, preventing the same cell from being used twice in one word match.

Word search adds one mechanism on top of the standard backtracking template: a set of integer position keys that represents the current path. Without it, the recursion could cycle back through a cell it already used to match an earlier character. get_point_key encodes a cell's row and column as a single integer using len_board times len_board_column times row + column, guaranteeing a unique key for each board cell. The letter check at line 63 prunes branches immediately: if the current cell does not match the next character, the function returns False before touching the visited set. For each valid unvisited neighbor, the key is added to visited_points_set before recursing and removed after the recursive call returns, restoring the state for the next neighbor. The outer word_exists function tries every cell as the starting position.

Key takeaway

Word search tracks visited cells with a position-hash set: add the hash before recursing and remove it after, so each branch only sees the cells on its own path.

def exits_word(
    board: list[list[str]],
    word: str,
    row: int,
    column: int,
    word_index: int,
    visited_points_set: set[int],
) -> bool:
    """
    Return True if it's possible to search the word suffix
    starting from the word_index.

    >>> exits_word([["A"]], "B", 0, 0, 0, set())
    False
    """

    if board[row][column] != word[word_index]:
        return False

    if word_index == len(word) - 1:
        return True

    traverts_directions = [(0, 1), (0, -1), (-1, 0), (1, 0)]
    len_board = len(board)
    len_board_column = len(board[0])
    for direction in traverts_directions:
        next_i = row + direction[0]
        next_j = column + direction[1]
        if not (0 <= next_i < len_board and 0 <= next_j < len_board_column):
            continue

        key = get_point_key(len_board, len_board_column, next_i, next_j)
        if key in visited_points_set:
            continue

        visited_points_set.add(key)
        if exits_word(board, word, next_i, next_j, word_index + 1, visited_points_set):
            return True

        visited_points_set.remove(key)
7 / 7

Hamiltonian cycle: two-condition validation gates each path extension

backtracking/hamiltonian_cycle.py:49

util_hamilton_cycle extends the path only when valid_connection confirms both an edge and a fresh vertex, then backtracks by writing -1 to the path slot.

A Hamiltonian cycle visits every vertex exactly once and returns to the start. The path is represented as a list initialized with the start vertex in position 0 and -1 in all other positions. The base case at line 80 checks whether all n vertices have been placed: if so, it returns True only if an edge exists from the last placed vertex back to the start, closing the cycle. The recursive step iterates over every vertex and calls valid_connection, which checks two things: the adjacency matrix entry for the current-to-next edge must be 1, and the next vertex must not already appear in the path. When both hold, the vertex is committed by writing it into path[curr_ind]. If the recursion fails, the slot is reset to -1. The doctest confirms that for a five-vertex graph the found cycle is [0, 1, 2, 4, 3, 0].

Key takeaway

Hamiltonian cycle backtracks by writing -1 into the path slot; valid_connection enforces two gates before any vertex is committed: edge must exist, vertex must be unseen.

def util_hamilton_cycle(graph: list[list[int]], path: list[int], curr_ind: int) -> bool:
    """
    Pseudo-Code
    Base Case:
    1. Check if we visited all of vertices
        1.1 If last visited vertex has path to starting vertex return True either
            return False
    Recursive Step:
    2. Iterate over each vertex
        Check if next vertex is valid for transiting from current vertex
            2.1 Remember next vertex as next transition
            2.2 Do recursive call and check if going to this vertex solves problem
            2.3 If next vertex leads to solution return True
            2.4 Else backtrack, delete remembered vertex

    Case 1: Use exact graph as in main function, with initialized values
    >>> graph = [[0, 1, 0, 1, 0],
    ...          [1, 0, 1, 1, 1],
    ...          [0, 1, 0, 0, 1],
    ...          [1, 1, 0, 0, 1],
    ...          [0, 1, 1, 1, 0]]
    >>> path = [0, -1, -1, -1, -1, 0]
    >>> curr_ind = 1
    >>> util_hamilton_cycle(graph, path, curr_ind)
    True
    >>> path
    [0, 1, 2, 4, 3, 0]

    Case 2: Use exact graph as in previous case, but in the properties taken from
        middle of calculation
    >>> graph = [[0, 1, 0, 1, 0],
    ...          [1, 0, 1, 1, 1],
    ...          [0, 1, 0, 0, 1],
    ...          [1, 1, 0, 0, 1],
    ...          [0, 1, 1, 1, 0]]
    >>> path = [0, 1, 2, -1, -1, 0]
    >>> curr_ind = 3
    >>> util_hamilton_cycle(graph, path, curr_ind)
    True
    >>> path
    [0, 1, 2, 4, 3, 0]
    """

    # Base Case
    if curr_ind == len(graph):
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free