Day30
重新安排行程
题目
给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。
提示:
如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前 所有的机场都用三个大写字母表示(机场代码)。 假定所有机票至少存在一种合理的行程。 所有的机票必须都用一次 且 只能用一次。 示例 1:
输入:[[“MUC”, “LHR”], [“JFK”, “MUC”], [“SFO”, “SJC”], [“LHR”, “SFO”]] 输出:[“JFK”, “MUC”, “LHR”, “SFO”, “SJC”] 示例 2:
输入:[[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]] 输出:[“JFK”,“ATL”,“JFK”,“SFO”,“ATL”,“SFO”] 解释:另一种有效的行程是 [“JFK”,“SFO”,“ATL”,“JFK”,“ATL”,“SFO”]。但是它自然排序更大更靠后。
思路
这道题目有几个难点:
- 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
- 有多种解法,字母序靠前排在前面,如何该记录映射关系呢 ?
- 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
- 搜索的过程中,如何遍历一个机场所对应的所有机场。
更详细的分析可去
一个行程中,如果航班处理不好容易变成一个圈,成为死循环
用一个数组used记录已经去过的航班
boolean used[] = new boolean[tickets.size()];
有多种解法,字母序靠前排在前面,如何该记录映射关系呢 ?
对初始航班信息进行排序 tickets ,那么遍历的时候会默认使用字母序靠前的
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));
使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
path.size() == tickets.size() + 1
回溯三部曲
回溯模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
- 递归函数参数
List<String> res = null;
List<String> path = new ArrayList<>(); // 记录过程中的路径
public boolean backtracking(List<List<String>> tickets, boolean used[])
注意函数返回值我用的是bool!
我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢?
因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线
tickets需要进行重新排序,字母序靠前排在前面
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));
- 递归终止条件
拿题目中的示例为例,输入: [[“MUC”, “LHR”], [“JFK”, “MUC”], [“SFO”, “SJC”], [“LHR”, “SFO”]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。
所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。
- 单层搜索的逻辑
用used[i]记录使用过的航班,避免死循环
for(int i = 0; i < tickets.size(); i++){
if(!used[i] && tickets.get(i).get(0).equals(path.get(path.size() - 1))){
used[i] = true;
path.add(tickets.get(i).get(1));
if(backtracking(tickets, used)){
return true;
}
path.remove(path.size() - 1);
used[i] = false;
}
}
代码
class Solution {
List<String> res = null;
List<String> path = new ArrayList<>();
public List<String> findItinerary(List<List<String>> tickets) {
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));
boolean used[] = new boolean[tickets.size()];
path.add("JFK");
backtracking(tickets, used);
return res;
}
public boolean backtracking(List<List<String>> tickets, boolean used[]){
if(path.size() == tickets.size() + 1){
res = new ArrayList<>(path);
return true;
}
for(int i = 0; i < tickets.size(); i++){
if(!used[i] && tickets.get(i).get(0).equals(path.get(path.size() - 1))){
used[i] = true;
path.add(tickets.get(i).get(1));
if(backtracking(tickets, used)){
return true;
}
path.remove(path.size() - 1);
used[i] = false;
}
}
return false;
}
}
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”]]
思路
皇后们的约束条件:
- 不能同行
- 不能同列
- 不能同斜线
确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。
下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gsDNruTc-1689406933125)(https://code-thinking-1253855093.file.myqcloud.com/pics/20210130182532303.jpg “51.N皇后”)]
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
回溯三部曲
模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
- 递归函数参数
我依然是定义全局变量二维数组result来记录最终结果。
参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。
List<List<String>> res = new ArrayList<>();
private void backtracking(int n, int row, char [][]boards);
- 递归终止条件
当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。
if(n == row){
res.add(conventArrayList(boards));
return;
}
- 单层搜索的逻辑
递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。
每次都是要从新的一行的起始位置开始搜,所以都是从0开始。
for(int i = 0; i < n; i++){
if(isValid(n, row, i, boards)){
boards[row][i] = 'Q';
backtracking(n, row + 1, boards);
boards[row][i] = '.';
}
}
验证棋盘是否合法
按照如下标准去重:
- 不能同行
- 不能同列
- 不能同斜线 (45度和135度角)
private boolean isValid(int n, int row, int col, char [][]boards){
// 检查列
for(int i = 0; i < row; i++){
if(boards[i][col] == 'Q'){
return false;
}
}
// 检查45度
for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){
if(boards[i][j] == 'Q'){
return false;
}
}
// 检查135度
for(int i = row - 1, j = col + 1; i >= 0 && j <= n - 1; i--, j++){
if(boards[i][j] == 'Q'){
return false;
}
}
return true;
}
- 时间复杂度: O(n!)
- 空间复杂度: O(n)
可以看出,除了验证棋盘合法性的代码,省下来部分就是按照回溯法模板来的。
代码
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char [][]boards = new char[n][n];
for(char []board : boards){
Arrays.fill(board, '.');
}
backtracking(n, 0, boards);
return res;
}
private void backtracking(int n, int row, char [][]boards){
if(n == row){
res.add(conventArrayList(boards));
return;
}
for(int i = 0; i < n; i++){
if(isValid(n, row, i, boards)){
boards[row][i] = 'Q';
backtracking(n, row + 1, boards);
boards[row][i] = '.';
}
}
}
private List<String> conventArrayList(char [][]boards){
List<String> rowStr = new ArrayList<>();
for(char[] board : boards){
rowStr.add(String.copyValueOf(board));
}
return rowStr;
}
private boolean isValid(int n, int row, int col, char [][]boards){
// 检查列
for(int i = 0; i < row; i++){
if(boards[i][col] == 'Q'){
return false;
}
}
// 检查45度
for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){
if(boards[i][j] == 'Q'){
return false;
}
}
// 检查135度
for(int i = row - 1, j = col + 1; i >= 0 && j <= n - 1; i--, j++){
if(boards[i][j] == 'Q'){
return false;
}
}
return true;
}
}
解数独
题目
编写一个程序,通过填充空格来解决数独问题。
一个数独的解法需遵循如下规则: 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 ‘.’ 表示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4SAL6guS-1689406933125)(https://code-thinking-1253855093.file.myqcloud.com/pics/202011171912586.png “解数独”)]
一个数独。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UHmDUnSL-1689406933125)(https://code-thinking-1253855093.file.myqcloud.com/pics/20201117191340669.png “解数独”)]
答案被标成红色。
提示:
- 给定的数独序列只包含数字 1-9 和字符 ‘.’ 。
- 你可以假设给定的数独只有唯一解。
- 给定数独永远是 9x9 形式的。
思路
N皇后问题 (opens new window)是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置。
本题就不一样了,本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
回溯三部曲
- 递归函数以及参数
递归函数的返回值需要是bool类型,为什么呢?
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
private boolean backtracking(char[][] board)
- 递归终止条件
本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
不用终止条件会不会死循环?
递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
- 递归单层搜索逻辑
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for(int i = 0; i < board.length; i++){
for(int j = 0; j < board[i].length; j++){
if(board[i][j] != '.') continue;
for (char k = '1'; k <= '9'; k++){
if(isValid(i, j, k, board)){
board[i][j] = k;
if(backtracking(board)) return true;
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
注意这里return false的地方,这里放return false 是有讲究的。
因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
那么会直接返回, 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!
判断棋盘是否合法
判断棋盘是否合法有如下三个维度:
- 同行是否重复
- 同列是否重复
- 9宫格里是否重复
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
private boolean isValid(int row, int col, char val, char[][] board){
for(int i = 0; i < 9; i++){
if(board[i][col] == val) return false;
}
for(int i = 0; i < 9; i++){
if(board[row][i] == val) return false;
}
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for(int i = startRow; i < startRow + 3; i++){
for(int j = startCol; j < startCol + 3; j++){
if(board[i][j] == val) return false;
}
}
return true;
}
代码
class Solution {
public void solveSudoku(char[][] board) {
backtracking(board);
}
private boolean backtracking(char[][] board){
// 本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for(int i = 0; i < board.length; i++){
for(int j = 0; j < board[i].length; j++){
if(board[i][j] != '.') continue;
for (char k = '1'; k <= '9'; k++){
if(isValid(i, j, k, board)){
board[i][j] = k;
if(backtracking(board)) return true;
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
private boolean isValid(int row, int col, char val, char[][] board){
for(int i = 0; i < 9; i++){
if(board[i][col] == val) return false;
}
for(int i = 0; i < 9; i++){
if(board[row][i] == val) return false;
}
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for(int i = startRow; i < startRow + 3; i++){
for(int j = startCol; j < startCol + 3; j++){
if(board[i][j] == val) return false;
}
}
return true;
}
}