2

我正在尝试解决 Udacity 上的一个问题,如下所述:

# Find Eulerian Tour
#
# Write a function that takes in a graph
# represented as a list of tuples
# and return a list of nodes that
# you would follow on an Eulerian Tour
#
# For example, if the input graph was
# [(1, 2), (2, 3), (3, 1)]
# A possible Eulerian tour would be [1, 2, 3, 1]

我写的代码如下。它不是超级优雅,但似乎可以完成这项工作。

def getCurPoint(points, curPoint):
    for pair in range(len(points)):
        if curPoint in points[pair]:
            for i in points[pair]:
                if i != curPoint:
                    points.pop(pair)
                    return [curPoint] + getCurPoint(points, i)
    return []

def takeTour(graph):
    point = graph[0][0]
    criticals = []
    points = []
    for pair in range(len(graph)):
        if point in graph[pair] and len(criticals) <= 1:
            criticals.append(graph[pair])
        else:
            points.append(graph[pair])

    stops = [point]

    curPoint = criticals[0][1]

    stops += getCurPoint(points, curPoint)

    for x in criticals[1]:
        if x != point:
            stops.append(x)

    stops.append(point)

    return stops

问题是,当我提交代码时,它通过了每个测试用例,除非graph = [(0, 1), (1, 5), (1, 7), (4, 5), (4, 8), (1, 6), (3, 7), (5, 9), (2, 4), (0, 4), (2, 5), (3, 6), (8, 9)]

知道为什么它会通过测试吗?(如果你有关于如何使这段代码更优雅的提示,我很想听听!)

4

1 回答 1

13

你没有回溯机制getCurPoint()。这意味着您的算法正在使用找到的第一条在图中行进的边,构建一条可能导致死胡同的路径,而无需遍历所有节点。

这正是您的示例所发生的情况。您的算法将从 node 开始0到 node 1。此节点提供 3 条边来继续您的旅行(分别是(1, 5)(1, 7)(1, 6)),但其中一条边会导致死胡同,而无法完成欧拉之旅。不幸的是,您的图形定义中列出的第一条边(1, 5)是错误的路径,不会让您有任何机会到达节点637

要检查我的确认,您可以尝试更改图的给定定义以用 反转边(1, 5)(1, 7)然后查看您的算法将如何正确列出所有节点。

我该如何帮助

首先,为了帮助您自己解决这些问题,您应该始终制作图表(不仅是为了让费曼开心),并尝试在图表上遵循您的算法。这些案例很容易得出,很明显算法不正确,您可能会发现原因。

其次,这个练习是关于回溯的。你知道这是什么吗 ?如果没有,你可以尝试用谷歌搜索这个词,甚至在 stackoverflow 搜索框中搜索它,维基百科也有一篇很好的文章。

最后,我可以给你一些关于你的编码风格的建议。请将它们视为个人信息,并采取似乎适合您当前需求的方式:

如果可能:

  • for x in range(len(graph)):如果您以后才使用,请避免使用graph[x]。Python 提供了一个非常优雅的替换解决方案for edge in graph:。如果你真的需要你正在迭代的元素的索引,你可以选择优雅的: for i, edge in enumerate(graph):
  • 避免你的 3 行构造(你已经使用过两次)涉及 aif中 的孤独for

    for x in criticals[1]:
        if x != point:
            stops.append(x)
    

    更喜欢显式和更小:

    node_a, node_b = critical_edges[1] 
    stops += [node_a] if node_b == node else [node_b]
    
  • 不太重要的代码装饰风格备注:

    • 避免 java CamelCase 用于变量、函数名和方法名,python 在样式上有一个 PEP8,它更喜欢变量名使用带下划线的小写字母。
    • 将“point”重命名为“node”以坚持图形上更广泛使用的词汇
    • 将 'pair' 重命名为 'edge' 以获得对变量含义而不是其类型的描述性命名(尽管您可以考虑将这两种信息混合在所谓的Hungarian Notation中)

应用装饰性备注,例如,我们将getCurPoint重命名为get_next_nodes,其中:

  • variable_with_underscore 而不是 CamelCase 和Point->nodes如前所述,
  • 复数,因为它将返回节点列表
  • 'cur' 被重命名为'next',因为它是关于获取下一个节点。

以更 Python 的方式重写代码的示例

请注意,这只是一个重写,并且之前的错误始终存在。看看get_next_nodes()函数是如何变化的:

def get_next_nodes(edges, cur_node):
    connected_edges = [x for x in edges
                       if cur_node in x]
    if connected_edges:
        a, b = connected_edges[0]
        next_node = b if a == cur_node else a
        edges.remove(connected_edges[0])
        return [cur_node] + get_next_nodes(edges, next_node)
    return []

def take_tour(graph):
    start_node = graph[0][0]
    critical_edges = []
    nodes = []
    for edge in graph:
        if start_node in edge and len(critical_edges) < 2:
            critical_edges.append(edge)
        else:
            nodes.append(edge)

    second_node = critical_edges[0][1]
    stops = [start_node] + get_next_nodes(nodes, second_node)

    a, b = critical_edges[1]
    stops += [a] if b == start_node else [b]
    return stops + [start_node]

现在,您的算法还有更多问题

  • 您的脚本应该如何对非欧拉图做出反应?您的算法将输出各种各样的结果,从错误的节点列表到令人困惑的异常。如果要避免异常,则应引发适当的非混淆异常。用这些图试试你的算法:

    • 非欧拉:

      [(0, 1), (1, 5), (1, 7), (4, 5), (4, 8), (1, 6), (3, 7), (5, 9), ( 2, 4), (2, 5), (3, 6), (8, 9)]

    • 非欧拉:[(0, 1), (0, 3)]

    • 欧拉:[(0, 0)]
    • 非欧拉:[]
  • 您在列表中手动添加 3 个节点take_tour。有一个更优雅的解决方案,代码更少!

  • 你为什么选择一个结束边take_tour和起始边?你可能选错了!您看到选择多项选择中的第一个可能是错误的。试试这个图表:

    • 欧拉:[(0, 1), (0, 1), (0, 2), (0, 2)],正确的结果应该是[0, 1, 0, 2, 0]

最后,让遇见给你一个正确的答案

这个简单的递归代码将回答您尝试解决的初始问题。

请注意初始getCurPoint位置离做一些回溯不远。唯一的补充是引入了for循环,它将解析所有可能的解决方案,而不是盲目地采用第一个解决方案。

但要允许回溯,该函数必须能够退出当前路径计算。这是通过False在找不到路径的情况下允许返回值来完成的。False然后将从子函数调用到父函数重新执行,有效地解开递归,直到它可以尝试另一个边缘,这要归功于for循环。

您会注意到您可以合并getCurPointtake_tour。这增加了在 new 中隐藏 2 个功能的好处take_tour

  • 它将能够向您展示欧拉路径(这与必须在同一节点上开始和结束的欧拉之旅不同)。
  • 如果您足够好奇以强制另一个起始节点查看路径,您可以提供一个起始节点。

这是代码:

def take_tour(graph, node_start=None, cycle_only=True):
    if len(graph) == 0:
        if node_start is None:
            return []
        return [node_start]

    node_start = graph[0][0] if node_start is None else node_start

    for chosen_edge in [x for x in graph if node_start in x]:
        (node_a, node_b) = chosen_edge
        path = take_tour([e for e in graph if e != chosen_edge],
                         node_b if node_start == node_a else node_a,
                         cycle_only=False)
        if path is not False and (not cycle_only or node_start == path[-1]):
            return [node_start] + path
    return False

如果您正在寻找其他一些简单的练习,这里有 2 个简单到中等的问题,这些问题都悬而未决,可以一口气回答:

  • 您将如何修改take_tour以返回所有可能的游览/路径的列表?
  • 并且您能否take_tour在所有可能的游览/路径的迭代器中进行转换?(一个聪明的迭代器,只根据请求计算下一条路径)。

我希望这已经足够具有教育意义了。

于 2012-11-17T23:47:09.907 回答