玩两道广度优先搜索算法
打开锁转盘
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又在朝我们大喊:“让我来,让我来!”
题干给我们一个二维数组用以说明当前棋盘的情况,我们完全可以将其装换成一维的去看,为什么转,因为转成一维的,方便我们去基于当前状态,穷举下一层啊.
上图给出棋盘空白位置如果在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);
}
希望对你有所帮助