Skip to content
DSA recursion 9 min read

Common Backtracking Problems: Permutations, Combinations & N-Queens

The backtracking templatechoose, explore, un-choose — is most easily learned by applying it to the classics. This page walks through three canonical problems: permutations (all orderings), combinations (choose k of n), and N-Queens (a constraint puzzle). Each reuses the same skeleton; only the choices, the validity check, and the “complete” condition change.

Permutations

A permutation is an ordering of all elements. [1, 2, 3] has 3! = 6 permutations. The choice at each step is “which unused element goes next?” We mark elements used on the way down and un-mark them on the way back up.

void permute(std::vector<int>& nums, std::vector<bool>& used,
             std::vector<int>& current,
             std::vector<std::vector<int>>& result) {
    if (current.size() == nums.size()) {
        result.push_back(current);          // a full ordering
        return;
    }
    for (int i = 0; i < (int)nums.size(); i++) {
        if (used[i]) continue;              // skip already-placed elements
        used[i] = true;  current.push_back(nums[i]); // CHOOSE
        permute(nums, used, current, result);         // EXPLORE
        used[i] = false; current.pop_back();          // UN-CHOOSE
    }
}

Complexity: O(n · n!) — there are n! permutations and copying each costs O(n).

Combinations

A combination chooses k elements where order doesn’t matter: from {1,2,3,4} choosing 2 gives {1,2},{1,3},{1,4},{2,3},{2,4},{3,4}. To avoid duplicates like {2,1}, we only ever pick elements after the last one chosen, tracked by a start index.

void combine(int n, int k, int start, std::vector<int>& current,
             std::vector<std::vector<int>>& result) {
    if ((int)current.size() == k) {
        result.push_back(current);
        return;
    }
    for (int i = start; i <= n; i++) {
        current.push_back(i);                 // CHOOSE i
        combine(n, k, i + 1, current, result); // EXPLORE only larger values
        current.pop_back();                   // UN-CHOOSE
    }
}

Tip: The start index is the difference between combinations and permutations. Permutations revisit every unused element each step; combinations only move forward, which is exactly what removes order from the answer.

N-Queens

Place N queens on an N×N board so none attack each other (no shared row, column, or diagonal). We place one queen per row and try each column, pruning any column or diagonal already under attack. Here we count the number of valid arrangements.

The pruning trick: a queen at (row, col) occupies column col, ”↘” diagonal row - col, and ”↙” diagonal row + col. We track those three with sets.

int solve(int n, int row,
          std::set<int>& cols, std::set<int>& diag1, std::set<int>& diag2) {
    if (row == n) return 1;             // placed a queen in every row
    int count = 0;
    for (int col = 0; col < n; col++) {
        if (cols.count(col) || diag1.count(row - col) || diag2.count(row + col))
            continue;                   // prune: square is attacked
        cols.insert(col); diag1.insert(row - col); diag2.insert(row + col); // CHOOSE
        count += solve(n, row + 1, cols, diag1, diag2);                     // EXPLORE
        cols.erase(col); diag1.erase(row - col); diag2.erase(row + col);    // UN-CHOOSE
    }
    return count;
}

Call it with solve(n, 0, {}, {}, {}). For n = 4 it returns 2; for n = 8, 92.

Going deeper: The diagonal trick (row - col, row + col are constant along each diagonal) turns an O(n) attack check into O(1). This kind of clever state encoding is what makes constraint backtracking practical. N-Queens is O(n!)-ish without pruning; the diagonal/column sets cut the explored tree dramatically.

Practice these on the recursion & backtracking exercises, or see the related backtracking pattern write-up.

Last updated June 25, 2026
Was this helpful?