Leetcode332. 重新安排行程——欧拉回路解法

文章目录

  • 引入
  • Hierholzer 算法
  • 其他题目解析

引入

前些天忙着面试,也不知道是哪一天遇到了解欧拉回路的题:332. 重新安排行程,一直存到了今天正式开始理解和解决,题目是这样的:

给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。

这道题文字描述了很长,但是可以简单的理解:(仔细体会)
给定一个 n 个点 m 条边的图,要求从指定的顶点出发,经过所有的边恰好一次(可以理解为给定起点的「一笔画」问题),使得路径的字典序最小

这种「一笔画」问题与欧拉图或者半欧拉图有着紧密的联系,通过图中所有边恰好一次且行遍所有顶点的通路或者回路,称为欧拉通路或者欧拉回路。

让我们从最简单的开始理解,下面这张图,从JFK开始,一笔画有几种解法?
Leetcode332. 重新安排行程——欧拉回路解法_第1张图片
我们很容易从直观上看出解法的个数,即:

  • JFK→AAA→JFK→BBB→JFK
  • JFK→BBB→JFK→AAA→JFK

原则1:既然要求字典序最小,那么我们每次应该贪心地选择当前节点所连的节点中字典序最小的那一个,并将其入栈。最后栈中就保存了我们遍历的顺序。

为了保证我们能够快速找到当前节点所连的节点中字典序最小的那一个,我们可以使用优先队列存储当前节点所连到的点,每次我们 O(1)地找到最小字典序的节点,并 O(logm) 地删除它。

然而如果使用原则1来考虑下面这种情况:
Leetcode332. 重新安排行程——欧拉回路解法_第2张图片
原则1告诉我们要贪心的选择字典序最小的节点,当我们先访问 AAA 时,我们无法回到 JFK,这样我们就无法访问剩余的边了。

所以仅仅依靠原则1是不行的,还需要其他方式。

Hierholzer 算法

Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:

  1. 从起点出发,进行深度优先搜索。
  2. 每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。
  3. 如果没有可移动的路径,则将所在节点加入到栈中,并返回。

我们注意到只有那个入度与出度差为 1 的节点会导致死胡同,比如AAA入度是1,而出度是0,那么必然是最后一个才遍历AAA。

我们可以改变上述步骤3入栈的规则,当我们遍历完一个节点所连的所有节点后,我们才将该节点入栈(即逆序入栈)。

分析一下为什么该方法能行。

代码如下:

public class Solution {
    Map<String, PriorityQueue<String>> map = new HashMap<String, PriorityQueue<String>>();
    List<String> list = new LinkedList<String>();

    public List<String> findItinerary(List<List<String>> tickets) {
        //构造带字典序的欧拉图
        for (List<String> ticket : tickets) {
            String from = ticket.get(0);
            String to = ticket.get(1);
            if (map.containsKey(from)) {
                PriorityQueue<String> priorityQueue = map.get(from);
                priorityQueue.add(to);
            } else {
                PriorityQueue<String> priorityQueue = new PriorityQueue<>();
                priorityQueue.add(to);
                map.put(from, priorityQueue);
            }
        }

        dfs("JFK");
        Collections.reverse(list);
        return list;
    }

    public void dfs(String curr) {
        while (map.containsKey(curr) && map.get(curr).size() != 0) {
            String next = map.get(curr).poll();
            dfs(next);
        }
        list.add(curr);
    }
}

其他题目解析

题目753. 破解保险箱,也是一道具备欧拉回路的题,这道题的难点在于建模上。

题目在此就不做赘述,我们来分析下建模方式:
这道题说的是给了 k 个数字,值为 0 到 k-1,让我们组成 n 位密码。我们可以发现,为了尽可能的使钥匙串变短,所以我们的密码之间尽可能要相互重叠。

  • 如果密码是 3 个数,0120 可以被看作密码012和密码120,他们之间共享了12。

我们可以发现,两个长度为 n 的密码最好能共享 n-1 个数字,这样累加出来的钥匙串肯定是最短的。所以,密码共有 n 位,每一个位可以有 k 个数字,那么总共不同的密码总数就有 k n k^n kn 个,而每个密码可以公用 n − 1 n - 1 n1 位,所以破解保险箱的密码最短长度为: n − 1 + k n n - 1 + k^n n1+kn 位。

所以,如何做共享密码的建模呢?
即将所有的 n−1 位数作为节点。每个节点有 k 条边,节点上添加数字 0∼k−1 视为一条边,举例来说明:
比如k=2,n=3的时候。密码要求3位,目前每一位有0,1两个取值。这时候就可以做如下的建模,将【节点代表的 n - 1 位数字 + 边代表的 1 位数字】作为经过该条边后尝试的密码值,而经过边后到达的新节点代表的 n - 1 位数字,可以作为下一个密码的前 n - 1 位数字使用,这样可以保证每个密码值不重复,又完全覆盖了 k^n 个密码值。
Leetcode332. 重新安排行程——欧拉回路解法_第3张图片

class Solution {
    Set<Integer> seen = new HashSet<Integer>();
    StringBuffer ans = new StringBuffer();
    int highest;
    int k;

    public String crackSafe(int n, int k) {
        highest = (int) Math.pow(10, n - 1);
        this.k = k;
        dfs(0);
        for (int i = 1; i < n; i++) {
            ans.append('0');
        }
        return ans.toString();
    }

    public void dfs(int node) {
        for (int x = 0; x < k; ++x) {
            int nei = node * 10 + x;
            if (!seen.contains(nei)) {
                seen.add(nei);
                dfs(nei % highest);
                ans.append(x);
            }
        }
    }
}

你可能感兴趣的:(LeetCode)