玩两道广度优先搜索算法

玩两道广度优先搜索算法

打开锁转盘

https://leetcode.cn/problems/open-the-lock/

题干:

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。

列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。

示例 1:

输入:deadends = [“0201”,“0101”,“0102”,“1212”,“2002”], target = “0202”
输出:6
解释:
可能的移动序列为 “0000” -> “1000” -> “1100” -> “1200” -> “1201” -> “1202” -> “0202”。
注意 “0000” -> “0001” -> “0002” -> “0102” -> “0202” 这样的序列是不能解锁的,
因为当拨动到 “0102” 时这个锁就会被锁定。


一般遇到最少要操作几次这样的字眼,都在暗示我们可能要用到bfs算法了.

首先bfs算法有一个自己的理解:

bfs常用于寻求两个点之间的最短距离!并且通常都会把起点和终点都给你,让你去算二者的最短距离是多少,我们最容易想到的就是暴力穷举,在所有的结果当中,走的步数最少的不就是我们要的结果吗?这其实也就是bfs的本质了,可以理解成这样:给你一个起点,此时你可以有多种选择,选择其中一个,你又可以继续往前走,第一次选择的我们都将其称之为一个路径,每个路径上都派一个人去走,大家每次都走一步,所有的路径上,若出现了某条路径上的那个人成功到达终点了,算法就到此结束,那搭配这个玩法的数据结构就是我们学层序遍历经常会用到的队列,因为层序遍历就是每次处理一层嘛,所以队列就非常符合我们刚才的玩法.

需注意:即使知道bfs怎么玩了,其实还不够,有很多的细节也要注意,比如有一些路径跑着跑着会回到起点,那如果其他人都已经走到头了,也没有到达所谓的终点,bfs岂不是就会死循环了吗.所以对于可能出现环的情况,注意搭配使用一个备忘录,“让我们始终在向前行”.


根据上述思路可以有一个bfs的伪代码:

//假设起点以简单的二叉树的TreeNode的形式给出
Queue<TreeNode> q = new LinkedList<>();//bfs的核心数据结构
HashSet<TreeNode> visited = new HashSet<>();//避免死循环
q.offer(start);
visited.add(start);
int depth=0;//有时候为1,看题目具体要求
while(!q.isEmpty()){
    int sz =q.size();//当前层的节点数量
    for(int i = 0;i<sz;i++){
        TreeNode front = q.poll();
        if(front==target) return depth;
        //此处可能有时候题目会提出让你剪枝,待会看了开锁的题就明白了
        for(TreeNode child:front.neighbor){
            //此处再铺设下一层了
            if(!visited.contains(child)){
                q.offer(child);
            }
        }
    }
    depth++;
}
return -1;//所有人都走到了终点,也没有达到题目要求的

接下来就是用伪代码尝试解决开锁问题:

四位数的锁,每个位都有0-9十个样式,只要变动一位就会出现所谓的"下一层",而且题目的死锁,就是让我们剪枝

public int openLock(String[] deadends, String target) {
        HashSet<String> deadLine = new HashSet<>();
        for(String tmp: deadends){
            deadLine.add(tmp);
        }
        String start = "0000";
        //让start变成target的最少次数
        //显然密码调整过程中是可能变回原样的,所以就是存在环的可能,锁以要避免
        Queue<String> q = new LinkedList<>();
        HashSet<String> visited = new HashSet<>();
        q.offer(start);
        visited.add(start);
        int steps=0;//还没去拨呢
        while(!q.isEmpty()){
            int sz =q.size();
            for(int i = 0;i<sz;i++){
                String front = q.poll();
                if(front.equals(target)) return steps;
                //又因为遇到死锁,就不能以这种状态去继续拨锁了
                if(deadLine.contains(front)) continue;
                //此处开始布设下一层了,所以需要穷举出当前锁号能有多少种下一个情况(讲的有点绕)
                //每个位置都能往上调一格,和往下调一格
                for(int j=0;j<4;j++){
                    String up = plusOne(front,j);
                    if(!visited.contains(up)){
                        q.offer(up);
                        visited.add(up);
                    }
                    String down = minusOne(front,j);
                    if(!visited.contains(down)){
                        q.offer(down);
                        visited.add(down);
                    }
                }
            }
            steps++;
        }
        return -1;
    }
    private String plusOne(String str,int i){
        char[] arr = str.toCharArray();
        if(arr[i]=='9'){
            arr[i]='0';
        }else {
            arr[i]+=1;
        }
        return new String(arr);
    }
    private String minusOne(String str,int i){
        char[] arr = str.toCharArray();
        if(arr[i]=='0'){
            arr[i]='9';
        }else {
            arr[i]-=1;
        }
        return new String(arr);
    }

上述就清楚的明白了,基于伪代码,然后会穷举当前状态的下一个状态都有哪些,什么时候就算抵达目的地,什么时候需要剪枝.都是dfs需要考虑的问题.

此处再来一道类似的dfs:

滑动谜题

https://leetcode.cn/problems/sliding-puzzle/

题干:在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示。一次 移动 定义为选择 0 与一个相邻的数字(上下左右)进行交换.

最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。

给出一个谜板的初始状态 board ,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。

示例 1:自己去网上看,不截图了

输入:board = [[1,2,3],[4,0,5]]
输出:1
解释:交换 0 和 5 ,1 步完成


是不是看到"最少"两个字已经有一点敏感了,没错,敏感是正确的,dfs又在朝我们大喊:“让我来,让我来!”

题干给我们一个二维数组用以说明当前棋盘的情况,我们完全可以将其装换成一维的去看,为什么转,因为转成一维的,方便我们去基于当前状态,穷举下一层啊.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9AsTaLZ-1654758118503)(C:\Users\lebronHArden\AppData\Roaming\Typora\typora-user-images\image-20220609145639069.png)]

上图给出棋盘空白位置如果在5下标位置时能与之交换的三个蓝色区域的下标分别为:1,3,5.当然0在0-5这些个位置,分别能与哪些其他位置进行交换,相比已经很清楚了.

这种简单的情况,我们就可以直接把它以一个二维数组的形式给出了.整体的注意事项还是那些:要不要查重呀?要不要剪枝呀?到终点没有啊?穷举当前状态的下一层的孩子情况啊?…细节在注释中,可自行查看.

 public int slidingPuzzle(int[][] board) {
        //华容道的最少胜利移动次数
        int m = 2;
        int n = 3;
        //将二维数组分布情况转成字符串,好操作一点
        StringBuilder sb = new StringBuilder();
        for(int i = 0;i<m;i++){
            for(int j=0;j<n;j++){
                sb.append(board[i][j]);
            }
        }
        String start = sb.toString();
        //因为每次0号棋子只能和上下左右的某个存在的位置进行交换,所以每次移动的邻格都是可穷举的
        //如果将二维数组从上到下,从左到右依次去编号0,1....
        //可以简单罗列一下,每个位置都能和其他哪些位置进行交换元素
        //简单推导发现:
        /**
         * 0:[1,3]
         * 1:[0,2,4]
         * 2:[1,5]
         * 3:[0,4]
         * 4:[3,5,1]
         * 5:[4,2]*/
         
        //所以我们可以把这个索引映射建成一个变长数组
        int[][] neighbor = new int[][]{
                {1,3},
                {0,2,4},
                {1,5},
                {0,4},
                {3,5,1},
                {4,2}
        };
        //开始bfs算法
        Queue<String> queue = new LinkedList<>();//遍历每一层
        HashSet<String> visited = new HashSet<>();//防止死循环
        String target = "123450";
        queue.offer(start);
        visited.add(start);
        int depth = 0;//当前还没动呢
        while(!queue.isEmpty()){
            int sz = queue.size();
            for(int i = 0;i<sz;i++){
                String front = queue.poll();
                if(front.equals(target)) return depth;
                //找寻0的下标,并将能与之交换的新的棋盘字符串添加至下一层
                int initIndex = 0;
                for(;front.charAt(initIndex)!='0';initIndex++);
                for(int j = 0;j<neighbor[initIndex].length;j++){
                    //将0和能与之交换的位置交换之后得到的新棋盘字符串加入到下一层
                    String new_board = swap(front,initIndex,neighbor[initIndex][j]);
                    if(!visited.contains(new_board)){
                        queue.offer(new_board);
                        visited.add(new_board);
                    }
                }
            }
            depth++;
        }
        return depth;
    }
    private String swap(String str,int i,int j){
        char[] arr = str.toCharArray();
        char tmp = arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
        return new String(arr);
    }

希望对你有所帮助

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值