引言
参照的是左程云的课程:https://space.bilibili.com/8888480/lists/3509640?type=series
本笔记是class40的内容,基于N皇后问题Python实现,分析了经典回溯算法和位运算优化版本的原理与实现。N皇后问题是回溯算法的经典应用,也是展示位运算巧妙应用的绝佳案例。
040【必备】N皇后问题-重点是位运算的版本
/class40-n%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/n%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98%E6%8F%8F%E8%BF%B0.png)
问题描述
N皇后问题是在N×N的棋盘上放置N个皇后,使得任意两个皇后都不能相互攻击的问题。皇后可以攻击同一行、同一列或同一对角线上的任何棋子。
测试链接:https://leetcode.cn/problems/n-queens-ii/
核心挑战
N皇后问题的时间复杂度是O(N!),这意味着随着N的增长,计算量会急剧增加。因此,优化常数时间和进行有效剪枝变得尤为重要。
方法一:数组表示路径(经典回溯)
核心思想
使用数组 path[i] = j 表示第i行的皇后放在第j列。对于每一行,尝试所有可能的列位置,通过冲突检测函数判断是否可以放置皇后。
具体实现流程:
- 在每一行,
ban = col | left | right计算出所有被攻击的位置。(共同的限制) candidate = limit & (~ban)找出所有可以放置皇后的候选位置(列)。- 循环遍历
candidate中的每一个1(代表一个可行的列)。
place = candidate & (-candidate)是一个巧妙的技巧,用于分离出最右边的1。 - 对于每一个可行的
place,递归到下一行,并更新三个位掩码:- 列限制:
col | place - 左对角线限制:
(left | place) >> 1(下一行,对角线向右移一位) - 右对角线限制:
(right | place) << 1(下一行,对角线向左移一位)
- 列限制:
算法实现
1 | def totalNQueens1(self, n: int) -> int: |
/class40-n%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/%E5%85%AC%E5%85%B1%E5%AF%B9%E8%A7%92%E7%BA%BF%E7%9A%84%E5%88%A4%E6%96%AD.png)
冲突检测详解
- 列冲突:
j == path[k]- 同一列不能有两个皇后 - 对角线冲突:
abs(i - k) == abs(j - path[k])- 对角线上行差的绝对值等于列差的绝对值
算法分析
- 时间复杂度:O(N! × N),每个位置需要O(N)时间检查冲突
- 空间复杂度:O(N)
- 优缺点:思路清晰但效率较低
方法二:位运算优化(推荐)
核心思想
使用三个整数的位信息来表示所有被占用的位置,从而极大地加速冲突检测:
- col:第k位为1表示第k列被占用
- left:第k位为1表示左上到右下对角线被占用
- right:第k位为1表示右上到左下对角线被占用
/class40-n%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/col%E5%8F%98%E9%87%8F.png)
/class40-n%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/left%E5%8F%98%E9%87%8F.png)
对角线映射规律
左上到右下对角线(left)
- 特点:行-列的值相同
- 下一行时:对角线向右移动一位,用右移操作
>> 1
右上到左下对角线(right)
- 特点:行+列的值相同
- 下一行时:对角线向左移动一位,用左移操作
<< 1
算法实现
1 | def totalNQueens2(self, n: int) -> int: |
关键位运算技巧
1. 提取最右边的1
1 | place = candidate & (-candidate) |
- 原理:
-candidate是candidate的补码,两者相与得到最右边的1 - 例子:
candidate = 0b010100,-candidate = 0b101100,place = 0b000100
2. 移除已选择的位
1 | candidate ^= place |
- 使用异或操作移除已经选择的位置
3. 计算有效候选位置
1 | candidate = limit & (~ban) |
~ban:翻转ban得到可放置的位置limit:确保只考虑有效的n位棋盘范围
执行过程示例
以4皇后问题为例:
1 | 初始状态: |
算法分析
- 时间复杂度:O(N!),但常数时间大幅优化
- 空间复杂度:O(N)
- 优势:位运算操作极快,适合大规模问题
性能对比
根据代码测试结果:(测试电脑:联想ThinkBook 16+)
| N值 | 数组方法 | 位运算方法 | 性能提升 |
|---|---|---|---|
| 14 | 188224 | 9011 | 约10倍 |
| 14 | 超时 | ~10秒内 | 数十倍 |
测试代码
1 | if __name__ == '__main__': |
位运算核心技巧总结
1. 基本位操作
1 | # 设置第i位为1 |
2. 高级位技巧
1 | # 提取最右边的1 |
3. 对角线映射
1 | # 左上到右下:下一行右移 |
优化策略总结
1. 数据结构选择
- 数组方法:直观但需要O(N)时间检查冲突
- 位运算方法:用位信息表示状态,O(1)时间冲突检测
2. 状态表示优化
- 用整数的二进制位表示多个布尔状态
- 利用位运算的并行性处理多个位
3. 算法剪枝
- 早期冲突检测
- 状态压缩减少内存访问
4. 常数优化
- 位运算替代数组操作
- 减少函数调用开销
学习要点
1. 回溯算法模板
1 | def backtrack(state): |
2. 位运算在算法中的应用
- 状态压缩
- 集合操作
- 快速计算
- 空间优化
3. 性能优化思路
- 算法层面:减少时间复杂度
- 实现层面:优化常数因子
- 数据结构层面:选择合适的表示方式
4. 问题分析方法
- 理解问题约束
- 识别状态表示
- 设计状态转移
- 考虑优化空间
实际应用场景
1. 约束满足问题(CSP)
- 数独求解
- 图着色问题
- 任务调度
2. 位运算优化适用场景
- 状态空间较小(通常≤64位)
- 需要频繁的集合操作
- 对性能要求极高的场景
3. 工程实践建议
- 先实现清晰版本,再考虑优化
- 位运算虽快但可读性差,需要充分注释
- 在性能关键路径上应用位运算优化
总结
N皇后问题展示了从经典回溯算法到位运算优化的完整演进过程。位运算版本虽然理解难度较高,但在性能上有显著提升,特别适合处理大规模问题。掌握这种优化思路对于解决其他状态空间搜索问题具有重要意义。
通过对比两种方法,我们可以看到算法优化的不同层次:算法设计层面的改进和实现技巧层面的优化。在实际工程中,应该根据具体需求在代码可读性和执行效率之间做出合理权衡。