LeetCode 752. 打开转盘锁 / 127. 单词接龙 / 773. 滑动谜题(学习双向BFS,A*算法)
Posted Zephyr丶J
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode 752. 打开转盘锁 / 127. 单词接龙 / 773. 滑动谜题(学习双向BFS,A*算法)相关的知识,希望对你有一定的参考价值。
752. 打开转盘锁
2021.6.25每日一题
题目描述
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有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" 时这个锁就会被锁定。
示例 2:
输入: deadends = ["8888"], target = "0009"
输出:1
解释:
把最后一位反向旋转一次即可 "0000" -> "0009"。
示例 3:
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:
无法旋转到目标数字且不被锁定。
示例 4:
输入: deadends = ["0000"], target = "8888"
输出:-1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/open-the-lock
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
我的思路就是暴力解法吧,广度优搜索,从0000出发遍历每一个能达到的数,即每一位加1或者减1,用一个数组存储当前遍历过的数,然后遇到阻碍的数就跳过,当到达目标数时,就是最小步数
class Solution {
public int openLock(String[] deadends, String target) {
//想想怎么搞,从0000到0202按理说,4次,就ok,但是会经过0101,0201或者0102,所以要避开这两个,
//也就是需要先将另外两位中的任一位加一,最后再减1,也就是原本4次+2次,用了6次
//最一般的方法,就是从一个数字,一个个试,如果在deadends里面,就返回,不在就继续
//但是出口在哪呢,就一个个试呗,用一个used数组统计使用的情况
//加一和减一有区别吗,好像没有,减能到达,加也能到达,但是因为统计的是最小次数
//所以也需要考虑减的情况
Set<String> set = new HashSet<>();
for(String s : deadends)
set.add(s);
//标记每个数字是否达到过
boolean[] used = new boolean[10000];
Queue<String> queue = new LinkedList<>();
//两个特殊情况,需要考虑
if(set.contains("0000"))
return -1;
if(target.compareTo("0000") == 0)
return 0;
queue.add("0000");
used[0] = true;
int step = 0;
while(!queue.isEmpty()){
int size = queue.size();
step++;
while(size-- > 0){
String s = queue.poll();
//然后遍历s的下面8种情况,0-3加,4-7减
for(int i = 0; i < 8; i++){
String next = getString(s, i);
if(!set.contains(next) && !used[Integer.valueOf(next)]){
queue.add(next);
used[Integer.valueOf(next)] = true;
}
if(next.compareTo(target) == 0){
//if(next.equals(target))
return step;
}
}
}
}
return -1;
}
public String getString(String s, int index){
boolean flag = index < 4 ? true : false; //前4个是加,后4个减
index = index % 4;
char[] cc = s.toCharArray();
if(flag){
cc[index] = cc[index] == '9' ? '0' : (char)(cc[index] + 1);
}else{
cc[index] = cc[index] == '0' ? '9' : (char)(cc[index] - 1);
}
return new String(cc);
}
}
看了一下题解,主要还是BFS和A算法,A算法不想深究了,之前上机器学习的课讲过,主要学习一下双向BFS。跳转到题127,基本和这个题一样吧,这个题是转变数字,127是变字母,写一下双向BFS的代码
127. 单词接龙
题目描述
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
序列中第一个单词是 beginWord 。
序列中最后一个单词是 endWord 。
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典 wordList 中的单词。
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。
示例 1:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
示例 2:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
输出:0
解释:endWord "cog" 不在字典中,所以无法进行转换。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-ladder
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
双向BFS,来降低搜索空间的宽度,当两个队列中存在相同元素时,说明已经找到了变化的路径,返回当前变化所需要的步数
这里主要注意,因为队列中不存在一个方法可以判断是否有一个元素存在,所以需要将队列中的元素放在一个临时的哈希表中判断
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> set = new HashSet<>(wordList);
//如果不在结果集中,直接返回0
if(!set.contains(endWord))
return 0;
if(beginWord.equals(endWord))
return 1;
//双向,两个queue
Queue<String> q1 = new LinkedList<>();
q1.add(beginWord);
Queue<String> q2 = new LinkedList<>();
q2.add(endWord);
//标记字符串的使用情况
Set<String> used = new HashSet<>();
//遍历的步数
int step = 1;
//开始双向搜索
while(!q1.isEmpty() && !q2.isEmpty()){
//从较小的一遍开始搜索,定义q1为较小的那边
if(q1.size() > q2.size()){
Queue<String> temp = q1;
q1 = q2;
q2 = temp;
}
//对q1进行扩散
int len = q1.size();
step++;
while(len-- > 0){
//取出当前字符串
String s = q1.poll();
//判断这个字符串扩散后的结果
boolean flag = getString(s, q1, q2, used, set);
if(flag)
return step;
}
}
return 0;
}
public boolean getString(String s, Queue<String> q1, Queue<String> q2, Set<String> used, Set<String> set){
int l = s.length();
char[] cc = s.toCharArray();
Set<String> temp = new HashSet<>(q2);
for(int i = 0; i < l; i++){
char cur = cc[i];
for(char j = 'a'; j <= 'z'; j++){
if(cc[i] == j)
continue;
cc[i] = j;
//新的字符串
String next = new String(cc);
if(set.contains(next) && !used.contains(next)){
q1.add(next);
used.add(next);
}
if(temp.contains(next))
return true;
}
cc[i] = cur;
}
return false;
}
}
773. 滑动谜题
2021.6.26每日一题,和昨天的题思路基本一样,就写一起了
题目描述
在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示.
一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换.
最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。
给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。
示例:
输入:board = [[1,2,3],[4,0,5]]
输出:1
解释:交换 0 和 5 ,1 步完成
输入:board = [[1,2,3],[5,4,0]]
输出:-1
解释:没有办法完成谜板
输入:board = [[4,1,2],[5,0,3]]
输出:5
解释:
最少完成谜板的最少移动次数是 5 ,
一种移动路径:
尚未移动: [[4,1,2],[5,0,3]]
移动 1 次: [[4,1,2],[0,5,3]]
移动 2 次: [[0,1,2],[4,5,3]]
移动 3 次: [[1,0,2],[4,5,3]]
移动 4 次: [[1,2,0],[4,5,3]]
移动 5 次: [[1,2,3],[4,5,0]]
输入:board = [[3,2,4],[1,5,0]]
输出:14
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sliding-puzzle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
单向的BFS,将数组处理成字符串,就和昨天题基本一模一样了
class Solution {
//预处理相邻位置
int[][] neighbor = {{1, 3}, {0, 2, 4}, {1, 5}, {0, 4}, {1, 3, 5}, {2, 4}};
public int slidingPuzzle(int[][] board) {
//还是和昨天一样一个BFS,考虑了一下数字交换怎么进行,还是变成昨天的字符串?或者直接在数组上操作?
//学习一下官解的处理方法
//因为这个数组大小是固定的,所以字符串排列成0 1 2 3 4 5 的形式,
//那么0位置相邻可以交换的位置就是 1 3, 1位置相邻可以交换的就是 0 2 4
//把这个预先进行处理,然后到达某个位置交换的时候就行遍历
//同样用一个set表示已经达到过的状态
//先转换成字符串
StringBuilder sb = new StringBuilder();
for(int i = 0; i < 2; i++){
for(int j = 0; j < 3; j++){
sb.append(board[i][j]);
}
}
String s = sb.toString();
String target = "123450";
if(s.equals(target))
return 0;
Queue<String> queue = new LinkedList<>();
queue.offer(s);
int step = 0;
Set<String> set = new HashSet<>();
set.add(s);
while(!queue.isEmpty()){
int size = queue.size();
step++;
while(size-- > 0){
String curr = queue.poll();
int index = 0;
for(int i = 0; i < curr.length(); i++){
if(curr.charAt(i) == '0'){
index = i;
break;
}
}
int[] pos = neighbor[index];
//将curr中的两个位置进行交换得到新的字符串
for(int i = 0; i < pos.length; i++){
String next = getString(curr, index, pos[i]);
if(!set.contains(next)){
set.add(next);
queue.offer(next);
}
if(next.equals(target))
return step;
}
}
}
return -1;
}
public String getString(String s, int i, int j){
char[] cc = s.toCharArray();
char temp = cc[i];
cc[i] = cc[j];
cc[j] = temp;
return new String(cc);
}
}
三道题都出现了Astar算法,所以这里对这个算法研究了一下
具体还是看官解的介绍吧,感觉已经算详细了,两个性质也写的很明白,结合代码看会很清晰
class Solution {
//看了一下官解的Astar,然后基本没看懂怎么搞,但是这个代码和bfs代码基本大同小异吧,结合代码看就很明白了
//只不过队列换成优先队列,然后排序的规则改了一下,这个规则也就是主要的算法思想吧
int[][] neighbors = {{1, 3}, {0, 2, 4}, {1, 5}, {0, 4}, {1, 3, 5}, {2, 4}};
public int slidingPuzzle(int[][] board) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
sb.append(board[i][j]);
}
}
String initial = sb.toString();
if ("123450".equals(initial)) {
return 0;
}
//优先队列,按照f进行排列
PriorityQueue<AStar> pq = new PriorityQueue<AStar>((a, b) -> a.f - b.f);
pq.offer(new AStar(initial, 0));
Set<String> seen = new HashSet<String>();
seen.add(initial);
while (!pq.isEmpty()) {
//取出队列顶部的元素,进行更新
AStar node = pq.poll();
for (String nextStatus : get(node.status)) {
if (!seen.contains(nextStatus)) {
if ("123450".equals(nextStatus)) {
return node.g + 1;
}
pq.offer(new AStar(nextStatus, node.g + 1));
seen.add(nextStatus);
}
}
}
return -1;
}
// 枚举 status 通过一次交换操作得到的状态
public List<String> get(String status) {
List<String> ret = new ArrayList<String>();
char[] array = status.toCharArray();
int x = status.indexOf('0');
for (int y : neighbors[x]) {
swap(array, x, y);
ret.add(new String(array));
swap(array, x, y);
}
return ret;
}
public 以上是关于LeetCode 752. 打开转盘锁 / 127. 单词接龙 / 773. 滑动谜题(学习双向BFS,A*算法)的主要内容,如果未能解决你的问题,请参考以下文章