yaox023
yaox023's blog

yaox023's blog

Backtracking - N Queues Problem

Backtracking - N Queues Problem

yaox023's photo
yaox023
·Sep 24, 2022·

4 min read

Subscribe to my newsletter and never miss my upcoming articles

Backtracking is an algorithm to solve the constraint satisfaction problem. The most typical problem is the eight queue problem. It is a problem of placing n queens on an n x n chessboard such that no two queens attack each other. The detailed description we can see in N-Queens. Let's see how to solve this problem with backtracking.

Backtracking is very much like the brute force way. For eight queue problem, we can just figure out all the possible placement of queues and then filter out the invalid answers. Well, with backtracking, we follow a certain way of searching, and we will do tests when we are searching. So during the searching process, we will already filter out invalid answers with tests. So backtracking actually is a lot faster then raw brute force.

With this n-queue problem, what we will do is try to place queues on the board one by one. And we will not place queues blindly, but we will only place the queue in valid positions.

How to do this? Well, we know that 2 queues cannot be in the same row. So if there is already a queue in one row, then we will filter out all the positions on the same row. Same thing happens for columns.

The tricky part is for diagonal positions. Actually there are also patterns exists. Take a look at the 4*4 board below.

[
  (0,0),(0,1),(0,2),(0,3),
  (1,0),(1,1),(1,2),(1,3),
  (2,0),(2,1),(2,2),(2,3),
  (3,0),(3,1),(3,2),(3,3),
]

If we try to calculate the row+col, then we will have a board like this.

[
  0, 1, 2, 3,
  1, 2, 3, 4,
  2, 3, 4, 5,
  3, 4, 5, 6
]

So we can see that for ascending diagonal, we have distinct values as [0, 1, 2, 3, 4, 5, 6].

If we try to calculate the row-col, then we will have a board like this.

[
  0,  -1, -2, -3,
  1,  0,  -1, -2,
  2,  1,   0, -1,
  3,  2,   1,  0
]

So for descending diagonal, we have distinct values as [3, 2, 1, 0, -1, -2, -3].

So with all these pattern, we can simple use 4 sets to test for valid positions.

Before we get to the solution, let first see the template of backtracking.

const solutions = [];
const candidates = [];

function backtrack(i) {
    if (conditionsFulfilled) {
        solutions.push("one solution");
        return;
    }

    for (; i < candidates.length; i++) {
        const candidate = candidates[i];
        if (testsFailed(candidate)) continue;

        pushCurrentCandidate(candidate);

        backtrack(i + 1);

        popCurrentCandidate(candidate);
    }
}

As you can see from this template, we will try all the candidate and store the valid solutions along the way.

OK, let see the real solution for n-queue problem.


function solveNQueens(n: number): string[][] {
    const rows = new Set();
    const cols = new Set();
    const descendingDiagonal = new Set();
    const ascendingDiagonal = new Set();

    const queues: number[][] = [];
    const results: string[][] = [];

    const candidates: number[][] = [];
    for (let row = 0; row < n; row++) {
        for (let col = 0; col < n; col++) {
            candidates.push([row, col]);
        }
    }

    function backtrack(i: number) {

        // found one solution
        if (queues.length === n) {
            const board = Array.from({ length: n }, _ => {
                return Array.from({ length: n }, _ => ".");
            });
            for (const [row, col] of queues) {
                board[row][col] = "Q";
            }
            results.push(board.map(row => row.join("")));
        }

        // try all other options
        for (; i < candidates.length; i++) {
            const [row, col] = candidates[i];

            // tests
            if (
                rows.has(row) ||
                cols.has(col) ||
                descendingDiagonal.has(row - col) ||
                ascendingDiagonal.has(row + col)
            ) {
                continue;
            }

            // use current candidate
            rows.add(row);
            cols.add(col);
            descendingDiagonal.add(row - col);
            ascendingDiagonal.add(row + col);
            queues.push([row, col]);

            backtrack(i + 1);

            // delete current candidate
            rows.delete(row);
            cols.delete(col);
            descendingDiagonal.delete(row - col);
            ascendingDiagonal.delete(row + col);
            queues.pop();
        }
    }

    backtrack(0);
    return results;
};

This solution is almost the same with the template, so very easy to understand.

For n-queue problem, we actually doing a lot of repetitions here. For example, if we have one queue on one row, we can jump to the next row directly. Below solution is a non-standard but a faster way.

function solveNQueens(n: number): string[][] {

    const board = Array.from({ length: n }, _ => {
        return Array.from({ length: n }, _ => ".");
    });

    const cols = new Set();
    const descendingDiagonal = new Set();
    const ascendingDiagonal = new Set();

    const results: string[][] = [];

    function backtrack(row: number) {
        if (row === n) {
            results.push(board.map(row => row.join("")));
            return;
        }

        for (let col = 0; col < n; col++) {
            if (
                cols.has(col) ||
                descendingDiagonal.has(row - col) ||
                ascendingDiagonal.has(row + col)
            ) {
                continue;
            }

            cols.add(col);
            descendingDiagonal.add(row - col);
            ascendingDiagonal.add(row + col);
            board[row][col] = "Q";

            backtrack(row + 1);

            cols.delete(col);
            descendingDiagonal.delete(row - col);
            ascendingDiagonal.delete(row + col);
            board[row][col] = ".";
        }
    }

    backtrack(0);

    return results;
};

This solution makes use of some assumptions for this particular problem: each row should have one and only one queue. So inside the backtracking process, we only need to loop one row. If there is no position for current row, then just throw away this path. So this makes the process a lot faster.

 
Share this