代码随想录第二十五天|回溯算法part05--332.重新安排行程、51.N皇后、37.解数独

刷题小记:

三道困难题,理解成本不低,推荐结合题解视频进行理解。回溯问题的本质是暴力搜索,在面对过于复杂的问题时,要把握事物的主要矛盾,即应当先实现基本思路,再考虑剪枝(次要矛盾),否则可能不但没成功剪枝,反倒“枝横叶乱”。

332.重新安排行程(332.重新安排行程)

题目分析:

给定一个航线列表List> tickets,其中tickets[i] = [fromi, toi]表示飞机出发和降落的机场地点。请对该行程进行重新规划排序,条件如下:

  1. 行程必须从"JFK"机场出发
  2. 若存在多种有效的行程(出发点相同或降落点相同),请按字典顺序返回最小的行程组合(比较不同的两者)
  3. 假定所有机票至少存在一种合理的行程
  4. 所有的机票必须都用且只使用一次

用例限定条件:

  • ticket数量在1到300之间
  • from和to等长,均为3
  • from和to都仅有大写英文字母组成
  • from一定不等于to

解题思路:

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回溯。

解题步骤:

全局参数:
  • 返回值resPath
  • 记录值curPath
按字典排序的实现手段:
  1. 使用Collections类下的sort方法(可借助正则表达式定义排序方法
  2. 使用TreeMap,默认的Comparator就是按字典升序排序
回溯算法:

回溯的终止条件:

  • tickets全部用完(行程组合的长度=机票数+1),则将resPath置为curPath的浅拷贝,并返回true。
  • 讨论:是否需要比较不同的可行的行程组合?
  • :无需,在添入组合前已经进行比较。

回溯的参数:tickets,整型数组used,当前层的from(上一层的to)

回溯的返回值:boolean,表示是否已找到结果

回溯的搜索遍历逻辑:

  • 先将from添加至curPath
  • 做回溯的终止条件判断
  • 遍历符合条件的ticket:
    • 符合条件:
      • 对应的used不为1(0代表未用,1代表已用)
      • ticket.get(0)符合from
    • 按对应的to的排序尝试使用该ticket并向下递归
      • 若递归返回值为true,则直接返回true
      • 否则继续遍历符合条件的ticket
  • 若符合条件的ticket遍历完毕,则返回false

补充:该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);
        }
    }

}

关于Collections.sort()和Comparator、Comparable的整理:

以332.重新安排行程为例:

List> tickets中的每一个元素,即ticket,是长度为2的List,其中:ticket.get(0) 表示机票的起飞点,ticket.get(1) 表示机票的降落点。

现希望按照降落点字典顺序对机票进行排列:

定义Comparator(比较器):

Comparator> comparator = (a, b) -> a.get(1).compareTo(b.get(1));

即,对于类型为List的对象,按照其第2个元素进行升序排序。

定义Collections.sort():

Collections.sort(tickets, comparator);

即,对于tickets,使用comparator进行排序。

简化版:

Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)))

即,针对tickets中的元素,对其按照其第二个元素进行排序。

这是正则表达式的用法之一,有空再做整理。

拓展:

Comparator 是一个非常灵活的接口,可以在几乎所有需要定义排序规则的场景中使用。它不仅适用于简单的排序,还可以结合流操作、集合框架、多线程等高级特性,实现复杂的排序逻辑。

1.排序相关:
  • Arrays.sort():
String[][] array = {{"a", "b"}, {"c", "d"}, {"e", "f"}};
Arrays.sort(array, Comparator.comparing(arr -> arr[1]));// 按二维数组的第二列对行进行排序
  • TreeSet、TreeMap或PriorityQueue:
TreeSet set = new TreeSet<>(Comparator.reverseOrder());//字典逆序
TreeMap map = new TreeMap<>(Comparator.reverseOrder());//对键比较,字典逆序
PriorityQueue queue = new PriorityQueue<>(Comparator.naturalOrder());//字典正序,默认情况
2.自定义排序规则:
  • 按多个字段排序:组合了多个字段的比较逻辑
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和Comparator的区别:
Comparable定义:

Comparable 是一个接口,用于定义对象的自然排序规则。如果一个类实现了 Comparable 接口,那么这个类的对象就有了一个默认的排序方式。这个排序方式通常是基于对象的某个属性或逻辑的自然顺序。

Comparable方法:

只包含一个方法——int compareTo(T o);。

  • 参数 o 是另一个对象,用于与当前对象进行比较。
  • 返回值:
    • 如果当前对象小于 o,返回负整数。
    • 如果当前对象等于 o,返回零。
    • 如果当前对象大于 o,返回正整数。
int compareTo(T o);
Comparable举例:

假设我们有一个 Person 类,希望按照年龄进行自然排序:

Person通过实现Comparable接口,重写compareTo方法,从而实现比较。

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 是一个接口,用于定义对象的外部排序规则。它允许为同一个类定义多个排序规则,而这些规则可以与类的自然排序规则不同。

Comparator方法:

Comparator 接口包含以下两个核心方法:

  1. int compare(T o1, T o2);
    • 比较两个对象 o1o2 的顺序。
    • 返回值规则与 ComparablecompareTo 方法相同。
  1. boolean equals(Object obj);
    • 用于比较两个比较器是否相等(通常不需要手动实现)。
Comparator举例:

假设我们仍然使用 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]
    }
}

51.N皇后(51.N皇后)

题目分析:

在国际象棋中,皇后可以攻击与之处在同一行,同一列或同一对角斜线上的棋子。

N皇后问题,研究的是如何将n个皇后放在n×n的棋盘中,并且使他们彼此之间不能相互攻击。

现在,给定一个整数n,返回所有不同的n皇后解决方案:

每一种解法包含一个不同的N皇后棋子放置方案,方案中'Q'和'.'分别代表皇后和空位。

例如:[".Q..","...Q","Q...","..Q."],表示4个皇后分别位于4×4棋盘中第一行的第二个,第二行的第四个,第三行的第一个,第四行的第三个。

解题思路:

要求使用回溯算法暴力搜索全部情况,我们可以先分析一些规律。

显然,对于n个棋子,假设按行递归,则回溯递归的层数为n,那么每层遍历的宽度即为按列遍历的区间n。

遍历层次即当前遍历行row,对于尝试的列col:

  • 检查row之前的所有行的col列
  • 检查左上方对角线,即row--,col--
  • 检查右上方对角线,即row--,col++

因为按行遍历,所以无需检查同行。

由此,我们得到了3个规则的判别方式。

解题步骤:

初始化棋盘字符串数组chessboard(用'.'填充每一处)

全局参数:

  • 返回列表resList

回溯方案:

  • 回溯的参数:整数n,当前递归层次row(当前遍历行),棋盘字符串数组chessboard(避免从resList取方案时使用get造成的时间开销)
  • 回溯的返回值:void
  • 回溯的终止条件:row等于n,将chessBoard逐行转变成字符串得到完整方案,添加至resList并返回。
  • 回溯的搜索遍历逻辑:
    • 按列遍历,尝试将第level行的皇后添入棋盘并更新方案
      • 检查行为row、列为col的填充方案是否合法
      • 如果合法
        • 填入'Q'
        • 向下一行递归
        • 回溯chessboard[row][col]处为'.'

总结反思:

重点在于理解棋盘问题中的递归深度和遍历宽度。

对于棋盘的操作,可以借助二维的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;
    }
}

37.解数独(37.解数独)

题目分析:

给定一个9×9的棋盘字符串数组,有些地方已经填入1-9之间的数字(整数),没填入数字的空白格用‘.’表示。

数独规则如下:

  1. 数字1-9在每一行只出现一次
  2. 数字1-9在每一列只出现一次
  3. 数字1-9在每一个以粗实线分割的3×3宫内只能出现一次。(9×9的棋盘刚好划分成九个3×3的宫)

解题思路:

先将九宫情况分析:

对于一个位于i行j列(取0~8)的数,其位于(i / 3)层,(j / 3)个宫内。

然后考虑回溯递归方式:

由于棋盘上的每一格都要添入数字,那么先行后列,逐个递归

再设计合法性检验方式:

对于board[i][j]:

  • 按行检查:board[i][0~8]
  • 按列检查:board[0~8][j]
  • 按宫检查:
    • 宫层:m = i / 3
    • 宫列:n = j / 3
    • 检查board[0~2 + 3*m][0~2 + 3*n]

最后得到解题步骤如下

解题步骤:

二维递归即逐个递归的思想很重要。

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;
    }
}

你可能感兴趣的:(代码随想录算法训练营一刷,算法,java,数据结构,leetcode)