Back to blog
Interview Prep

Mastering Backtracking LeetCode Problems in Java

Master backtracking LeetCode problems in Java with our standardized template and 10 step-by-step classic problem walk-throughs.

CloakAI Editorial Team
September 13, 2026

Backtracking LeetCode problems in Java can be resolved systematically by utilizing a standardized "choose-explore-unchoose" recursive template that tracks dynamic state path candidates and snapshots valid outcomes. Implementing early constraint checks (pruning) allows your code to abort fruitless branches immediately, avoiding unnecessary recursive computations. Developing this structured approach is crucial for optimizing time and space complexities during rigorous technical interviews.

TL;DR: Key Takeaways

  • The Backtracking Blueprint: A core state machine cycle of "choose, explore, unchoose" allows you to build solution paths and undo decisions smoothly during recursion.
  • Dynamic Snapshots: Always deep-copy the path list (e.g., new ArrayList<>(currentPath)) before saving a solution to avoid registering empty lists.
  • Subsets & Combinations: Use a dynamic startIndex parameter in recursive calls to prevent generating duplicate permutations of the same elements.
  • Permutations Framework: Run iterations starting from index 0 and manage visited indices via a high-performance boolean[] used tracker.
  • Pruning & Duplicate Checks: Sorting candidate arrays beforehand enables simple O(1) duplicate element skipping and early pruning when targets are exceeded.
  • Real-Time Confidence: Relying on an invisible helper like CloakAI during mock preparations streamlines your memory retrieval and keeps decision anxiety at bay.

If you are preparing for technical interviews, mastering backtracking leetcode problems in java is one of the most high-leverage skills you can develop. It is common to experience anxiety or decision fatigue when faced with multiple branching recursive states, but learning how to reduce decision fatigue in coding interviews can completely change your outlook and performance. By reducing the diverse array of problems into standard behavioral categories, you can write clean, bug-free solutions under pressure.


What is the standard backtracking template in Java?

Backtracking is essentially a controlled depth-first search (DFS) over a state-space tree of candidate solutions. Instead of building every possible combination from scratch, the algorithm builds a candidate incrementally, and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot possibly be completed to a valid solution.

A standard Java backtracking recursive function typically executes in O(N!) or O(2^N) time complexity, making early pruning at the beginning of the loop essential.

Here is the general architectural skeleton used to solve these problems:

public class BacktrackingTemplate {
    public void solve(int[] input) {
        List<List<Integer>> results = new ArrayList<>();
        List<Integer> currentPath = new ArrayList<>();
        backtrack(results, currentPath, input, 0);
    }
    
    private void backtrack(List<List<Integer>> results, List<Integer> currentPath, int[] input, int startIndex) {
        // 1. Base case / Snapshot recording
        if (isValidSolution(currentPath)) {
            results.add(new ArrayList<>(currentPath)); // Deep copy is mandatory
            return;
        }
        
        for (int i = startIndex; i < input.length; i++) {
            // 2. Prune / Skip duplicates
            if (shouldPrune(input, i, startIndex)) {
                continue;
            }
            
            // 3. Choose
            currentPath.add(input[i]);
            
            // 4. Explore
            backtrack(results, currentPath, input, i + 1);
            
            // 5. Unchoose (Backtrack)
            currentPath.remove(currentPath.size() - 1);
        }
    }
}

How do you solve subset and combination search problems?

Subset and combination problems require searching for groups of elements where the relative order of elements does not matter. The critical strategy here is utilizing a startIndex parameter to ensure that we only look forward in the array, thereby preventing redundant configurations.

When solving Combination Sum II (LeetCode 40), sorting the input array first is an absolute prerequisite to achieve an efficient O(K * 2^N) level-deduplication runtime.

LeetCode Category Start Index Behavior Duplicate Handling Strategy Time Complexity Typical Problem
Subsets Moves forward (i + 1) Skip equal elements if i > start $O(N \cdot 2^N)$ Subsets II (LeetCode 90)
Combinations Same index (i) or next (i + 1) Sort and early prune if element > target $O(K \cdot 2^N)$ Combination Sum (LeetCode 39)
Permutations Restarts at 0 every level Use boolean[] used array tracker $O(N \cdot N!)$ Permutations II (LeetCode 47)
Board Search Contextual/coordinate-based Track coordinates, block lists, or diagonals $O(4^N)$ or $O(N!)$ Word Search, N-Queens

1. Subsets (LeetCode 78)

Generate all possible subsets (the power set) of a set of unique integers.

import java.util.ArrayList;
import java.util.List;

public class SubsetsSolver {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> output = new ArrayList<>();
        generateSubsets(0, nums, new ArrayList<>(), output);
        return output;
    }

    private void generateSubsets(int index, int[] nums, List<Integer> current, List<List<Integer>> output) {
        output.add(new ArrayList<>(current));
        for (int i = index; i < nums.length; i++) {
            current.add(nums[i]);
            generateSubsets(i + 1, nums, current, output);
            current.remove(current.size() - 1);
        }
    }
}

2. Subsets II with Duplicates (LeetCode 90)

When the input array can contain duplicate values, sort the array first and skip identical elements that appear at the same recursion depth.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SubsetsIISolver {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> output = new ArrayList<>();
        Arrays.sort(nums); // Group duplicates together
        findUniqueSubsets(0, nums, new ArrayList<>(), output);
        return output;
    }

    private void findUniqueSubsets(int index, int[] nums, List<Integer> current, List<List<Integer>> output) {
        output.add(new ArrayList<>(current));
        for (int i = index; i < nums.length; i++) {
            // Skip duplicates at the same level of recursion
            if (i > index && nums[i] == nums[i - 1]) {
                continue;
            }
            current.add(nums[i]);
            findUniqueSubsets(i + 1, nums, current, output);
            current.remove(current.size() - 1);
        }
    }
}

3. Combination Sum (LeetCode 39)

Find all unique combinations of candidates that sum up to a target, where the same candidate number can be chosen an unlimited number of times.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CombinationSumSolver {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> output = new ArrayList<>();
        Arrays.sort(candidates); // Enable early pruning
        searchCombinations(0, candidates, target, new ArrayList<>(), output);
        return output;
    }

    private void searchCombinations(int index, int[] candidates, int remaining, List<Integer> current, List<List<Integer>> output) {
        if (remaining == 0) {
            output.add(new ArrayList<>(current));
            return;
        }
        for (int i = index; i < candidates.length; i++) {
            // Prune remaining tree branch if current candidate exceeds remaining target
            if (candidates[i] > remaining) {
                break;
            }
            current.add(candidates[i]);
            // Notice: 'i' is passed as the index because we can reuse elements
            searchCombinations(i, candidates, remaining - candidates[i], current, output);
            current.remove(current.size() - 1);
        }
    }
}

4. Combination Sum II (LeetCode 40)

Find all unique combinations where each candidate number can only be used once, and the input may contain duplicate numbers.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CombinationSumIISolver {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> output = new ArrayList<>();
        Arrays.sort(candidates); // Required for grouping and pruning
        collectUniqueCombinations(0, candidates, target, new ArrayList<>(), output);
        return output;
    }

    private void collectUniqueCombinations(int index, int[] candidates, int remaining, List<Integer> current, List<List<Integer>> output) {
        if (remaining == 0) {
            output.add(new ArrayList<>(current));
            return;
        }
        for (int i = index; i < candidates.length; i++) {
            // Level-based deduplication
            if (i > index && candidates[i] == candidates[i - 1]) {
                continue;
            }
            // Pruning
            if (candidates[i] > remaining) {
                break;
            }
            current.add(candidates[i]);
            collectUniqueCombinations(i + 1, candidates, remaining - candidates[i], current, output);
            current.remove(current.size() - 1);
        }
    }
}

How do you solve permutation LeetCode problems in Java?

In permutation problems, the relative order of elements matters. Therefore, instead of using a startIndex parameter to scan strictly forward, we must always loop from index 0 up to N - 1 on every recursive call. To avoid picking the same element multiple times, we track active elements along our current branch.

Using a primitive boolean array instead of a dynamic lookup set to track visited indices in Permutations (LeetCode 46) reduces auxiliary space overhead to exactly O(N).

5. Permutations with Unique Inputs (LeetCode 46)

Generate all possible permutations of an array of distinct integers.

import java.util.ArrayList;
import java.util.List;

public class PermutationsSolver {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> output = new ArrayList<>();
        boolean[] used = new boolean[nums.length];
        generatePermutations(nums, used, new ArrayList<>(), output);
        return output;
    }

    private void generatePermutations(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> output) {
        if (current.size() == nums.length) {
            output.add(new ArrayList<>(current));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) {
                continue;
            }
            used[i] = true;
            current.add(nums[i]);
            generatePermutations(nums, used, current, output);
            current.remove(current.size() - 1);
            used[i] = false;
        }
    }
}

6. Permutations II with Duplicates (LeetCode 47)

Generate all unique permutations when the input array has duplicate integers.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class PermutationsIISolver {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> output = new ArrayList<>();
        Arrays.sort(nums); // Group duplicates
        boolean[] used = new boolean[nums.length];
        generateUniquePermutations(nums, used, new ArrayList<>(), output);
        return output;
    }

    private void generateUniquePermutations(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> output) {
        if (current.size() == nums.length) {
            output.add(new ArrayList<>(current));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) {
                continue;
            }
            // Skip duplicate permutations:
            // If the current element is equal to the previous element, and the previous
            // element has NOT been used in the current recursive step, skip to avoid duplicates.
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }
            used[i] = true;
            current.add(nums[i]);
            generateUniquePermutations(nums, used, current, output);
            current.remove(current.size() - 1);
            used[i] = false;
        }
    }
}

How do you solve complex board search and constraint-satisfaction problems?

Some of the most challenging backtracking problems involve grid traversals or complex multi-dimensional constraint puzzles. These require checking coordinate safety and reverting board configurations when a path fails.

For the classic N-Queens puzzle on an 8x8 chessboard, using diagonal lookup arrays with mapping formulas of 'r + c' and 'r - c + n' provides O(1) safety checks for every placement choice.

7. Palindrome Partitioning (LeetCode 131)

Partition a string such that every substring of the partition is a palindrome.

import java.util.ArrayList;
import java.util.List;

public class PalindromePartitioningSolver {
    public List<List<String>> partition(String s) {
        List<List<String>> output = new ArrayList<>();
        findPalindromePartitions(0, s, new ArrayList<>(), output);
        return output;
    }

    private void findPalindromePartitions(int start, String s, List<String> current, List<List<String>> output) {
        if (start == s.length()) {
            output.add(new ArrayList<>(current));
            return;
        }
        for (int end = start; end < s.length(); end++) {
            if (isPalindrome(s, start, end)) {
                current.add(s.substring(start, end + 1));
                findPalindromePartitions(end + 1, s, current, output);
                current.remove(current.size() - 1);
            }
        }
    }

    private boolean isPalindrome(String s, int left, int right) {
        while (left < right) {
            if (s.charAt(left++) != s.charAt(right--)) {
                return false;
            }
        }
        return true;
    }
}

8. Solve N-Queens (LeetCode 51)

Place $N$ queens on an $N \times N$ chessboard such that no two queens attack each other.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class NQueensSolver {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> output = new ArrayList<>();
        char[][] board = new char[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(board[i], '.');
        }
        boolean[] cols = new boolean[n];
        boolean[] diag1 = new boolean[2 * n]; // row + col
        boolean[] diag2 = new boolean[2 * n]; // row - col + n
        placeQueens(0, n, board, cols, diag1, diag2, output);
        return output;
    }

    private void placeQueens(int row, int n, char[][] board, boolean[] cols, boolean[] diag1, boolean[] diag2, List<List<String>> output) {
        if (row == n) {
            output.add(constructBoard(board));
            return;
        }
        for (int col = 0; col < n; col++) {
            int d1 = row + col;
            int d2 = row - col + n;
            if (cols[col] || diag1[d1] || diag2[d2]) {
                continue;
            }
            // Choose
            board[row][col] = 'Q';
            cols[col] = diag1[d1] = diag2[d2] = true;

            // Explore
            placeQueens(row + 1, n, board, cols, diag1, diag2, output);

            // Unchoose
            board[row][col] = '.';
            cols[col] = diag1[d1] = diag2[d2] = false;
        }
    }

    private List<String> constructBoard(char[][] board) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < board.length; i++) {
            list.add(new String(board[i]));
        }
        return list;
    }
}

9. Word Search (LeetCode 79)

Find if a word exists in a 2D grid of characters by moving to horizontally or vertically adjacent cells.

public class WordSearchSolver {
    public boolean exist(char[][] board, String word) {
        int rows = board.length;
        int cols = board[0].length;
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (board[r][c] == word.charAt(0)) {
                    if (searchGrid(r, c, 0, board, word)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean searchGrid(int r, int c, int index, char[][] board, String word) {
        if (index == word.length()) {
            return true;
        }
        if (r < 0 || r >= board.length || c < 0 || c >= board[0].length || board[r][c] != word.charAt(index)) {
            return false;
        }
        
        char temp = board[r][c];
        board[r][c] = '#'; // Temporarily mark cell as visited to prevent reuse
        
        // Explore up, down, left, and right directions
        boolean found = searchGrid(r + 1, c, index + 1, board, word) ||
                        searchGrid(r - 1, c, index + 1, board, word) ||
                        searchGrid(r, c + 1, index + 1, board, word) ||
                        searchGrid(r, c - 1, index + 1, board, word);
                        
        board[r][c] = temp; // Unchoose / Restore state
        return found;
    }
}

10. Generate Parentheses (LeetCode 22)

Given $N$ pairs of parentheses, generate all combinations of well-formed parentheses.

import java.util.ArrayList;
import java.util.List;

public class ParenthesesGenerator {
    public List<String> generateParenthesis(int n) {
        List<String> output = new ArrayList<>();
        buildParentheses(n, 0, 0, new StringBuilder(), output);
        return output;
    }

    private void buildParentheses(int max, int open, int close, StringBuilder current, List<String> output) {
        if (current.length() == max * 2) {
            output.add(current.toString());
            return;
        }
        // Only append open brackets if we have not reached the limit
        if (open < max) {
            current.append('(');
            buildParentheses(max, open + 1, close, current, output);
            current.deleteCharAt(current.length() - 1); // Backtrack
        }
        // Only append close brackets if they are fewer than open brackets
        if (close < open) {
            current.append(')');
            buildParentheses(max, open, close + 1, current, output);
            current.deleteCharAt(current.length() - 1); // Backtrack
        }
    }
}

What are the best strategies for mastering backtracking leetcode problems in java?

To master backtracking problems without memorizing every single LeetCode variation, you should focus on classifying problems by how they form their search trees. Keep a clear trace of recursion depth, base cases, and exactly what elements are valid choices at any given level.

Using an advanced tool like CloakAI during mock sessions helps you practice recognizing backtracking patterns without the pressure of live coding round anxiety. As the best invisible AI coding copilot for technical interviews, CloakAI assists you in structuring complex recursive algorithms and debugging base-case mistakes in real-time, working silently alongside your IDE to optimize performance.


FAQ

Q: How does backtracking differ from standard depth-first search (DFS)? A: Backtracking is a specific form of DFS where you actively modify and revert the state of a shared search path. While general DFS visits all nodes in a tree or graph, backtracking systematically discards paths (pruning) and undoes decisions ("unchoosing") to search for specific valid configurations.

Q: Why does my backtracking result list contain only empty lists in Java? A: This happens when you add your path reference directly to the results list (e.g., results.add(path)) instead of creating a deep copy. Since the path is cleared or modified during backtracking, all added references will end up pointing to the final empty state of the path; always write results.add(new ArrayList<>(path)) to capture a valid snapshot.

Q: What is the general time complexity of a backtracking algorithm? A: Backtracking algorithms usually have exponential or factorial time complexities. Generating subsets takes $O(N \cdot 2^N)$ time, calculating permutations takes $O(N \cdot N!)$ time, and exploring search grids takes $O(4^N)$ time, where $N$ represents the size of the input elements or grid size.

Q: How do you identify if a problem should be solved using backtracking? A: You should look for keywords in the problem description like "find all subsets," "generate all permutations," "find all combinations," or constraint-satisfaction problems like board placements. If the solution requires exploring all possible configurations and building paths step-by-step, backtracking is the correct choice.

Q: How can I optimize a backtracking algorithm that is timing out? A: The most effective optimization is early pruning, which involves sorting your input array to break out of loops as soon as target limits are exceeded, or using fast lookup arrays (like boolean trackers for board rows/diagonals) to evaluate safety checks in $O(1)$ time.

Enjoyed this article?

Subscribe to get more insights on interview strategies and AI tools delivered to your inbox.