Leetcode332.重新安排行程(回溯法)

本文介绍了如何根据《代码随想录》复习算法,通过审题和理解题目要求,使用深度优先遍历和回溯法解决机场路径规划问题。作者首先对输入和输出进行解析,然后构建递归逻辑,通过排序和递归回溯找到符合字典顺序的完整路径。文章强调了递归出口的条件和回溯的重要性,并指出此方法的时间复杂度较高。
摘要由CSDN通过智能技术生成

 最近在跟着《代码随想录》复习算法,顺着做题习惯复习+整理一下(我很菜,但是ipad上画图了很多笔记,终于趁着准备秋招,把这些整理成博客吧)

审题

(当然首先审题,把题目整理成一些能看懂的条件吧,这是我做题的习惯)

1.输入类型:可以简单理解为数组(或者二维数组),数组元素是成对字符串,每个字符串表示一个机场名称。

2.输出类型:也是一个数组,数组元素是字符串(机场名称),数组的顺序表示一条完整的路径

3.执行逻辑:可以通过题目给出的示例看出,输出的条件要满足:是一条完整的不重合的路径(欧拉路径),题目说的字典排序是路径的顺序。

有了审题的信息,是不是不难看出:是让我们根据字典排序做一个搜索呢?在代码中怎么处理这些字符串?用什么方法去搜索呢?

代码逻辑

根据整理的信息,我个人的第一反应是根据字典排序后,做一个图上的深度优先遍历(当然你可能有你的道理),在不考虑复杂度的情况下,这样的思考是非常直观的。尤其在第二个示例上

 从JFK出发,根据字典排序,我们可以在tickets数组中,找到下一条边连接着ATL;

到达ATL后,判断可以利用的边(起点为ATL的边)根据字典排序,下一条边连接着JFK;

。。。是不是很像回溯!!!垃圾递归毁我内存....不过代码逻辑是真的直接干脆

开始写代码:

这是我写递归(回溯法)的基本代码框架(也就是说我什么都没做,定义一个递归函数呗。。。)

class Solution {
    

    public List<String> findItinerary(List<List<String>> tickets) {
            
    }
    boolean backtracking(ArrayList<List<String>> tickets){
            
    }
}

主函数内(跟main没关系啊!我好担心自己表达不清楚)

在类内,定义返回的数组res,以及收纳递归过程的数组path

private LinkedList<String> res;
private LinkedList<String> path = new LinkedList<>();

对于原始的tickets数组,我们希望能够得到一个以终点字典排序的顺序,这样就可以在遍历时直接顺序遍历就行。所以在主函数内,把输入的数组tickets根据tickets[i][1]排个序,这个地方是值得注意的。(参照代码随想录调了下库,如果不服自行手撸)

//以终点(tickets数组元素的第二位数据)排序
Collections.sort(tickets,(a,b)->a.get(1).compareTo(b.get(1)));

另外将题目规定的JFK作为唯一起点,也就是在递归路径path中把"JFK"固定为第一个元素

 //JFK固定为起点
 path.add("JFK");

在主函数内,还需要做最后一件事:“一张票只需要用一次”,也就是说,包含同样起点终点的一条边,只需要被递归函数考虑一次。我们有很多方法去完成这件事,比如利用集合,hash,当然最简单直观省事儿不费内存的方法就是,定义一个bool数组,来标记这条边是否被纳入过

 //标记这张票是否已经用过
 boolean []used = new boolean[tickets.size()];

主函数的任务已经基本完成,只剩下调用递归函数,然后直接返回结果数组res,res是类内的私有变量,可以被类内函数直接修改,所以接下来就来分析递归函数要干嘛了。

递归函数(单一操作,控制返回)

递归函数只考虑单层逻辑的话,它需要服务的事情就是:把当前检索的边里的节点正确纳入路径数组path。一次考虑一条边,也就是一次只会纳入一个节点,这个逻辑很清晰吧!所以其实我要讲的是递归的出口!那么总共需要多少个节点呢?

这部分请见小学数学之线段端点问题~

//路径读取的条件:路径长度等于机票数量+1 
if(path.size()==tickets.size()+1){
    //path中纳入了全部节点,就传给结果数组res
    res = new LinkedList(path);
    //结束这场内存的噩梦吧
    return true;
}

单层逻辑

注意分析:为什么递归函数返回值为boolean?

因为这样可以帮助我们确定正确顺序的出口。如果没有找到,则进行回溯。

            //思考单层逻辑
            for(int i = 0;i < tickets.size();i++){
                //纳入条件:经排序后,当前路径path的最后一个元素,和机票中的起点相同
                if(!used[i]&&tickets.get(i).get(0).equals(path.getLast())){
                    //将终点纳入
                    path.add(tickets.get(i).get(1));
                    //标记该张机票已经被处理过
                    used[i] = true;
                    //如果能够正确返回!!!!!这里很重要
                    if(backtracking(tickets,used)){
                        //通过return true的方式跳出递归过程
                        return true;
                    }
                    //如果没有正确返回,则进行回溯
                    used[i] = false;
                    path.removeLast();
                }
            }

全部代码

class Solution {
    private LinkedList<String> res;
    private LinkedList<String> path = new LinkedList<>();
    public List<String> findItinerary(List<List<String>> tickets) {
            //以终点(tickets数组元素的第二位数据)排序
            Collections.sort(tickets,(a,b)->a.get(1).compareTo(b.get(1)));
            //JFK固定为起点
            path.add("JFK");
            //标记这张票是否已经用过
            boolean []used = new boolean[tickets.size()];
            backtracking((ArrayList)tickets,used);
            return res;
    }
    boolean backtracking(ArrayList<List<String>> tickets,boolean[] used){
            //路径读取的条件:路径长度等于机票数量-1 
            if(path.size()==tickets.size()+1){
                res = new LinkedList(path);
                return true;
            }
            //思考单层逻辑
            for(int i = 0;i < tickets.size();i++){
                //纳入条件:经排序后,当前路径path的最后一个元素,和机票中的起点相同
                if(!used[i]&&tickets.get(i).get(0).equals(path.getLast())){
                    //将终点纳入
                    path.add(tickets.get(i).get(1));
                    //标记该张机票已经被处理过
                    used[i] = true;
                    //如果能够正确返回!!!!!这里很重要
                    if(backtracking(tickets,used)){
                        //通过return true的方式跳出递归过程
                        return true;
                    }
                    //如果没有正确返回,则进行回溯
                    used[i] = false;
                    path.removeLast();
                }
            }
            //循环中给不出结果的,当然默认为false
            return false;
    }
}

总结

类似这样的题目有很多,在准备用递归解决问题时,一定要注意几个点:

1.跳出出递归的条件(这点看似简单,其实很值得深入)

比如本题中,递归出口被默认只有一个,并且当没有找到正确路径时,才进行回溯

这也是为什么要把递归函数返回类型规定为boolean的原因。

2.这不同于组合、组合加、子集等需要纳入所有路径的题目(注意思考区别)

3.这种方法只是在复习回溯法时用到,时间复杂度太高了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值