力扣773题:滑动谜题

1. 题目描述

在一个 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 步完成

示例 2:

输入:board = [[1,2,3],[5,4,0]]
输出:-1
解释:没有办法完成谜板

示例 3:

输入: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.length == 2
  • board[i].length == 3
  • 0 <= board[i][j] <= 5
  • board[i][j] 中每个值都 不同

来源:力扣(LeetCode
链接:. - 力扣(LeetCode)

2.题解

仅代表个人首次解答,比较暴力,时空复杂度可能不理想,但我后续已经根据题解调整

class Solution {
    final int[][] directions = {{1,3},{0,2,4},{1,5},{0,4},{1,3,5},{4,2}};
    public int slidingPuzzle(int[][] board) {
        Set<String> visited = new HashSet<>();
        Set<String> endSet = new HashSet<String>();
        endSet.add("123450");
        Set<String> beginSet = new HashSet<>();
        String beginStr = "";
        for(int i = 0;i < board.length;i++) {
            for(int j = 0;j < board[i].length;j++) {
                beginStr += board[i][j];
            }
        }
        if(endSet.contains(beginStr)) {
            return 0;
        }
        beginSet.add(beginStr);
        int step = 0;

        while(!beginSet.isEmpty() && !endSet.isEmpty()) {
            if(beginSet.size() > endSet.size()) {
                Set<String> temp = beginSet;
                beginSet = endSet;
                endSet = temp;
            }

            Set<String> nextLevelVisited = new HashSet<String>();
            for(String str : beginSet) {
                int index = str.indexOf('0');
                char[] chars = str.toCharArray();
                for(int dir : directions[index]) {
                    char[] newChars = swap(chars, dir, index);
                    if(endSet.contains(new String(newChars))) {
                        return step + 1;
                    }else if(!visited.contains(new String(newChars))) {
                        nextLevelVisited.add(new String(newChars));
                        visited.add(new String(newChars));
                    }
                    swap(chars, dir, index);
                }
            }
            beginSet = nextLevelVisited;
            ++step;
        }
        return -1;
    }

    public char[] swap(char[] chars, int swap1, int swap2) {
        char swapChar = chars[swap1];
        chars[swap1] = chars[swap2];
        chars[swap2] = swapChar;
        return chars;
    }
}

使用的是双向广度优先搜索

双向广度优先搜索我也是刚了解,看了官方的例题,通过Leetbook《广度优先搜索》中的双向广度优先搜索一章总结出了一个双向BFS模板,可能考虑不全面,仅供参考:

class Solution {
    public int D_BFS(....) {
        //设置查重哈希集
        Set<T> visited = new HashSet<>();
        // 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用(等价于使用队列)
        Set<T> beginVisited = new HashSet<>();
        beginVisited.add(BEGIN);
        Set<T> endVisited = new HashSet<>();
        endVisited.add(BEGIN2);
        while (!beginVisited.isEmpty() && !endVisited.isEmpty()) {
            // 优先选择小的哈希表进行扩散,考虑到的情况更少
            if (beginVisited.size() > endVisited.size()) {
                Set<T> temp = beginVisited;
                beginVisited = endVisited;
                endVisited = temp;
            }
            // 到这里,保证 beginVisited 是相对较小的集合,nextLevelVisited 在扩散完成以后,会成为新的 beginVisited
            Set<String> nextLevelVisited = new HashSet<>();
            for (T word : 周围子节点数组) {
                if (一些判断条件,例如边界处理) {
                    if (endVisited.contains(经过处理后的word)) {//两个BFS有交集
                        return TRUE;//找到了最短路径
                    }
                    if (!visited.contains(经过处理后的word)) {//如果没有重复
                        nextLevelVisited.add(经过处理后的word);
                        visited.add(经过处理后的word);
                    }
                }
            }
            // 原来的 beginVisited 丢弃,从 nextLevelVisited 开始新的双向 BFS
            beginVisited = nextLevelVisited;
            //如果要找最短路径等,这里是放置路径变量的一个参考点
            //step++;
        }
        return FALSE;
    }
}

此题代码通过模板扩展而来

3.题目思路与代码讲解

别看到困难就歇菜,这道题想到思路并不难,5分钟足以想到完整解答思路,难在代码编写

先说一下思路:看到这道题,无非就是套模板,但是这个二维数组如何表示出来比较复杂,如果在哈希集那里直接往里头加操作过后的二维数组,先不说占的空间大,代码编写就是个问题,所以这里我使用了“数组扁平化”,将二维数组按照一定顺序将里面的元素像串珠子一样串到一个一维数组里面去

但是这样的解法,如何将已经成片的二维数组的下标转换成一维数组的下标又成了个新的问题,但是我想了一个方法,绕过了这个过程,并且无需考虑越界,请看代码片段:

final int[][] directions = {{1,3},{0,2,4},{1,5},{0,4},{1,3,5},{4,2}};

这个数组比较特别,跟普通的变化量数组有很大区别,但是这种方法在小数据处理上非常有用(个人认为)

低情商:枚举所有可能的走法(doge

高情商:这个数组比较特别(doge

没有任何引战意思!!!!看一乐,请勿过度解读!!!

没有任何引战意思!!!!看一乐,请勿过度解读!!!

没有任何引战意思!!!!看一乐,请勿过度解读!!!

这个数组的根本思想是模拟二维数组中0在各个位置时的访问情况,其中{1, 3}代表0在二维数组左上角可以访问到的数据的索引,{0, 4}代表0在左下角可以访问到的数据的索引,以此类推

例如请看二维数组(鄙人不会搞图片,凑活看一下吧)

图A:

0        1        2

3        4        5

扁平化:nums: [0, 1, 2, 3, 4, 5].

可以看到,在图A中0在左上角,此时0可以与下面的3交换也可以与右边的1交换,而在数组扁平化中,3和1分别位于索引3与1,处于directions中

图B

1        2        3

4        0        5

扁平化:nums: [1, 2, 3, 4, 0, 5].

可以看到,在图B的下方中点的0可以访问到上方的2,左方的4和右方的5,在扁平化中分别对应索引1,3和5,处于directions中

以此类推从左到右,从上至下顺序枚举所有的可访问到的元素的索引,之后按照上文顺序制成二维数组,就得到了directions数组

在后面的详解中您就会知道这个数组有什么用

Set<String> visited = new HashSet<>();
Set<String> endSet = new HashSet<String>();
endSet.add("123450");
Set<String> beginSet = new HashSet<>();
String beginStr = "";
for(int i = 0;i < board.length;i++) {
    for(int j = 0;j < board[i].length;j++) {
        beginStr += board[i][j];
    }
}
if(endSet.contains(beginStr)) {
    return 0;
}
beginSet.add(beginStr);
int step = 0;

其中,这里是将扁平化后的问题答案加入哈希集,同时将传入的board数组进行扁平化,为了防止阴险的测试用例,提前测试给予的数组是否与答案相同,如果相同则代表不需要变换

接下来是主实现部分:

while(!beginSet.isEmpty() && !endSet.isEmpty()) {
       if(beginSet.size() > endSet.size()) {
            Set<String> temp = beginSet;
            beginSet = endSet;
            endSet = temp;
       }

       Set<String> nextLevelVisited = new HashSet<String>();
       for(String str : beginSet) {
             int index = str.indexOf('0');
             char[] chars = str.toCharArray();
             for(int dir : directions[index]) {
                 char[] newChars = swap(chars, dir, index);
                 if(endSet.contains(new String(newChars))) {
                     return step + 1;
                 }else if(!visited.contains(new String(newChars))) {
                     nextLevelVisited.add(new String(newChars));
                     visited.add(new String(newChars));
                 }
                 swap(chars, dir, index);
            }
       }
       beginSet = nextLevelVisited;
       ++step;
}
return -1;

/*代码手工排版,看起来有点怪请见谅!*/

其中上面的判断大小的代码是让大小较小的哈希集先处理,可以降低时间复杂度(Leetbook是这么说的)

我的思路是这样的:

对每个beginSet中的元素进行枚举和四个方向的搜索(不越界,需要用到directions数组),并且进行交换枚举的数据,一旦发现两个BFS重合,就返回step,否则将其添加进visited查重哈希集,代表已经查找过,并且加入nextLevelVisited哈希集

int index = str.indexOf('0');
char[] chars = str.toCharArray();
for(int dir : directions[index]) {
     char[] newChars = swap(chars, dir, index);
     if(endSet.contains(new String(newChars))) {
          return step + 1;
     }else if(!visited.contains(new String(newChars))) {
         nextLevelVisited.add(new String(newChars));
         visited.add(new String(newChars));
     }
     swap(chars, dir, index);
}

这部分代码负责对四个方向进行枚举,因为directions数组根据0的位置制成,所以我需要用index获取并暂存0的位置

因为我们直接从扁平化数组中获得0的索引,这和我在编写directions数组时采取的逻辑,顺序全部一样,所以直接获取index下标处的directions子数组就可以获得当前0位置可以交换并且不发生越界的值,之后使用swap方法直接交换两个数据,相当于一次枚举

之后就是一些对比操作,检测BFS有无重合,检测有没有记录查重等等

又因为我们一次枚举只能交换两个数据的值,交换完之后又因为方法传递数组传的不是值而是指针(Java用了指针,只不过没说,这里的指针比较另类,可以理解为C++的引用),这就导致chars有损变化,可能会导致后面的方向的枚举出错,所以再次交换一次,不就把交换的数据换回原位了吗?

最后就是将一些模板上有的补充一下,完善swap方法(这个方法应该都会,此处不再赘述)

当代码运行到最后(指出了while循环)还没有返回,说明没有找到对应的值(要是找到了就直接在while中返回了),根据题目返回-1

4.复杂度分析

时间复杂度:假设初始状态到目标状态的最小步数为 d,那么搜索的状态数最坏情况下是 O(b^d),其中 b 是每个状态的分支因子(即每个状态可以扩展出的子状态数量)

空间复杂度:O(b^d)

鄙人只能写到这种程度了,勿喷拜托!

5.结语

感谢您的观看与支持!这只是鄙人对这道题的解法,肯定不是此题最优解,但还是能感谢您能抽出时间来看我的小破文章!

如有错误感谢在评论区中指出!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值