N皇后问题如何写出简洁的最优解 - 回溯法及LeetCode应用详解

本文主要是介绍N皇后问题如何写出简洁的最优解 - 回溯法及LeetCode应用详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

backtracking(回溯),是LeetCode上比较常见的一类典型题。
本博文所有的代码均可在
https://github.com/Hongze-Wang/LeetCode_Java
https://github.com/Hongze-Wang/LeetCode_Python
中找到,欢迎star。

回溯法之所以称之为回溯法,是因为它其实是DFS/BFS+回溯操作进行的穷举。回溯和DFS/BFS的区别在于回溯操作。也有人把回溯法称为BFS/DFS,这没有错,但是不太准确的,回溯法是特殊的DFS或者BFS,因为DFS或者BFS在某些情况下无法递归处理所有的情况(即不完全穷举),需要执行一定的后悔操作,才能穷举所有情况。

我们以LeetCode实例进行说明:

首先我们从DFS扩展到backtracking

112. Path Sum (Easy)

Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.

Note: A leaf is a node with no children.

Example:
Given the below binary tree and sum = 22,

      5/ \4   8/   / \11  13  4/  \      \
7    2      1

return true, as there exist a root-to-leaf path 5->4->11->2which sum is 22.

因为是树形结构,很容易联想到使用DFS递归来解:

// 112. Path Sum
/*** Definition for a binary tree node.* public class TreeNode {*     int val;*     TreeNode left;*     TreeNode right;*     TreeNode(int x) { val = x; }* }*/
public class PathSum {public boolean hasPathSum(TreeNode root, int sum) {if(root == null) return false;// Path value condition need that the last node must be a leaf nodeif((sum -= root.val) == 0 && root.left == null && root.right == null) {return true;}return hasPathSum(root.left, sum) || hasPathSum(root.right, sum);}
}

113. Path Sum II (Medium)

Given a binary tree and a sum, find all root-to-leaf paths where each path’s sum equals the given sum.

Note: A leaf is a node with no children.

Example:
Given the below binary tree and sum = 22,

      5/ \4   8/   / \11  13  4/  \    / \
7    2  5   1

Return:

[[5,4,11,2],[5,8,4,5]
]

这道题是上面简单题的扩展版,从求有没有解到把所有的解都求出来,这个时候就需要回溯法了。因为DFS找到解之后就直接返回了,它无法穷举所有的情况。

// 113. Path Sum II/*** Definition for a binary tree node.* public class TreeNode {*     int val;*     TreeNode left;*     TreeNode right;*     TreeNode(int x) { val = x; }* }*/public class Solution {public List<List<Integer>> pathSum(TreeNode root, int sum) {List<List<Integer>> res = new ArrayList<>();dfs(root, sum, res, new ArrayList());return res;}public void dfs(TreeNode root, int sum, List<List<Integer>> res, List<Integer> list) {if(root == null) return; // 递归终止条件sum -= root.val;    // 需要回溯的操作1list.add(root.val); // 需要回溯的操作2if(root.left == null && root.right == null) {if(sum == 0) {res.add(new ArrayList(list));}list.remove(list.size()-1); // 回溯 取消操作2 把操作2加进去的元素删掉// sum += root.val;         // 回溯 取消操作1 不再向下递归 这一句做不做都一样return;}dfs(root.left, sum, res, list);dfs(root.right, sum, res, list);list.remove(list.size()-1);    // 回溯 取消操作2 把操作2加进去的元素删掉// sum += root.val;            // 回溯 取消操作1 因为不再向下递归 这一句做不做都一样}
}

回溯法比较难搞懂的是它的执行路径,因为它建立在递归之上,而且还有后悔操作。以这个例子为例,list.remove(list.size()-1)移除的到底是哪一个元素呢?没错,是本次递归所加进去的元素。递归调用会保存堆栈,两行dfs返回之后list的状态是没有执行两行dfs的状态,而不是执行了两行dfs之后的状态,这点是反直觉的。你可以把代码辅助到Idea中,打个断点,然后一步一步观察相关变量是如何变化的,以加深自己的理解。

下面我们在看看和的组合以及序列这两个LeetCode的回溯典型题:

39. Combination Sum (Medium)

Given an array of distinct integers candidates and a target integer target, return a list of all unique combinations of candidates where the chosen numbers sum to target. You may return the combinations in any order.

The same number may be chosen from candidates an unlimited number of times. Two combinations are unique if the frequency of at least one of the chosen numbers is different.

It is guaranteed that the number of unique combinations that sum up to target is less than 150 combinations for the given input.

Example 1:
Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
Explanation:
2 and 3 are candidates, and 2 + 2 + 3 = 7. Note that 2 can be used multiple times.
7 is a candidate, and 7 = 7.
These are the only two combinations.

Example 2:
Input: candidates = [2,3,5], target = 8
Output: [[2,2,2,2],[2,3,3],[3,5]]

Example 3:
Input: candidates = [2], target = 1
Output: []

Example 4:
Input: candidates = [1], target = 1
Output: [[1]]

Example 5:
Input: candidates = [1], target = 2
Output: [[1,1]]

Constraints:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
All elements of candidates are distinct.
1 <= target <= 500

class Solution {public void backtrack(int[] candidates, int index, int target, List<Integer> list, List<List<Integer>> res) {if(target == 0) { // 递归终止条件res.add(new ArrayList(list));return;}for(int i=index; i<candidates.length; i++) { // 因为这道题 数组元素是可以重复利用的所以i从index开始 递归调用也会传入index 这点请注意if(target - candidates[i] < 0) {break;}target -= candidates[i]; // 需要回溯的操作1list.add(candidates[i]); // 需要回溯的操作2backtrack(candidates, i, target, list, res);target += candidates[i]; // 回溯操作1list.remove(list.size()-1); // 回溯操作2}}public List<List<Integer>> combinationSum(int[] candidates, int target) {List<List<Integer>> res = new ArrayList<>();Arrays.sort(candidates); // 需要对数组排序backtrack(candidates, 0, target, new ArrayList(), res);return res;}
}

这道题我们可以看出,解回溯题是,需要回溯的操作位于递归调用之前,递归调用之后应该立即取消这些操作。

40. Combination Sum II (Medium)

Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target.

Each number in candidates may only be used once in the combination. (注意这点,这是和第39题的区别)

Note: The solution set must not contain duplicate combinations. (注意这点,这是和第39题的区别)

Example 1:
Input: candidates = [10,1,2,7,6,1,5], target = 8
Output:

[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

Example 2:
Input: candidates = [2,5,2,1,2], target = 5
Output:

[
[1,2,2],
[5]
]

Constraints:

1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30

class Solution {public void backtrack(int[] candidates, int index,int target, List<Integer> list, List<List<Integer>> res) {if(target == 0) { // 递归终止条件1res.add(new ArrayList(list));return;}for(int i=index; i<candidates.length; i++) {if(target - candidates[i] < 0) { // 递归终止条件2break;}if(i > index && candidates[i] == candidates[i-1]) { // 防止回溯时再次把之前记忆过的答案再加进去continue;}target -= candidates[i];  // 需要回溯的操作1list.add(candidates[i]);  // 需要回溯的操作2backtrack(candidates, i+1, target, list, res);  // 注意 和39不同,这里因为每个元素只能用一次 所以传的参数是i+1target += candidates[i];    // 回溯操作1list.remove(list.size()-1); // 回溯操作2}}public List<List<Integer>> combinationSum2(int[] candidates, int target) {List<List<Integer>> res = new ArrayList<>();Arrays.sort(candidates); // 注意需要对数组排序backtrack(candidates, 0, target, new ArrayList(), res);return res;}
}

46. Permutations (Medium)

Given an array nums of distinct integers, return all the possible permutations. You can return the answer in any order.

Example 1:
Input: nums = [1,2,3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Example 2:
Input: nums = [0,1]
Output: [[0,1],[1,0]]

Example 3:
Input: nums = [1]
Output: [[1]]

Constraints:

1 <= nums.length <= 6
-10 <= nums[i] <= 10
All the integers of nums are unique.

// 46. Permutations
// c++ offer next_permutation to generate next permutationclass Solution {List<Integer> temp;public List<List<Integer>> arr = new ArrayList();public List<List<Integer>> permute(int[] nums) {permute(nums, 0, nums.length-1);return arr;}private void permute(int nums[], int left, int right) {if(left == right) {temp = new ArrayList<Integer>();for(int k: nums) {temp.add(k);}arr.add(temp);} else {for(int i=left; i<=right; i++) {swap(nums, i, left); // 需要回溯的操作permute(nums, left+1, right);swap(nums, i, left);  // 回溯操作}}}private void swap(int[] nums, int i, int left) {int temp = nums[i];nums[i] = nums[left];nums[left] = temp;}

这道题体现了回溯法的穷举特性,以Example1为例,加到结果集arr顺序是[1,2,3] ->[1,3,2]->[2,1,3]->[2,3,1]->[3,2,1]->[3,1,2]。思考一下递归执行的整个过程是怎样的?

47. Permutations II (Medium)

Given a collection of numbers, nums, that might contain duplicates, return all possible unique permutations in any order.

Example 1:
Input: nums = [1,1,2]
Output:
[[1,1,2], [1,2,1], [2,1,1]]

Example 2:
Input: nums = [1,2,3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Constraints:
1 <= nums.length <= 8
-10 <= nums[i] <= 10

// 47. Permutations IIclass Solution {
public:vector<vector<int>> permuteUnique(vector<int>& nums) {sort(nums.begin(), nums.end());vector<vector<int>> res = {nums};// if(nums.size() == 0) return res;while(next_permutation(nums.begin(),nums.end())) {res.push_back(nums);}return res;}
};

开个玩笑!不过这道题我只写了Python版本的。使用Python内置的计数器对数组元素计数,然后用一个就减掉一个,减到0直接删除,计数器为空的时候即为所有的元素都使用完毕,加入到结果集中。Python的回溯法可以写一个内置函数,是一种闭包的形式,可以减少很多参数的传递,改变变量作用域是闭包的重要作用之一。

# 47. Permutations IIclass Solution:def permuteUnique(self, nums: List[int]) -> List[int]:counter = collections.Counter(nums)def dfs(cur):res = []if not counter: # 递归终止条件return [cur]for num in list(counter.keys()):counter[num] -= 1 # 需要回溯的操作if counter[num] == 0:del counter[num]res += dfs(cur + [num]) # 等价于 res.append(dfs(cur + [num]))counter[num] = counter.get(num, 0) + 1 # 回溯操作return resreturn dfs([])

最后,就是我们的主人公了,经典的N皇后问题。

51. N-Queens (Hard)

The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other.

Given an integer n, return all distinct solutions to the n-queens puzzle.

Each solution contains a distinct board configuration of the n-queens’ placement, where ‘Q’ and ‘.’ both indicate a queen and an empty space, respectively.

Example 1:
在这里插入图片描述
Input: n = 4
Output: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above
Example 2:

Input: n = 1
Output: [["Q"]]

Constraints:
1 <= n <= 9

如果你不太看得懂对角线规律是这么来的话,参见这里。

// 51. N-Queens// 回溯法模板题 + 找规律
// 回溯法适用于枚举问题,比如全排列、组合之类的
// 这些问题往往需要在枚举的所有情况中选择满足条件的情况生成解或者是求最优解 因此需要注意if判断条件删除一些不需要考虑的情况
// 回溯法和DFS、BFS的区别在于为了枚举 有回溯过程 即为了生成所有情况而还原某些操作 比如下面的操作1和操作2 都是需要回溯的操作
// 千万不能忘掉回溯 否则无法生成所有解 或者漏掉最优解的过程// i表示第i行 j表示第j列
// 规律 对角线坐标
// 斜对角线坐标 行列坐标差值永远相等 为了避免出现负值 使用i-j+n 为此 diag1的容量应为2n
// 反斜对角线左边 行列坐标的和永远相等 为了避免出现溢出 diag1的容量为2nclass Solution {public List<List<String>> res = new ArrayList<>();public List<List<String>> solveNQueens(int n) {char[][] board = new char[n][n];boolean[] col = new boolean[n];boolean[] diag1 = new boolean[n+n];boolean[] diag2 = new boolean[n+n];// 初始化棋盘for(int i=0; i<n; i++) {for(int j=0; j<n; j++) {board[i][j] = '.';}}solveNQueens(0, n, board, col, diag1, diag2);return res;}public void solveNQueens(int i, int n, char[][] board, boolean[] col, boolean[] diag1, boolean[] diag2) {if(i == n) { // 递归终止条件List<String> list = new ArrayList<>();for(int r=0; r<n; r++) {list.add(new String(board[r])); // 因为是传引用 所以要new出新的String加入list}res.add(list);return;}for(int j=0; j<n; j++) {if(!col[j] && !diag1[n-i+j] && !diag2[i+j]) {board[i][j] = 'Q'; // 需要回溯的操作1col[j] = diag1[n-i+j] = diag2[i+j] = true; // 需要回溯的操作2solveNQueens(i+1, n, board, col, diag1, diag2);col[j] = diag1[n-i+j] = diag2[i+j] = false; // 回溯操作2board[i][j] = '.'; // 回溯操作1}}}
}

52. N-Queens II (Hard)

The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other.

Given an integer n, return the number of distinct solutions to the n-queens puzzle.

Example 1:
在这里插入图片描述
Input: n = 4
Output: 2
Explanation: There are two distinct solutions to the 4-queens puzzle as shown.

Example 2:
Input: n = 1
Output: 1

Constraints:

1 <= n <= 9

回溯解法一(非最优解):

解法同51,只不过到达递归终止条件时,我们就计数。这不是最优解,仅仅求出解的数量可以通过取巧来节省穷举整个求解过程的某些计算过程,见下面的解法。

class Solution {public int res = 0;public void solveNQueens(int n) {char[][] board = new char[n][n];boolean[] col = new boolean[n];boolean[] diag1 = new boolean[n+n];boolean[] diag2 = new boolean[n+n];for(int i=0; i<n; i++) {for(int j=0; j<n; j++) {board[i][j] = '.';}}solveNQueens(0, n, board, col, diag1, diag2);}public void solveNQueens(int i, int n, char[][] board, boolean[] col, boolean[] diag1, boolean[] diag2) {if(i == n) {res++;}for(int j=0; j<n; j++) {if(!col[j] && !diag1[n-i+j] && !diag2[i+j]) {board[i][j] = 'Q';col[j] = diag1[n-i+j] = diag2[i+j] = true;solveNQueens(i+1, n, board, col, diag1, diag2);col[j] = diag1[n-i+j] = diag2[i+j] = false;board[i][j] = '.';}}}public int totalNQueens(int n) {solveNQueens(n);return res;}
}

回溯解法二(最优解):

使用整型的二进制表示做标志位
用n个十进制数 即可表示棋盘 0表示可以放Q 1表示不能放Q一旦某一行被放置了Q 则该位置变为1 整行整列都不能放Q了 因而整行整列都变成1 对应for循环操作a
反对角线 不能放Q 对应循环操作b
对角线   不能放Q 对应循环操作c
// 使用整型的二进制表示做标志位
// 用n个十进制数 即可表示棋盘 0 表示可以放Q 1表示不能放Q// 一旦某一行被放置了Q 则该位置变为1 整行整列都不能放Q了 因而整行整列都变成1 对应for循环操作a
// 反对角线 不能放Q 对应循环操作b
// 对角线   不能放Q 对应循环操作c// 以n = 3为例
// 000b = 0
// 000b = 0
// 000b = 0// 我们在第一行第一列放置Q Q存的是1 我这里为了区别因为本列本行或者对角线放了Q而不能放的位置 写成了Q
// Q00b = 4
// 000  = 0
// 000  = 0// for循环a操作 使得
// Q11  = 7
// 100  = 4
// 100  = 4// for循环b操作 使得
// Q11  = 7
// 110  = 6
// 100  = 4// for循环c操作 使得
// Q11  = 7
// 110  = 6
// 101  = 5// 最终得到
// Q11  = 7
// 11Q  = 7
// 1Q1  = 7
// 只有这一种解class Solution {public int total = 0;public int totalNQueens(int[] queens, int len) {int total = 0;for(int i=0; i<queens.length; i++) {if((queens[len-1] & (1 << i)) == 0) {if(len == queens.length) {total += 1;} else {int[] rem = new int[queens.length-len];for(int j=len; j<queens.length; j++) {rem[j-len] = queens[j];            // 储存操作前状态 为了回溯queens[j] |= 1 << i;               // 操作aqueens[j] |= 1 << (i+j - len + 1); // 操作b 对角线规律 行列坐标和相等 -len+1 防止溢出nqueens[j] |= 1 << (i-j + len - 1); // 操作c 对角线规律 行列坐标差相等 +len-1 防止小于0}total += totalNQueens(queens, len+1);for(int j=len; j<queens.length; j++) {queens[j] = rem[j-len];            // 回溯操作 同时撤销了操作a、b、c}}}}return total;}public int totalNQueens(int n) {int[] queens = new int[n];return totalNQueens(queens, 1);}
}

这篇关于N皇后问题如何写出简洁的最优解 - 回溯法及LeetCode应用详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/623262

相关文章

详解如何通过Python批量转换图片为PDF

《详解如何通过Python批量转换图片为PDF》:本文主要介绍如何基于Python+Tkinter开发的图片批量转PDF工具,可以支持批量添加图片,拖拽等操作,感兴趣的小伙伴可以参考一下... 目录1. 概述2. 功能亮点2.1 主要功能2.2 界面设计3. 使用指南3.1 运行环境3.2 使用步骤4. 核

一文详解JavaScript中的fetch方法

《一文详解JavaScript中的fetch方法》fetch函数是一个用于在JavaScript中执行HTTP请求的现代API,它提供了一种更简洁、更强大的方式来处理网络请求,:本文主要介绍Jav... 目录前言什么是 fetch 方法基本语法简单的 GET 请求示例代码解释发送 POST 请求示例代码解释

详解nginx 中location和 proxy_pass的匹配规则

《详解nginx中location和proxy_pass的匹配规则》location是Nginx中用来匹配客户端请求URI的指令,决定如何处理特定路径的请求,它定义了请求的路由规则,后续的配置(如... 目录location 的作用语法示例:location /www.chinasem.cntestproxy

SpringBoot首笔交易慢问题排查与优化方案

《SpringBoot首笔交易慢问题排查与优化方案》在我们的微服务项目中,遇到这样的问题:应用启动后,第一笔交易响应耗时高达4、5秒,而后续请求均能在毫秒级完成,这不仅触发监控告警,也极大影响了用户体... 目录问题背景排查步骤1. 日志分析2. 性能工具定位优化方案:提前预热各种资源1. Flowable

CSS will-change 属性示例详解

《CSSwill-change属性示例详解》will-change是一个CSS属性,用于告诉浏览器某个元素在未来可能会发生哪些变化,本文给大家介绍CSSwill-change属性详解,感... will-change 是一个 css 属性,用于告诉浏览器某个元素在未来可能会发生哪些变化。这可以帮助浏览器优化

Python基础文件操作方法超详细讲解(详解版)

《Python基础文件操作方法超详细讲解(详解版)》文件就是操作系统为用户或应用程序提供的一个读写硬盘的虚拟单位,文件的核心操作就是读和写,:本文主要介绍Python基础文件操作方法超详细讲解的相... 目录一、文件操作1. 文件打开与关闭1.1 打开文件1.2 关闭文件2. 访问模式及说明二、文件读写1.

详解C++中类的大小决定因数

《详解C++中类的大小决定因数》类的大小受多个因素影响,主要包括成员变量、对齐方式、继承关系、虚函数表等,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录1. 非静态数据成员示例:2. 数据对齐(Padding)示例:3. 虚函数(vtable 指针)示例:4. 继承普通继承虚继承5.

前端高级CSS用法示例详解

《前端高级CSS用法示例详解》在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交互和动态效果的关键技术之一,随着前端技术的不断发展,CSS的用法也日益丰富和高级,本文将深... 前端高级css用法在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La