欧拉图问题——如何实现最后走死胡同 2020-08-31(未允禁

2020-08-31  本文已影响0人  9_SooHyun

1.欧拉图

对于一个有向图或无向图G,
如果能够不重复地一次走完所有的边,且起点和终点不需相同,称G含有欧拉路径,G为半欧拉图
如果能够不重复地走完所有的边,且起点和终点相同,即从起点出发不重复地一次走完所有边后再次回到起点,称G含有欧拉回路,G为欧拉图
欧拉图是特殊的半欧拉图

图G是欧拉图的充要条件<——>G有欧拉回路<——>G的每个节点的出入度相同 / G的每个节点的相关边有偶数条
有了上面的条件,不难得到图G是半欧拉图的充要条件:图G是半欧拉图的充要条件<——>G有欧拉路径<——>除起点和终点外,G的每个节点的出入度相同 / 除起点和终点外,G的每个节点的相关边有偶数条

简单来说,欧拉图问题就是常见的图的一笔画问题

当我们遇到一个图结构的问题时,可以看看它是否是一个欧拉图或者半欧拉图。如果是的话,往往可以将问题抽象为求一笔画的路径

2.code model

先看Hierholzer算法
问题简述:现给出一个有向欧拉图。求一个欧拉回路

Hierholzer算法过程:
选择任一顶点为起点。DFS深度优先搜索,访问所有相邻顶点。
将经过的边都"删除"。这里的删除,本质上是指当前的边走过后,不可重复经过。可以通过维护一个set,存储所有已经经过的边,来实现所谓的“删除”效果
如果当前顶点已经没有相邻边,则将顶点【入栈】
栈中的顶点倒序输出,就是从起点出发的欧拉回路。

求欧拉路径也是同样,只是起点必须是奇数度的顶点,而不能任意顶点,其他不变

伪代码,十分简洁

visited = set()
dfs(node):
    for nei in neibors:
        # 如果这条边未曾走过
        if node-nei not in visited:
            # 加入visited集合来模拟删除
            visited.add(node-nei)
            dfs(nei)
    # 遍历完所有邻居后,所有相邻边也被删除,当前顶点已经没有相邻边,【入栈】
    stack.push(node)

从代码的结构看,Hierholzer算法就是一个十分标准的【后序遍历DFS】

下面通过图例说明:


image1

这是一个半欧拉图,欧拉路径顺序应该是J-B-J-A
我们从起点J出发,dfs它的所有相邻节点并且把经过的边删除,可能有两种情况

  1. 从J开始,先访问邻居B,回环边。这时dfs下去会依次删除JB, BJ, JA。这样所有边都删掉了,每个顶点都木有邻居了,根据程序的顺序,A-J-B-J依次入栈
  2. 从J开始,先访问邻居A,死胡同。这时删除JA,这样顶点A木有邻居了,A入栈;然后从J访问邻居B,dfs下去会依次删除JB, BJ,此时再次来到J,而J没有邻居了,J入栈,然后B入栈,然后J入栈。整体顺序仍然是A-J-B-J

3.对Hierholzer算法的剖析

我不再正向审视问题,不再正序地必须要求死胡同这条路最后再走;而是反向思考问题,路的顺序已不重要,而只保证逻辑终点最先被结算。这种【后序遍历+栈】的组合拳,用逆向思维保证了正向顺序

4.例题

4.1 leetcode 332. 重新安排行程

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

示例:
输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。

分析:输入list的每一个元素都可以看作一条边,这样题目抽象出来就是一次遍历所有边且不重复,典型的欧拉图问题

class Solution:
    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        def dfs(curr: str):
            while vec[curr]:
                # pop就是删除边了!每走过一条边就删除一条
                tmp = heapq.heappop(vec[curr])
                dfs(tmp)
            # 遍历完所有下一结点后,才将当前结点入栈,保证单度结点最先入栈
            stack.append(curr)

        vec = collections.defaultdict(list)
        # 建立有向图邻接表
        for depart, arrive in tickets:
            vec[depart].append(arrive)
        # 排序
        for key in vec:
            heapq.heapify(vec[key])
        
        stack = list()
        dfs("JFK")
        return stack[::-1]

4.2 leetcode 753. 破解保险箱

有一个需要密码才能打开的保险箱。密码是 n 位数, 密码的每一位是 k 位序列 0, 1, ..., k-1 中的一个 。
你可以随意输入密码,保险箱会自动记住最后 n 位输入,如果匹配,则能够打开保险箱。
举个例子,假设密码是 "345",你可以输入 "012345" 来打开它,只是你输入了 6 个字符.
请返回一个能打开保险箱的最短字符串

示例2:
输入: n = 2, k = 2
输出: "00110"
说明: "01100", "10011", "11001" 也能打开保险箱

分析:
这种单个字符变化的问题,基本上都可以抽象为图问题。但本题的难点在于发现能够抽象为欧拉图问题以及如何抽象成欧拉图。
对于一个n位长度的字符串,我们选择n-1长度的字符串s作为结点,每个结点共有k条出边,每条边分别对应0 - k-1;每个结点共有k条入边,每条边也分别对应0 - k-1。node+出边可以构成s+edge_val的密码,且到达的结点为(s+edge_val)[1:]。例如,n=3, k=2的情况下,结点00和出边1可以构成密码001,而达到结点01.。这样,每个结点都具有k入和k出,明显的欧拉图问题。所求 = 初始结点值+路径序,从任意一个结点开始都可以

欧拉图示例-图片来源于leetcode题解
class Solution(object):
    def crackSafe(self, n, k):
        walk = set()
        ans = []
        def dfs(node):
            for x in range(k):
                end_ele = str(x)
                # 结点是node,end_ele表示边,nei表示node的一条出边
                nei = node + end_ele
                # 如果nei这条边没走过,走它
                if nei not in walk:
                    walk.add(nei)
                    dfs(nei[1:])
                    ans.append(end_ele)

        dfs("0" * (n-1))
        # ans是倒序的,因此"0" * (n-1)加在最后
        return "".join(ans) + "0" * (n-1)
上一篇下一篇

猜你喜欢

热点阅读