LeetCode 刷题记录(三)
本篇文章主要介绍一个经典问题:「N 皇后」问题。
51. N-Queens
题目
下图为 8 皇后问题的一种解法。
「示例」:
Input: 4
Output: [
[".Q..", // Solution 1
"...Q",
"Q...",
"..Q."],
["..Q.", // Solution 2
"Q...",
"...Q",
".Q.."]
]
Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above.
思路
这道题是「回溯法」的经典应用。基本的思路是:从第一行开始,每行按列循环放置皇后,如果找到一个符合规则的位置,则到下一行,以此类推,如果可以一直进行到最后一行,则得到一个正确的解法,记录下来;如果到某一行发现没有符合要求的位置,就回到上一行,对该行还未循环的位置继续按列循环。重复上述过程,直到所有格子均被遍历。可以看出,这种解法实际上是一种「深度优先搜索」。
在实际编程时,我们通过「递归」实现上述思路。注意不论一条路线是否最终得到可行解,由于每一行只能有一个皇后,因此我们都需要撤销当前的棋子摆放,以便进行下一条路线的尝试,二者的区别在于得到可行解的路线会一直递归到最底层,将解记录下来再进行回溯,而无可行解的路线一般在递归中途即回溯。
关于对角线的攻击路线,可以发现对于一个给定的棋子位置,其主对角线和次对角线的棋子的行列信息存在如下规律:主对角线(左上到右下)上 行号 - 列号 = 常数
,次对角线上 行号 + 列号 = 常数
。为了防止出现负数,我们会对主对角线对应的数组下标进行扩大。
代码
Java 解法
class Solution {
int n; // 默认权限(仅同一包下的类可以访问)
int rows[]; // 当前行的列位置记录
int dales[]; // 主对角线位置记录
int hills[]; // 次对角线位置记录
int queens[]; // 皇后位置记录
List<List<String>> output = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 初始化各数组
this.n = n;
rows = new int[n];
dales = new int[2 * n - 1];
hills = new int[2 * n - 1];
queens = new int[n];
backtrack(0); // 从第一行开始调用回溯法,遍历所有格子
return output;
}
// 判断一个位置是否会被攻击到
private boolean isNotUnderAttack(int row, int col) {
int res = rows[col] + dales[row - col + n - 1] + hills[row + col];
return (res == 0)? true : false;
}
// 放置皇后,设定可攻击的位置
private void placeQueen(int row, int col) {
queens[row] = col;
rows[col] = 1; // 对应的列可攻击(回溯会确保每行只有一个,只需每行按列遍历)
dales[row - col + n - 1] = 1; // 主对角线的对应位置可攻击,范围 [0, 2n-2]
hills[row + col] = 1; // 次对角线的对应位置可攻击, 范围 [0, 2n-2]
}
// 取消当前位置的摆放,用于回溯
private void removeQueen(int row, int col) {
queens[row] = 0;
rows[col] = 0;
dales[row - col + n - 1] = 0;
hills[row + col] = 0;
}
// 添加一个解
private void addSoultion() {
List<String> solution = new ArrayList<>(); // 加泛型写法更标准
for (int i = 0; i < n; i++) { // 按行遍历
int col = queens[i];
StringBuilder sb = new StringBuilder();
for (int j = 0; j < col; j++) sb.append(".");
sb.append("Q");
for (int j = 0; j < n - col - 1; j++) sb.append(".");
solution.add(sb.toString());
}
output.add(solution);
}
// 回溯法核心(递归调用)
private void backtrack(int row) {
for (int col = 0; col < n; col++) {
if (isNotUnderAttack(row, col)) {
placeQueen(row, col);
if (row == n - 1) addSoultion(); // 如果已到达最后一行,则得到一个解
else backtrack(row + 1); // 否则继续向下一行前进,继续递归
removeQueen(row, col); // 撤销当前位置,开始探索下一条路线
}
} // 如果当前行没有可放置的位置,则直接返回上一行开始探索下一条路线
}
}
Python 解法
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
def could_place(row, col): # python 可以函数嵌套
return not(rows[col] + dale_diagonais[row - col + n - 1] + hill_diagonais[row + col])
def place_queen(row, col):
queens.add((row, col))
rows[col] = 1
dale_diagonais[row - col + n - 1] = 1
hill_diagonais[row + col] = 1
def remove_queen(row, col):
queens.remove((row, col))
rows[col] = 0
dale_diagonais[row - col + n - 1] = 0
hill_diagonais[row + col] = 0
def add_solution():
solution = []
for _, col in sorted(queens): # 先按行排序,再按列排序
solution.append('.' * col + 'Q' + '.' * (n - col - 1))
output.append(solution)
def backtrack(row = 0): # 默认从 0 开始
for col in range(n):
if could_place(row, col):
place_queen(row, col)
if row + 1 == n:
add_solution()
else:
backtrack(row + 1)
remove_queen(row, col)
rows = [0] * n # 快速创建 n 维数组(列表)
dale_diagonais = [0] * (2 * n - 1) # list 做全局变量无需声明 global
hill_diagonais = [0] * (2 * n - 1)
queens = set() # 无重复的 tuple 集合
output = []
backtrack() # python 需要先定义再调用
return output
52. N-Queens II
题目
给定一个整数n ,返回 n皇后不同的解决方案的数量。
「示例」:
Input: 4
Output: 2
Explanation: There are two distinct solutions to the 4-queens puzzle as shown below.
[
[".Q..", // Solution 1
"...Q",
"Q...",
"..Q."],
["..Q.", // Solution 2
"Q...",
"...Q",
".Q.."]
]
思路
这道题跟上一题思路相同,只是输出简单了一些。这里给出一个使用「位运算」的精妙解法。首先简单介绍一下本解法中使用到的与位运算相关的概念及性质。在位运算中,正负数的运算是基于「补码」实现的。下图给出了一个八位二进制数的补码系统表示,最高位为符号位,正数和 0 的补码为其本身,负数的补码为其对应的正数取反加一。
基于补码,我们将使用如下的两个位运算操作:
-
x & -x
:「按位与」一个数与其负数,这里负数会被表示成补码,可以验证,该操作会将原数字的最后一位出现的 1 保留,其它位数全部清 0(实际运算时不需要考虑符号位,一般不会超出位数限制,就算考虑也会变成 0) -
x & (x - 1)
:「按位与」一个数和其自身减 1,可以验证,该操作会将原数字的最后一位出现的 1 变成 0,其它位数保持不变。上述操作也可以通过「异或」来实现:x ^ (x & -x)
,因为 0 与任意数(0 或 1)异或的结果为原数字,所以只有最后一位出现的 1 会被清 0。
「举例说明」:
127 & -127:
(01111111 & 10000001) = 00000001 // 只有最后一个 1 被保留
127 & 126
(01111111 & 01111110) = 01111110 // 只有最后一个 1 被清 0
2 & 1
(00000010 & 00000001) = 00000000 // 即使存在借位上述结论也成立
基于上述位运算,我们可以给出一种巧妙的解法。算法的整体思路和之前相同,使用基于回溯的深度优先搜索。区别在于我们会使用 3 个「二进制数」来分别标记当前行哪些格子可以放置皇后,它们分别表示:列、主对角线和次对角线,二进制为 1 代表不能放置,二进制为 0 代表可以放置。注意在实际编码时,对于当前行所有可用的位置,我们会用 1 表示可以放置,0 表示不能放置,这是位运算的特性导致的,因此我们在合并时需要对一方进行「取反」操作。
代码
Java 解法
class Solution {
int count = 0; // 初始化最终输出
public int totalNQueens(int n) {
backtrack(n, 0, 0, 0, 0); // 从第一行开始执行搜索
return count;
}
private void backtrack(int n, int row, int col, int dale, int hill) {
/**
row: 当前行号
col: 当前行按列筛选的的位置 [1 = 不可放置,0 = 可以放置]
dale: 当前行按主对角线筛选的位置 [1 = 不可放置,0 = 可以放置]
hill: 当前行按次对角线筛选的位置 [1 = 不可放置,0 = 可以放置]
*/
if (row >= n) count++; // 如果已经遍历完所有行,则得到一个解(这里是到达最后一行的下一行)
else { // 否则继续搜索
// 通过位运算得到当前行所有可放置的位置 [1 = 可以放置,0 = 不可放置]
int bits = ((1 << n) - 1) & ~(col | dale | hill); // 第一项得到 n 个 1,表示均可放置
while (bits > 0) { // 如果存在可以放置的位置
int pick = bits & -bits; // 通过位运算筛选出一个 1,表示当前放置的位置
// 递归调用,向下一行搜索,按位或操作可以将当前放置位置所产生的攻击位置加入进去
backtrack(n, row + 1, (col | pick), (dale | pick) >> 1, (hill | pick) << 1);
bits &= bits - 1; // 通过位运算实现回溯,将当前放置的位置置为 0(不可放置)
}
}
}
}
Python 解法
class Solution:
def totalNQueens(self, n: int) -> int:
def backtrack(row = 0, col = 0, dale = 0, hill = 0, count = 0):
if row == n:
# 注意 count 定义为全局变量的话需要声明,不然会报错(赋值时存在歧义,被理解为局部变量)
# 类中 global 不管用,得使用 self 表示全局变量(global 需要引用类外面的变量)
count += 1
else:
bits = ((1 << n) - 1) & ~(col | dale | hill)
while bits:
pick = bits & -bits
count = backtrack(row + 1, (col | pick), (dale | pick) >> 1, (hill | pick) << 1, count)
bits &= bits - 1
return count
return backtrack()
- LVS+Keepalived高可用环境部署梳理(主主和主从模式)
- 随着区块链的火爆,相关顶级域名“矿池”KC.com已建站
- Flash/Flex学习笔记(50):3D线条与填充
- LVM常规操作记录梳理(扩容/缩容/快照等)
- Flash/Flex学习笔记(55):背面剔除与 3D 灯光
- 资源等待类型sys.dm_os_wait_stats
- NVIDIA不再允许数据中心用GeForce驱动,提供区块链服务除外
- 非常强悍并实用的双机热备+负载均衡线上方案
- Apache 压力测试工具ab
- SQL之收集SQL Server线程等待信息
- 聚合索引(clustered index) / 非聚合索引(nonclustered index)
- 域名资讯:单词域名can.com以15.5万美金成功交易
- jQuery无缝图片横向(水平)/竖向(垂直)滚动
- Centos下MooseFS(MFS)分布式存储共享环境部署记录
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 适合初学者的Python装饰器的简易教程
- 一起刷Leetcode第一篇,数组和字典的妙用
- 加速Python列表和字典,让你代码更加高效
- 如何使用Python的Flask和谷歌app Engine来构建一个web app
- 如何用Python实现电子邮件的自动化
- 在Win下安装Visual Studio和Parallel Studio XE
- 我们将项目语言从Python转向Go的5个原因
- GFN-xTB的编译与API使用
- 红外光谱的理论计算
- 一起刷题(leetcode)第二篇:如何用Python实现递归
- 如何成为Python的数据操作库Pandas的专家?
- 谈谈Gaussian软件中的guess=mix
- 用ORCA做DLPNO-CCSD(T)计算
- Fortran调用C函数
- 在Python中创建命令行界面的最佳方式