这三道题目都是困难题目,都是根据代码随想录的思路总结书写,慢慢理解,慢慢熟练
332.重新安排行程
题目链接:力扣
思路
不会,只能看题解了
这道题目主要要解决的几个问题:
1、一个行程中,如果航班处理不好容易变成一个圈,成为死循环
2、有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢
3、使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
4、搜索的过程中,如何遍历一个机场所对应的所有机场
1、怎么保证航班不会死循环
要保证航班不会死循环,首先要选择合适的存储航班信息的容器,才能更好地对航班信息进行处理
需要对已经到达过的航班进行处理,要么删除存在容器中的航班信息,要么就对容器中的航班信息进行标记,这样才能避免航班进入死循环
2、怎么记录航班的信息
一个起始机场可能存在多个到达机场,所以需要使用map<起始机场,<到达机场,到达机场……>>这种数据结构进行存储
因为要保证返回的排序是字典排序最小的形成组合,所以在存储到达机场的时候,应该按照顺序存储,java语言中能保证有序不可重复的集合有TreeSet和TreeMap
使用TreeSet的话就要到达一个航班就删除一个航班,这样才能避免死循环
使用TreeMap的话可以使用值来记录航班出现过的次数,来标记这个航班是否已经使用过了,相当于说我不删,而是做一个标记!
3、终止条件怎么写
回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了
本题的result就是记录路径的(就一条),在如下单层搜索的逻辑中result就添加元素了(因为已经排序过了,找到的第一条就是字典排序最前面的一条)
4、如何遍历一个机场对应的所有机场
在使用map集合映射机场的使用,遍历航班信息,将起始机场作为key,对应的所有到达机场作为value存放到一个集合中
从"JFK"开始,先遍历"JFK"对应的所有到达机场,在遍历"JFK"对应的所有到达机场的所有到达机场,层层递归……当找到满足符合条件航线时,就返回true,此时航线变量res中存储的就是结果
重新安排行程
使用回溯的思路想法并不难理解,就是代码实现起来比较麻烦
1、使用map集合实现对机票的映射信息
2、递归map集合
3、终止条件和收集结果的操作
class Solution { private LinkedList<String> res; // String 是起始机场 // Map<String,Integer> 是到达机场和其航班次数 private HashMap<String,TreeMap<String,Integer>> map; public List<String> findItinerary(List<List<String>> tickets) { // 初始化集合 map = new HashMap<>(); // 使用HashMap实现大Map res = new LinkedList<>(); // 使用链表记录每条航线 // 映射机票信息 for (List<String> ticket : tickets) { TreeMap<String,Integer> temp; // 如果HashMap<String,TreeMap<String,Integer>> map 中已经有了这个起始航班,那就将其加入TreeMap<String,Integer>中 // ticket中第一个元素是起始航班,第二元素是到达航班 if (map.containsKey(ticket.get(0))) { temp = map.get(ticket.get(0)); // ticket.get(1) 到达航班 // temp.getOrDefault(t.get(1), 0) + 1 到达航班出现的次数 temp.put(ticket.get(1),temp.getOrDefault(ticket.get(1), 0) + 1); } else { temp = new TreeMap<>(); // 有序(升序Map) temp.put(ticket.get(1), 1); } map.put(ticket.get(0),temp); } // 将起始机场添加进链表 res.add("JFK"); // 递归查找每一种路线 backtracking(tickets.size()); return res; } private boolean backtracking(int ticketNum) { if (res.size() == ticketNum + 1) { return true; } // 获取当前队列中的最后航班,按照这个航班寻找下一个可能的航班 String last = res.getLast(); if (map.containsKey(last)) { // 如果集合中存在最后航班的到达机场 // map.get(last).entrySet() 获取起始航班对应的到达机场 // Map.Entry<String,Integer> target 到达航班机场对应的set集合 for( Map.Entry<String,Integer> target:map.get(last).entrySet() ) { // 获取该到达航班使用过的次数 int count = target.getValue(); // 如果等于0 说明这个航班已经使用过了,避免死循环 // 如果大于0 说明这个航班还有次数 if (count > 0) { // 将合法航班添加到航线中 res.add(target.getKey()); target.setValue(count - 1); // 使用过一次了 if (backtracking(ticketNum)) { return true; } res.removeLast(); // 回溯 target.setValue(count); } } } return false; } }
51. N皇后
题目链接:力扣
思路
N皇后是使用回溯算法的经典题目,棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了
棋盘上的皇后们的约束条件:
1、不能同行 (递归的深度就自动检测了)
2、不能同列
3、不能同斜线
要做好N皇后要做到三点:
1、分清楚遍历的宽度和递归的深度
2、怎么判断皇后位置是否合法
3、怎么收集结果
N皇后的思路并不难,难得是对各块的操作
之前的回溯题目都是对一维数组进行处理的,N皇后是对二维数组进行处理
N皇后
class Solution { List<List<String>> result = new ArrayList<>(); List<String> path = new ArrayList<>(); // 主函数 public List<List<String>> solveNQueens(int n) { char[][] chessboard = new char[n][n]; for (char[] ch : chessboard) { Arrays.fill(ch,'.'); } backtracking(n,0,chessboard); return result; } // 回溯函数 // n 棋盘的列 // row 棋盘的行 // chessboard 皇后具体的位置 void backtracking(int n,int row, char[][] chessboard) { // 当row走到最后一行的时候,就可以收集结果了 if (row == n) { // 将二维数组中的内容转换成List<String> for (char[] ch : chessboard) { path.add(String.copyValueOf(ch)); } result.add(new ArrayList(path)); path.clear(); return; } // 对棋盘进行递归 for (int col = 0; col < n; col++) { // 判断皇后所在当前位置是否合法 if (isVaild(row,col,n,chessboard)) { // 如果合法,就可以给当前棋盘位置赋值皇后 chessboard[row][col] = 'Q'; backtracking(n,row+1,chessboard); chessboard[row][col] = '.'; } } } // 判断合法函数 boolean isVaild(int row, int col, int n, char[][] chessboard){ // 检查列 for (int i = 0; i < row; i++) { if (chessboard[i][col] == 'Q') { return false; } } // 检查对角线 // 检查45度对角线 // 检查这一行上面的,从左边开始 // i >=0 && j >=0 往45度 左上角 检测的界限 for (int i = row - 1, j = col - 1; i >=0 && j >=0; i--,j--) { if (chessboard[i][j] == 'Q') { return false; } } // 检查135度对角线 // i >=0 && j <= n - 1 往135度 右上角 检测的界限 for (int i = row - 1, j = col + 1; i >=0 && j <= n - 1; i--,j++) { if (chessboard[i][j] == 'Q') { return false; } } return true; } }
37. 解数独
题目链接:力扣
思路
解数独要比N皇后多一个维度,解数独和N皇后都是棋盘,行列都是相同的,不同的是N皇后初始化完二维数组后每一格的元素只有一种情况,但是解数独每一个有9中情况
所以需要二维递归。一层递归是在当前格子,另一层递归是在后面的格子
1、遍历棋盘格子
2、递归当前格子的不同情况
3、判断行合法
4、判断列合法
5、判断九宫格内合法
解数独
class Solution { public void solveSudoku(char[][] board) { backtracking(board); } // 递归函数 private boolean backtracking(char[][] board){ // 双层循环遍历棋盘上的网格 // 一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性! for (int i = 0; i < 9; i++) { // 遍历行 for (int j = 0; j < 9; j++) { // 遍历列 if (board[i][j] != '.') { // 跳过原始数字 continue; } // 每格上的元素从1-9 不断试验 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宫格里是否重复 */ boolean isValid(int row,int col, char val, char[][]board){ // 同行是否重复 for (int i = 0; i < 9; i++) { if (board[row][i] == val) { return false; } } // 同列是否重复 for (int i = 0; i < 9; i++) { if (board[i][col] == val) { return false; } } // 9宫格里面是否重复 // 先判断当前九宫格的范围 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; } }