程序员面试题精选100题(66)-开密码锁[算法]

问题:
给你一个包含四个环形转轮的密码锁,每个转轮上
1
0个格子对应从

0


‘9’
的1
0
个数字。这些转轮可以向两个方向自由转动,也就是可以从

0

转到
‘9’
,也可以从
‘9’
转到

0

。每一步你只可以转一个转轮一格。这个密码锁还有一些死锁状态
(
输入的
deadends)
,当四个转轮的密码转入这些状态时这个锁就再也打不开了。假设这个锁的初始状态是

0000

,给你一个目标状态
target
,请问你至少需要多少步才能够打开?如果不可能打开该密码锁,则输出-
1



ortant; word-wrap: break-word !important; line-height: 17.12px;”>

ortant; word-wrap: break-word !important;”>

ortant; word-wrap: break-word !important;”>注意序列”0000″ -> “0001” -> “0002” -> “0102” -> “0202”尽管更短只有4步,但由于中间有个死锁状态”0102″,因此这是一个无效的开锁步骤。

ortant; word-wrap: break-word !important; font-size: 16px;”>

ortant; word-wrap: break-word !important; font-size: 16px;”>分析:这是LeetCode第752题。

ortant; word-wrap: break-word !important; font-size: 16px;”>

ortant; word-wrap: break-word !important; font-size: 16px;”>解法一:广度优先搜索

ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>我们解决图的问题的第一步就是找出问题对应的图(Graph)。ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>中的一个ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>解决图的问题的第二步是决定用什么顺序来遍历图。广度优先搜索深度优先搜索。由于题目要求的是找出顶点”0000″到目标状态对应的顶点的最短路径,那么我们应该采用广度优先搜索算法。这是因为广度优先搜索是从源点开始首先达到所有距离源点为1的顶点,接着轮到达所有距离源点为2的所有顶点。根据广度优先搜索从源点到达某一顶点,那么一定是途径从源点到达该结点的最短路径。

ortant; word-wrap: break-word !important;”>

ortant; word-wrap: break-word !important;”>遍历密码锁对应的图时还要注意的是要避开死锁状态对应的顶点,因此一达到这些顶点之后就不能继续往下搜索了。

ortant; word-wrap: break-word !important;”>

ortant; word-wrap: break-word !important;”>下面是基于广度优先搜索的Java代码:ortant; word-wrap: break-word !important;”>

    if (dead.contains(init) || dead.contains(target)) {

        return -1;

    }

 

    Queue<String> queue1 = new LinkedList<>();

    Queue<String> queue2 = new LinkedList<>();

    int steps = 0;

    queue1.offer(init);

    while (!queue1.isEmpty()) {

        String cur = queue1.poll();

        if (cur.equals(target)) {

            return steps;

        }

 

        List<String> nexts = getNexts(cur);

        for (String next : nexts) {

            if (!dead.contains(next) && !visited.contains(next)) {

                visited.add(next);

                queue2.offer(next);

            }

        }

 

        if (queue1.isEmpty()) {

            steps++;

            queue1 = queue2;

            queue2 = new LinkedList<>();

        }

    }

 

    return -1;

}

ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>历。队列queue1里存的是需要转动n次达到的顶点,queue2是和queue1里的顶点相连同时还没有遍历到的顶点。当queue1的里顶点都删掉之后,接着遍历需要转动n+1次到达的顶点,也就是queue2里的顶点,此时变量steps加1。

ortant; word-wrap: break-word !important;”> 

ortant; word-wrap: break-word !important;”>下面是辅助函数getNexts的代码,它的作用是根据密码锁的转动规则得到与某一状态相连的8个状态:ortant; word-wrap: break-word !important;”> 

ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>

ortant; word-wrap: break-word !important;”>这个问题是单个源点(ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>target)的广度优先搜索。我们可以想象当我们到达目标节点的时候,我们同时还遍历了求解树中位于同一层的其他节点。尽管在那一层中我们只需要遍历一个节点,我们却实际上遍历很多不必要的节点,因此单向搜索是存在优化空间的。ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important;”>也就是既从源点出发向着目标搜索,也从目标出发向着源点搜索。如果两个方向搜索最终能够在中间某个位置相遇,那么表明存在从源点到目标的路径。

ortant; word-wrap: break-word !important;”> 

ortant; word-wrap: break-word !important;”>我们只需要在单向搜索的代码上稍微作些改动,就能实现双向搜索,如下所示:

    if (dead.contains(init) || dead.contains(target)) {

        return -1;

    }

 

    if (target.equals(init)) {

        return 0;

    }

 

    Set<String> set1 = new HashSet<>();

    set1.add(init);

    Set<String> set2 = new HashSet<>();

    set2.add(target);

 

    int steps = 0;

    while (!set1.isEmpty() && !set2.isEmpty()) {

        if (set1.size() > set2.size()) {

            Set<String> temp = set1;

            set1 = set2;

            set2 = temp;

        }

 

        Set<String> set3 = new HashSet<>();

        for (String cur : set1) {

            for (String next : getNexts(cur)) {

                if (set2.contains(next)) {

                    return steps + 1;

                }

 

                if (!dead.contains(next) && !visited.contains(next)) {

                    visited.add(next);

                    set3.add(next);

                }

            }

        }

 

        steps++;

        set1 = set3;

    }

 

    return -1;

}

ortant; word-wrap: break-word !important;”>ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>Queue改成了集合ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>HashSet能够更高效地完成这一要求。另外,我们只要做到遍历求解树的一层之后遍历下一层。同一层的遍历顺序并不重要,因此广度优先搜索算法本身也不是一定要用先进先出的队列实现。

ortant; word-wrap: break-word !important;”> 

ortant; word-wrap: break-word !important;”>我们一共用了三个集合。集合ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>set2保存两个方向当前遍历层的节点。我们总是优先遍历ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>set2中节点数目较少的那个方向的节点(通过交换ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>set2确保ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>set2的)。集合ortant; word-wrap: break-word !important; font-family: Calibri, sans-serif;”>set1的节点的下一层节点。

ortant; word-wrap: break-word !important;”> 

ortant; word-wrap: break-word !important;”>上述代码改动虽然很小,但性能优化效果却很明显。本文转载自微信公众号青云算法扫描下面的二维码,关注“青云算法”微信公众号,学习更多高频算法面试题的解法。

《程序员面试题精选100题(66)-开密码锁[算法]》

    原文作者:程序员面试题精选
    原文地址: http://zhedahht.blog.163.com/blog/static/25411174201801535557519/
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞