三道困难题,理解成本不低,推荐结合题解视频进行理解。回溯问题的本质是暴力搜索,在面对过于复杂的问题时,要把握事物的主要矛盾,即应当先实现基本思路,再考虑剪枝(次要矛盾),否则可能不但没成功剪枝,反倒“枝横叶乱”。
给定一个航线列表List> tickets,其中
tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请对该行程进行重新规划排序,条件如下:
用例限定条件:
from和to之间的比较,使用String类的.compareTo()实现:
int result = from.compareTo(to),若result<0则from小于to;若result>0则from大于to。
显然这是一个“最小排列”问题。
排列的第一位的from必须为JFK,若存在多个ticket的from为JFK,则选取to按字典顺序较小者。
接下来递归传入的tickets里面,应当不包含已经使用过的ticket(为避免元素的增删,可以通过传入一个used数组进行标记),并按照上一层的to选取当前层的from,再根据字典顺序比较当前层的to以选定ticket。
每一层都应当将这一层的from添入行程组合,直至所有机票使用完全,即行程组合的长度=机票数+1。
回溯既要对行程组合回溯,也要对可用的tickets回溯。
回溯的终止条件:
回溯的参数:tickets,整型数组used,当前层的from(上一层的to)
回溯的返回值:boolean,表示是否已找到结果
回溯的搜索遍历逻辑:
补充:该used可被map中的值替代。
class Solution {
List resPath = new ArrayList<>();// 记录最终行程组合
List curPath = new ArrayList<>();// 记录当前行程组合
Map> ticketsMap = new HashMap<>();// 记录以from为键,以to及其票数为值的键值对
int num = 0;// 标记tickets张数,减少回溯函数的传递参数
public List findItinerary(List> tickets) {
num = tickets.size();
buildTicketsMap(tickets);// 由tickets得到ticketsMap
curPath.add("JFK");
backtracking("JFK");// 以“JFK”为第一个出发点开始回溯
return resPath;// 返回最终的行程组合
}
public boolean backtracking(String from) {
// 再做终止判断
if (curPath.size() == num + 1) {// 若tickets使用完毕,即行程组合中的机场数比机票数恰好多1
resPath = new ArrayList<>(curPath);
return true;
}
// 首先防止无以该from为起点的机票出现
if (!ticketsMap.containsKey(from)) return false;
for(Map.Entry toMap : ticketsMap.get(from).entrySet()){// toMap是由TreeMap建立的,to串已按字典顺序排列。
int count = toMap.getValue();
if(count > 0){
curPath.add(toMap.getKey());
toMap.setValue(count - 1);
if(backtracking(toMap.getKey())) return true;// 如果找到符合条件的行程组合,一路返回
curPath.remove(curPath.size() - 1);// 回溯行程组合
toMap.setValue(count);// 回溯机票
}
}
return false;
}
public void buildTicketsMap(List> tickets) {
for (List ticket : tickets) {
String from = ticket.get(0);
Map toMap;
/*构造以ticket.get(0)为出发点from的票的降落点to的TreeMap,使to按字典自动升序排列 */
if (!ticketsMap.containsKey(from)) {
toMap = new TreeMap<>();
toMap.put(ticket.get(1), 1);
} else {
toMap = ticketsMap.get(from);
toMap.put(ticket.get(1), toMap.getOrDefault(ticket.get(1), 0) + 1);
}
ticketsMap.put(from, toMap);
}
}
}
以332.重新安排行程为例:
List> tickets中的每一个元素,即ticket,是长度为2的List
现希望按照降落点字典顺序对机票进行排列:
Comparator> comparator = (a, b) -> a.get(1).compareTo(b.get(1));
即,对于类型为List
Collections.sort(tickets, comparator);
即,对于tickets,使用comparator进行排序。
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)))
即,针对tickets中的元素,对其按照其第二个元素进行排序。
这是正则表达式的用法之一,有空再做整理。
Comparator 是一个非常灵活的接口,可以在几乎所有需要定义排序规则的场景中使用。它不仅适用于简单的排序,还可以结合流操作、集合框架、多线程等高级特性,实现复杂的排序逻辑。
String[][] array = {{"a", "b"}, {"c", "d"}, {"e", "f"}};
Arrays.sort(array, Comparator.comparing(arr -> arr[1]));// 按二维数组的第二列对行进行排序
TreeSet set = new TreeSet<>(Comparator.reverseOrder());//字典逆序
TreeMap map = new TreeMap<>(Comparator.reverseOrder());//对键比较,字典逆序
PriorityQueue queue = new PriorityQueue<>(Comparator.naturalOrder());//字典正序,默认情况
List people = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25));
people.sort(Comparator.comparing(Person::getAge).thenComparing(Person::getName));
List names = Arrays.asList("Alice", "bob", "Charlie");
names.sort(String::compareToIgnoreCase);
Comparable 是一个接口,用于定义对象的自然排序规则。如果一个类实现了 Comparable 接口,那么这个类的对象就有了一个默认的排序方式。这个排序方式通常是基于对象的某个属性或逻辑的自然顺序。
只包含一个方法——int compareTo(T o);。
o
是另一个对象,用于与当前对象进行比较。o
,返回负整数。o
,返回零。o
,返回正整数。int compareTo(T o);
假设我们有一个 Person
类,希望按照年龄进行自然排序:
Person通过实现Comparable
public class Person implements Comparable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // 按年龄排序
}
@Override
public String toString() {
return name + ": " + age;
}
}
public class PersonComparableExample {
public static void main(String[] args) {
List people = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25));
Collections.sort(people); // 使用Person的自然排序规则
System.out.println(people); // 输出:[Bob: 25, Alice: 30]
}
}
Comparator
是一个接口,用于定义对象的外部排序规则。它允许为同一个类定义多个排序规则,而这些规则可以与类的自然排序规则不同。
Comparator
接口包含以下两个核心方法:
int compare(T o1, T o2);
o1
和 o2
的顺序。Comparable
的 compareTo
方法相同。boolean equals(Object obj);
假设我们仍然使用 Person
类,但这次我们定义一个按名字排序的 Comparator
:
import java.util.*;
public class PersonComparatorExample {
public static void main(String[] args) {
List people = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25));
// 按名字排序
people.sort(Comparator.comparing(Person::getName));
System.out.println(people); // 输出:[Alice: 30, Bob: 25]
// 按年龄排序(使用自定义Comparator)
people.sort(Comparator.comparingInt(Person::getAge));
System.out.println(people); // 输出:[Bob: 25, Alice: 30]
}
}
在国际象棋中,皇后可以攻击与之处在同一行,同一列或同一对角斜线上的棋子。
N皇后问题,研究的是如何将n个皇后放在n×n的棋盘中,并且使他们彼此之间不能相互攻击。
现在,给定一个整数n,返回所有不同的n皇后解决方案:
每一种解法包含一个不同的N皇后棋子放置方案,方案中'Q'和'.'分别代表皇后和空位。
例如:[".Q..","...Q","Q...","..Q."],表示4个皇后分别位于4×4棋盘中第一行的第二个,第二行的第四个,第三行的第一个,第四行的第三个。
要求使用回溯算法暴力搜索全部情况,我们可以先分析一些规律。
显然,对于n个棋子,假设按行递归,则回溯递归的层数为n,那么每层遍历的宽度即为按列遍历的区间n。
遍历层次即当前遍历行row,对于尝试的列col:
因为按行遍历,所以无需检查同行。
由此,我们得到了3个规则的判别方式。
初始化棋盘字符串数组chessboard(用'.'填充每一处)
全局参数:
回溯方案:
重点在于理解棋盘问题中的递归深度和遍历宽度。
对于棋盘的操作,可以借助二维的chessboard数组完成,并最终将其转成N皇后方案。
class Solution {
List> res = new ArrayList<>();
public List> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {// 初始化棋盘字符串数组
Arrays.fill(c, '.');
}
backTracking(n, 0, chessboard);
return res;
}
public void backTracking(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0;col < n; ++col) {
if (isValid (row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
backTracking(n, row+1, chessboard);
chessboard[row][col] = '.';
}
}
}
public List Array2List(char[][] chessboard) {
List list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
public boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列
for (int i=0; i=0 && j>=0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查右上角对角斜线
for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
给定一个9×9的棋盘字符串数组,有些地方已经填入1-9之间的数字(整数),没填入数字的空白格用‘.’表示。
数独规则如下:
对于一个位于i行j列(取0~8)的数,其位于(i / 3)层,(j / 3)个宫内。
由于棋盘上的每一格都要添入数字,那么先行后列,逐个递归
对于board[i][j]:
最后得到解题步骤如下
二维递归即逐个递归的思想很重要。
class Solution {
public void solveSudoku(char[][] board) {
boolean find = backTrack(board);
return;
}
public boolean backTrack(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') continue;
for (int k = 1; k <= 9; k++) {
if (isValid(board, i, j, k)) {
board[i][j] = Character.forDigit(k, 10);
if (backTrack(board)) return true;
board[i][j] = '.';
}
}
return false;
}
}
return true;
}
public boolean isValid(char[][] board, int row, int col, int num) {
for (int i = 0, j = col; i < 9; i++) {// 按列检查
if (board[i][j]-'0' == num) return false;
}
for (int i = row, j = 0; j < 9; j++) {// 按行检查
if (board[i][j]-'0' == num) return false;
}
int m = row / 3;
int n = col / 3;
for (int i = 0; i < 3; i++) {// 按宫检查
for (int j = 0; j < 3; j++) {
if (board[i + m*3][j + n*3] - '0' == num) return false;
}
}
return true;
}
}