力扣第五十一题——N皇后

内容介绍

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例 1:

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:[["Q"]]

提示:

  • 1 <= n <= 9

完整代码

int solutionsSize;

char** generateBoard(int* queens, int n) {
    char** board = (char**)malloc(sizeof(char*) * n);
    for (int i = 0; i < n; i++) {
        board[i] = (char*)malloc(sizeof(char) * (n + 1));
        for (int j = 0; j < n; j++) board[i][j] = '.';
        board[i][queens[i]] = 'Q', board[i][n] = 0;
    }
    return board;
}

void backtrack(char*** solutions, int* queens, int n, int row, int* columns, int* diagonals1, int* diagonals2) {
    if (row == n) {
        char** board = generateBoard(queens, n);
        solutions[solutionsSize++] = board;
    } else {
        for (int i = 0; i < n; i++) {
            if (columns[i]) {
                continue;
            }
            int diagonal1 = row - i + n - 1;
            if (diagonals1[diagonal1]) {
                continue;
            }
            int diagonal2 = row + i;
            if (diagonals2[diagonal2]) {
                continue;
            }
            queens[row] = i;
            columns[i] = true;
            diagonals1[diagonal1] = true;
            diagonals2[diagonal2] = true;
            backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
            queens[row] = -1;
            columns[i] = false;
            diagonals1[diagonal1] = false;
            diagonals2[diagonal2] = false;
        }
    }
}

char*** solveNQueens(int n, int* returnSize, int** returnColumnSizes) {
    char*** solutions = malloc(sizeof(char**) * 501);
    solutionsSize = 0;
    int queens[n];
    int columns[n];
    int diagonals1[n + n];
    int diagonals2[n + n];
    memset(queens, -1, sizeof(queens));
    memset(columns, 0, sizeof(columns));
    memset(diagonals1, 0, sizeof(diagonals1));
    memset(diagonals2, 0, sizeof(diagonals2));
    backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
    *returnSize = solutionsSize;
    *returnColumnSizes = malloc(sizeof(int*) * solutionsSize);
    for (int i = 0; i < solutionsSize; i++) {
        (*returnColumnSizes)[i] = n;
    }
    return solutions;
}

思路详解

一、问题背景

N皇后问题是一个经典的回溯算法问题,要求在一个N×N的棋盘上放置N个皇后,使得它们互不攻击。也就是说,任何两个皇后都不能处于同一行、同一列或同一斜线上。

二、解题思路

  1. 表示棋盘

    • 使用一维数组queens来表示棋盘,数组的索引代表行号,数组中每个元素的值代表在该行中皇后所在的列号。
  2. 回溯算法

    • 采用回溯法尝试在每一行放置一个皇后,并检查是否与前面的皇后冲突。
    • 如果冲突,则回溯到上一个状态,并尝试下一个可能的位置。
  3. 冲突检测

    • 使用三个布尔数组columnsdiagonals1diagonals2分别表示列和两个方向的斜线是否已被占用。
    • columns[i]表示第i列是否已放置皇后。
    • diagonals1[i]表示从左上到右下的斜线是否已放置皇后。
    • diagonals2[i]表示从右上到左下的斜线是否已放置皇后。

三、代码详解

  1. 生成棋盘
    • generateBoard函数根据queens数组生成对应的棋盘表示,其中’Q’表示皇后,'.'表示空位。
char** generateBoard(int* queens, int n) {
    char** board = (char**)malloc(sizeof(char*) * n);
    for (int i = 0; i < n; i++) {
        board[i] = (char*)malloc(sizeof(char) * (n + 1));
        for (int j = 0; j < n; j++) board[i][j] = '.';
        board[i][queens[i]] = 'Q', board[i][n] = 0;
    }
    return board;
}
  1. 回溯函数
    • backtrack函数是回溯算法的核心,它尝试在每一行放置皇后,并递归地检查后续行的放置情况。
void backtrack(char*** solutions, int* queens, int n, int row, int* columns, int* diagonals1, int* diagonals2) {
    if (row == n) {
        char** board = generateBoard(queens, n);
        solutions[solutionsSize++] = board;
    } else {
        for (int i = 0; i < n; i++) {
            if (columns[i] || diagonals1[row - i + n - 1] || diagonals2[row + i]) {
                continue;
            }
            queens[row] = i;
            columns[i] = true;
            diagonals1[row - i + n - 1] = true;
            diagonals2[row + i] = true;
            backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
            queens[row] = -1;
            columns[i] = false;
            diagonals1[row - i + n - 1] = false;
            diagonals2[row + i] = false;
        }
    }
}
  1. 主函数
    • solveNQueens函数初始化所需的数据结构,并调用backtrack函数开始回溯过程。
char*** solveNQueens(int n, int* returnSize, int** returnColumnSizes) {
    char*** solutions = malloc(sizeof(char**) * 501);
    solutionsSize = 0;
    int queens[n];
    int columns[n];
    int diagonals1[n + n];
    int diagonals2[n + n];
    memset(queens, -1, sizeof(queens));
    memset(columns, 0, sizeof(columns));
    memset(diagonals1, 0, sizeof(diagonals1));
    memset(diagonals2, 0, sizeof(diagonals2));
    backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
    *returnSize = solutionsSize;
    *returnColumnSizes = malloc(sizeof(int*) * solutionsSize);
    for (int i = 0; i < solutionsSize; i++) {
        (*returnColumnSizes)[i] = n;
    }
    return solutions;
}

四、总结

通过回溯算法,我们可以在N皇后问题中找到所有可能的解决方案。关键在于如何高效地检查冲突,并在找到冲突时回溯到上一个状态。这个算法的时间复杂度是O(N!),因为它需要尝试所有可能的皇后排列。通过使用布尔数组来标记列和斜线,我们能够快速地检查冲突,从而提高算法的效率。

知识点精炼

一、核心概念

  1. 回溯算法:一种通过试错来寻找问题解决方案的算法,适用于组合问题。
  2. 冲突检测:在放置皇后时,确保新放置的皇后不与已放置的皇后在同一列或同一斜线上。
  3. 位运算优化:使用布尔数组来标记列和斜线是否已被占用,提高冲突检测的效率。

二、知识点精炼

  1. 棋盘表示

    • 使用一维数组queens表示棋盘,其中索引代表行,值代表皇后所在的列。
  2. 冲突检测数组

    • columns数组用于检测列冲突。
    • diagonals1diagonals2数组用于检测两个方向的斜线冲突。
  3. 回溯函数

    • backtrack函数递归地尝试在每一行放置皇后,并在放置后进行冲突检测。
    • 如果所有皇后都成功放置,则生成一个解决方案。
  4. 递归终止条件

    • 当所有行都已放置皇后时,表示找到一个有效解决方案。
  5. 状态重置

    • 在回溯时,需要将当前行的皇后位置、列标记和斜线标记重置,以便尝试其他可能的放置。
  6. 解决方案存储

    • 使用solutions数组存储所有找到的解决方案。
    • returnSizereturnColumnSizes用于记录和返回解决方案的数量和每行的列数。

三、性能优化

  • 空间优化:使用布尔数组而不是整型数组来减少空间占用。
  • 时间优化:通过提前终止递归路径来减少不必要的计算。

四、实际应用

  • 组合问题:N皇后问题是一种典型的组合问题,其解法可以推广到其他类似的组合优化问题。
  • 算法竞赛:在算法竞赛中,掌握回溯算法对于解决组合类问题非常有帮助。

五、代码实现要点

  • 动态内存分配:在生成棋盘和解决方案数组时,需要动态分配内存。
  • 内存释放:在实际应用中,需要确保在适当的时候释放分配的内存,避免内存泄漏。

 如何改进算法效率

N皇后问题的回溯算法已经相对高效,因为它避免了不必要的搜索空间。然而,仍有几种方法可以进一步优化算法效率:

  1. 位运算优化

    • 使用位运算来替代布尔数组进行冲突检测,可以减少内存使用并提高速度。每一位代表一列或一条斜线,通过位掩码(bitmask)来表示皇后的位置和攻击范围。
  2. 对称性剪枝

    • 由于棋盘的对称性,我们可以只搜索一半的棋盘(例如,只搜索左上角的区域),然后根据对称性生成其他部分的解。
  3. 迭代加深搜索(IDDFS)

    • 迭代加深搜索可以用来找到解的第一个解,而不是所有解。它可以在找到第一个解后立即停止,从而提高效率。
  4. 分支限界法

    • 在搜索过程中,如果某一列或斜线上已经有皇后,则可以跳过该列或斜线上的其他位置,减少搜索空间。

以下是具体的优化方法:

位运算优化

使用三个整型变量来替代columnsdiagonals1diagonals2数组,分别表示列和两个方向的斜线占用情况。

  • columnMask:使用位掩码表示哪些列已经被占用。
  • diag1Mask:使用位掩码表示从左上到右下的斜线占用情况。
  • diag2Mask:使用位掩码表示从右上到左下的斜线占用情况。

位运算操作如下:

  • 检查列是否被占用:columnMask & (1 << i)
  • 检查斜线是否被占用:diag1Mask & (1 << (row - i + n - 1)) 和 diag2Mask & (1 << (row + i))
  • 标记列和斜线被占用:columnMask |= (1 << i)diag1Mask |= (1 << (row - i + n - 1))diag2Mask |= (1 << (row + i))
  • 取消列和斜线标记:columnMask &= ~(1 << i)diag1Mask &= ~(1 << (row - i + n - 1))diag2Mask &= ~(1 << (row + i))

对称性剪枝

只搜索棋盘的一部分,然后根据对称性生成其他部分的解。例如,可以只搜索主对角线以上的区域,然后根据对称性得到其他部分的解。

迭代加深搜索(IDDFS)

使用迭代加深搜索来找到第一个解,而不是所有解。这种方法可以在找到第一个解时立即停止搜索,适用于只需要一个解的情况。

分支限界法

在放置皇后时,如果某一列或斜线上已经有皇后,则可以直接跳过该列或斜线上的其他位置,减少搜索空间。

通过这些优化方法,可以显著提高N皇后问题的回溯算法效率,尤其是在解决大规模问题时。然而,需要注意的是,优化后的代码可能会更加复杂,且在某些情况下优化效果可能不如预期显著。在进行优化时,应该权衡代码的可读性和性能。

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值