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 void swap(char[] array, int x, int y) {
        char temp = array[x];
        array[x] = array[y];
        array[y] = temp;
    }
}

//主要算法思想
class AStar {
    // 曼哈顿距离,即每个位置到达其他位置所需要的步数
    public static int[][] dist = {
        {0, 1, 2, 1, 2, 3},
        {1, 0, 1, 2, 1, 2},
        {2, 1, 0, 3, 2, 1},
        {1, 2, 3, 0, 1, 2},
        {2, 1, 2, 1, 0, 1},
        {3, 2, 1, 2, 1, 0}
    };

    public String status;
    public int f, g, h;

    //G(x) 表示从起点 s 到节点 x 的「实际」路径长度,注意 G(x) 并不一定是最短的
    //H(x) 表示从节点 x 到终点 t 的「估计」最短路径长度,称为启发函数;
    //H∗(x) 表示从节点 x 到终点 t 的「实际」最短路径长度,这是我们在广度优先搜索的过程中无法求出的,我们需要用 H(x) 近似H∗(x);
    //F(x) 满足 F(x) = G(x) + H(x),即为从起点 s 到终点 t 的「估计」路径长度。
    //我们总是挑选出最小的 F(x) 对应的 x 进行搜索,因此 A* 算法需要借助优先队列来实现。

    public AStar(String status, int g) {
        //这里的g其实就是走的步数,也就是到初始状态的距离
        this.status = status;
        this.g = g;
        this.h = getH(status);
        //f是初始状态到结束状态的距离,根据这个距离的大小排序,然后更新状态
        this.f = this.g + this.h;
    }

    // 计算启发函数
    // 计算当前状态到目标状态的距离,也就是到 123450的距离,所以这里要 减‘1’,例如,1需要到0位置,5需要到4位置
    // 而将五个数字都归位以后,0自然就归位了,所以这里不统计0的情况
    public static int getH(String status) {
        int ret = 0;
        for (int i = 0; i < 6; ++i) {
            if (status.charAt(i) != '0') {
                ret += dist[i][status.charAt(i) - '1'];
            }
        }
        return ret;
    }
}


再回头看昨天的题,A*算法其实还是和今天的一样,主要是设计那个求h的算法,而这两个题中,都是计算到目标状态的距离,然后根据距离更新队列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值