文章目录
- 46.全排列
- [78. 子集](https://leetcode-cn.com/problems/subsets/)
- [77. 组合](https://leetcode-cn.com/problems/combinations/)
- [51. N 皇后](https://leetcode-cn.com/problems/n-queens/) (剪枝)
- [52. N皇后 II](https://leetcode-cn.com/problems/n-queens-ii/)
- [37. 解数独](https://leetcode-cn.com/problems/sudoku-solver/)
- [36. 有效的数独](https://leetcode-cn.com/problems/valid-sudoku/)
46.全排列
就是返回其所有可能的组合种类
- 第1种方法 递归:遍历每一层还有哪些子节点没有被访问过,没有访问过的就push到当前组合里
终结条件:遍历的层级数等于传入数字个数时,返回结果
const permute = function(nums) {
const len = nums.length
const curr = [] // 用来记录当前的排列内容
const res = [] // 记录所有的排列顺序
const visited = {} // 用来避免重复使用同一个数字 {1: 1, 2: 1, 3: 1}
// 定义dfs函数,入参从0计数
function dfs(nth) {
// 终止条件,nth是第几层级,到了第3层级就返回结果并return
if(nth === len) {
res.push(curr.slice())
return
}
// 检查手里剩下的数字有哪些
for(let i=0;i<len;i++) {
// 如果当前nums[i]没有被访问过,就push到curr里,并标识为1
if(!visited[nums[i]]) {
visited[nums[i]] = 1 // 访问过后打标识
curr.push(nums[i])
// 下转到下列
dfs(nth+1)
// 初始化num[i]和已访问过的标识 ===> 回溯的特点:初始化
curr.pop()
visited[nums[i]] = 0
}
}
}
// 从0开始dfs
dfs(0)
return res
}
- 第2种递归方法
/* 遍历nums,参数作为空数组,判断此数组是否已经存在数值,有就return,没有就放到数组里并下探
直到数组的长度等于nums长度,说明下探到底了,把数组push到res并return出来
*/
/** 时间复杂度:O(n!) n的阶乘 n!=1x2x3x...x(n-1)xn 只要遇到嵌套的for循环,就是乘积
* 空间复杂度:递归,内部形成调用堆栈,线性增长趋势,O(n) n是递归的层数
*/
var permute = function(nums) {
const res = []
const backtrack = (path) =>{ // 进来的时候path是空数组[],然后遍历nums,没有在此数组中就concat进来
// 终止条件
if(path.length === nums.length){
res.push(path)
return
}
nums.forEach(n=>{
if(path.includes(n)) return
// 继续下探
backtrack(path.concat(n)) // concat不会改变到path,相当于拷贝一份,push会改变数组
})
}
backtrack([])
return res
}
78. 子集
跟46题的区别在于,每个数字可能出现,可能不出现
所以数组长度不必为3,可能是空数组,可能是一个或两个
终止条件:组合里数字个数最大值为3个数,操作3终止
var subsets = function(nums) {
const res=[]
const list=[]
const dfs=(index)=>{
// 每次进来都更新
res.push(list.slice())
// 终止条件
for(let i=index;i<nums.length;i++){
list.push(nums[i])
// 下探
dfs(i+1)
list.pop()
}
}
dfs(0)
return res
}
// 把有变化的,会更新的都作为参数
// 只能使用第n个数字后面的数字,所以start要当参数传进去
var subsets = function(nums) {
const res=[]
const backTrack=(path, len, start)=>{
// 终止条件
if(path.length === len){
res.push(path.slice())
return
}
// i等于start的作用是防止数组中出现重复的数字,如[1, 1]是重复的
for(let i=start;i<nums.length;i++){
// 不断下探,更新start
backTrack(path.concat(nums[i]), len, i+1)
}
}
// 遍历,每个数组可能是0、1、2、3个元素
for(let i=0;i<=nums.length;i++){
backTrack([], i, 0) // i表示一个数组里有多少个元素
}
return res
}
var subsets = function(nums) {
let res = []
let list = []
if(nums == null) return res
function dfs(i){
if(i == nums.length){
res.push(list.concat())
return
}
dfs(i+1) // 不选择
list.push(nums[i])
dfs(i+1) // 选择
// reverse state 把最后加的数从里面删除,因为不是本层变量,每次递归都会改变,所以要重置
list.pop()
}
dfs(0)
return res
}
77. 组合
分析题目:终止条件是当数组长度等于k时,返回结果
var combine = function(n, k) {
const res=[]
const list=[]
const dfs=(start)=>{
if(list.length === k){
res.push(list.slice())
return
}
// i等于start的作用是防止数组中出现重复的数字,如[1, 1]是重复的
for(let i=start;i<=n;i++){
list.push(i)
dfs(i+1)
list.pop()
}
}
dfs(1)
return res
}
51. N 皇后 (剪枝)
每一个皇后都不能在其它皇后的攻击范围里(即不能在其它皇后的行、列、对角线上)
遍历每一行的每一列中,判断现在要填的皇后在不在其它皇后攻击范围里(这个范围要存储起来),在就下一个,不在就放上去然后继续递归
1)遍历列,记录放置的皇后不会被攻击的位置,以及记录列、撇、捺会攻击的位置
2)下探到下一层,继续遍历,直到行数等于棋盘长度,返回结果
// 列,撇,捺存储着皇后能攻击的范围,之后需要放置的皇后不能占用set里面的值
var solveNQueens = function(n) {
if(n<1) return []
const res=[]
// 之前的皇后占的列,撇,捺的位置(皇后能攻击的范围),之后的皇后不能占用set里面的值
// row + col 是撇,row - col 是捺
const col = new Set([])
const pie = new Set([])
const na = new Set([])
const queens=[] // 存储每一行皇后的位置
const dfs=(row)=>{ // row表示当前行
// 终止条件
if(row == n){
let arr=[]
for(let i in queens){
arr.push('Q'.padStart(queens[i]+1, '.').padEnd(n, '.'))
}
res.push(arr)
return
}
for(let i=0;i<n;i++){ // i表示每一列
// 判断剪枝:判断i是否在攻击范围,是就跳出
if(col.has(i)||pie.has(row-i)||na.has(row+i)) continue
// 没在攻击位置,就存储皇后和列、撇、捺位置
col.add(i)
pie.add(row-i)
na.add(row+i)
queens.push(i)
// 下探一层
dfs(row+1)
// 恢复当前层的状态
queens.pop()
col.delete(i)
pie.delete(row-i)
na.delete(row+i)
}
}
dfs(0)
return res
}
52. N皇后 II
跟51的区别:输出最终结果的方法数量
var totalNQueens = function(n) {
if(n<1) return []
const res=[]
// 之前的皇后占的列,撇,捺的位置(皇后能攻击的范围),之后的皇后不能占用set里面的值
// row + col 是撇,row - col 是捺
const col = new Set([])
const pie = new Set([])
const na = new Set([])
const queens=[] // 存储每一行皇后的位置
const dfs=(row)=>{ // row表示当前行
// 终止条件
if(row == n){
let arr=[]
for(let i in queens){
arr.push('Q'.padStart(queens[i]+1, '.').padEnd(n, '.'))
}
res.push(arr)
return
}
for(let i=0;i<n;i++){
// 判断i是否在攻击范围,是就跳出
if(col.has(i)||pie.has(row-i)||na.has(row+i)) continue
// 没在攻击位置,就存储皇后和列、撇、捺位置
col.add(i)
pie.add(row-i)
na.add(row+i)
queens.push(i)
// 下探一层
dfs(row+1)
// 恢复当前层的状态
queens.pop()
col.delete(i)
pie.delete(row-i)
na.delete(row+i)
}
}
dfs(0)
return res.length
}
n皇后还可以用位运算解答
/**
* 解法二 巧用位运算 替代原来set方案 只需得到解个数
* 利用int二进制位来表示相应的列撇捺 有没有被占据掉
* 一个int的二进制位至少有32位 现代的计算机一般是64位的 64以内的皇后问题都可以存储 处理
* col pie na 分表表示先前皇后占据的位置 (假设我们只看后8位 即8皇后 前24位都是0 )
* col 00000001
* pia 00000010
* na 00000100
* col | pia | na => 000001111 表示所有被皇后攻击的所占据的格子
* 取反~ 则表示 即11111000 没有被占的格子变为1了
* x & ((1 << n) - 1) 将x最高位至第n位(含)清零:x & ((1 << n) - 1) 因为不需要前面那些位置
* 最终得到 0*24(前24高位清零) + 11111000
*
* row col pia na bits p(最低位1) bits(最低位清零)
* 0 00000000 00000000 00000000 11111111 00000001 11111110
* 1 00000001 00000010 00000000 11111100 00000100 11111000
* 2 00000101 00001100 00000010 11110000 00010000 11100000
* 3 ... 依次执行 指导bits=0b000000 没有可用位置了
*/
let count = 0
void (function DFS(row, col, pie, na) {
if (row >= n) return count++
// 得到当前所有的空位 即皇后可以放的地方
let bits = (~(col | pie | na)) & ((1 << n) - 1)
// 只要bits中含有1 等价于还有皇后可以放位置 DFS 直到全部全部搜索完0*32
while(bits) {
let p = bits & -bits // 得到最低位的1 表示当前可皇后可放入的位置
bits &= bits - 1 // 清零最低位的1 表示在p位置上放入皇后
DFS(row + 1, col | p, (pie | p) << 1, (na | p) >> 1 )
}
})(0, 0, 0, 0)
return count
}
37. 解数独
1)每一个格子使用暴力解法,’.'表示要填的空格,试探当前空格是否合法,合法就把空位填上
2)剩下的空格继续递归,如果都没问题return true,如果中途失败就还原回‘.’ ,如果都不行就return false
/**
* 数字1-9只能在每一行、列出现一次
* 在3*3宫内只能出现一次
*/
var solveSudoku = function(board) {
if(!board) return
const len = board.length
// 判断剪枝
const valid=(borad, row, col, target)=>{
// 判断当前行、列有没有出现这个数字,如果有就不合法;再判断3*3的格子里有没有出现这个数字,有就不合法
// 就是把数字从行走一遍、从列走一遍,看看有没有重复的数字
for(let i=0;i<9;i++){
if(board[i][col]!='.' && board[i][col] == target ) return false
if(board[row][i]!='.' && board[row][i] == target ) return false
let x=3*Math.floor(row/3) + Math.floor(i/3)
let y=3*Math.floor(col/3) + Math.floor(i%3)
if(board[x][y] !='.' && board[x][y] == target) return false
}
return true
}
const solve=(board)=>{
for(let i=0;i<len;i++){
for(let j=0; j<board[0].length;j++){
if(board[i][j] == '.'){
for(let target=1;target<=9;target++){
// 递归遍历下去,通过整个返回true, 否则恢复为'.'
if(valid(board, i, j, target)){
board[i][j] = target.toString() // 如果通过就填上target
if(solve(board)){
return true
}
board[i][j]='.'
}
}
return false
}
}
}
return true
}
solve(board)
}
/*
Math.floor(i / 3) * 3 即可得到对应相加值:
0/1/2:+0
3/4/5:+3
6/7/8:+6
*/
36. 有效的数独
方法:
- 哈希set,不断拿前面的值跟后面的做比较
- 布尔,要么存在要么不存在
- 位运算:存在为1,不存在为0,于出来看是0还是1判断同一列是否已经存在相同数字 (大家都是1才是1,都是0才是0)
按位或,只要有一个1就是1
如果只有0和1,可以考虑位运算压缩空间
var isValidSudoku = function(board) {
if(!board) return
for (let i = 0; i < 9; i++) {
// 遍历行、列
let row = new Set(), col = new Set()
// 遍历3*3小宫格
let block = new Set()
let x = (i / 3 >> 0) * 3, y = i % 3 * 3
for (let j = 0; j < 9; j++) {
if (board[i][j] !== '.') {
if (row.has(board[i][j])) return false
row.add(board[i][j])
}
if (board[j][i] !== '.') {
if (col.has(board[j][i])) return false
col.add(board[j][i])
}
if (board[x][y] !== '.') {
if (block.has(board[x][y])) return false
block.add(board[x][y])
}
y++
if ((j + 1) % 3 === 0) { // 是否到一个小宫格右侧,是就换行,列也清空
x += 1
y -= 3
}
}
}
return true
}