【问题标题】:Minimum removed nodes required to cut path from A to B algorithm in Python在 Python 中切割从 A 到 B 算法的路径所需的最小删除节点
【发布时间】:2014-02-09 02:27:59
【问题描述】:

我正在尝试解决与图论相关的问题,但似乎无法记住/找到/理解正确/最佳的方法,所以我想我会问专家...

我有一个来自两个节点(示例代码中的 1 和 10)的路径列表。我试图找到要删除的最小节点数以切断所有路径。我也只能删除某些节点。

我目前已将其实现(如下)作为蛮力搜索。这在我的测试集上运行良好,但在扩展到具有 100K 中的路径和 100 中的可用节点的图形时将成为一个问题(因素问题)。现在,我并不关心删除节点的顺序,但我会在某些时候考虑到这一点(切换集合以在下面的代码中列出)。

我相信应该有一种方法可以使用最大流量/最小切割算法来解决这个问题。不过,我正在阅读的所有内容都以某种方式超出了我的想象。做这种事情已经好几年了,我似乎什么都不记得了。

所以我的问题是:

1) 除了测试所有组合并取最小集合之外,有没有更好的方法来解决这个问题?

2) 如果是这样,您能否解释一下,或者最好提供伪代码来帮助解释?我猜可能有一个库已经以某种方式做到了这一点(我最近一直在寻找和使用 networkX,但对其他人开放)

3) 如果不是(或什至如此),关于如何多线程/处理解决方案的建议?我想尝试从计算机上获得我所能获得的每一点性能。 (我在这个问题上找到了一些很好的线程,我只是没有机会实现,所以我想我会碰巧同时问。我首先想让一切正常工作,然后再进行优化。)

4) 关于使代码更“Pythonic”的一般建议(可能也有助于提高性能)。我知道我可以做出一些改进,而且对 Python 还是很陌生。

感谢您的帮助。

#!/usr/bin/env python


def bruteForcePaths(paths, availableNodes, setsTested, testCombination, results, loopId):

    #for each node available, we are going to
    # check if we have already tested set with node
    # if true- move to next node
    # if false- remove the paths effected,
    #           if there are paths left,
    #               record combo, continue removing with current combo,
    #           if there are no paths left,
    #               record success, record combo, continue to next node

    #local copy
    currentPaths = list(paths)
    currentAvailableNodes = list(availableNodes)
    currentSetsTested = set(setsTested)
    currentTestCombination= set(testCombination)

    currentLoopId = loopId+1

    print "loop ID: %d" %(currentLoopId)
    print "currentAvailableNodes:"
    for set1 in currentAvailableNodes:
        print "  %s" %(set1)

    for node in currentAvailableNodes:
        #add to the current test set
        print "%d-current node: %s current combo: %s" % (currentLoopId, node, currentTestCombination)
        currentTestCombination.add(node)
        # print "Testing: %s" % currentTestCombination
        # print "Sets tested:"
        # for set1 in currentSetsTested:
        #     print "  %s" % set1

        if currentTestCombination in currentSetsTested:
            #we already tested this combination of nodes so go to next node
            print "Already test: %s" % currentTestCombination
            currentTestCombination.remove(node)
            continue

        #get all the paths that don't have node in it
        currentRemainingPaths = [path for path in currentPaths if not (node in path)]

        #if there are no paths left
        if len(currentRemainingPaths) == 0:
            #save this combination
            print "successful combination: %s" % currentTestCombination
            results.append(frozenset(currentTestCombination))
            #add to remember we tested combo
            currentSetsTested.add(frozenset(currentTestCombination))
            #now remove the node that was add, and go to the next one
            currentTestCombination.remove(node)
        else:
            #this combo didn't work, save it so we don't test it again
            currentSetsTested.add(frozenset(currentTestCombination))
            newAvailableNodes = list(currentAvailableNodes)
            newAvailableNodes.remove(node)
            bruteForcePaths(currentRemainingPaths,
                            newAvailableNodes,
                            currentSetsTested,
                            currentTestCombination,
                            results,
                            currentLoopId)

            currentTestCombination.remove(node)

    print "-------------------"
    #need to pass "up" the tested sets from this loop
    setsTested.update(currentSetsTested)

    return None

if __name__ == '__main__':

    testPaths = [
        [1,2,14,15,16,18,9,10],
        [1,2,24,25,26,28,9,10],
        [1,2,34,35,36,38,9,10],
        [1,3,44,45,46,48,9,10],
        [1,3,54,55,56,58,9,10],
        [1,3,64,65,66,68,9,10],

        [1,2,14,15,16,7,10],
        [1,2,24,7,10],
        [1,3,34,35,7,10],

        [1,3,44,35,6,10],
        ]


    setsTested = set()
    availableNodes = [2, 3, 6, 7, 9]
    results = list()
    currentTestCombination = set()

    bruteForcePaths(testPaths, availableNodes, setsTested, currentTestCombination, results, 0)

    print "results:"
    for result in sorted(results, key=len):
        print result

更新: 我使用 itertool 重新编写了代码以生成组合。它使代码更清晰,更快(并且应该更容易进行多进程。现在尝试找出建议的主节点和多进程功能。

def bruteForcePaths3(paths, availableNodes, results):

    #start by taking each combination 2 at a time, then 3, etc
    for i in range(1,len(availableNodes)+1):
        print "combo number: %d" % i

        currentCombos = combinations(availableNodes, i)

        for combo in currentCombos:
            #get a fresh copy of paths for this combiniation
            currentPaths = list(paths)
            currentRemainingPaths = []
            # print combo

            for node in combo:
                #determine better way to remove nodes, for now- if it's in, we remove
                currentRemainingPaths = [path for path in currentPaths if not (node in path)]
                currentPaths = currentRemainingPaths

            #if there are no paths left
            if len(currentRemainingPaths) == 0:
                #save this combination
                print combo
                results.append(frozenset(combo))

    return None

【问题讨论】:

    标签: python algorithm graph-algorithm mathematical-optimization python-multithreading


    【解决方案1】:

    这是一个忽略路径列表的答案。它只需要一个网络、一个源节点和一个目标节点,并在网络中找到最小的节点集,而不是源节点或目标节点,这样删除这些节点就会断开源节点与目标节点的连接。

    如果我想找到最小的边集,我可以通过搜索 Max-Flow min-cut 来找出方法。请注意,http://en.wikipedia.org/wiki/Max-flow_min-cut_theorem#Generalized_max-flow_min-cut_theorem 上的 Wikipedia 文章指出,有一个广义的最大流最小割定理,它考虑了顶点容量和边容量,这至少是令人鼓舞的。另请注意,边缘容量以 Cuv 给出,其中 Cuv 是从 u 到 v 的最大容量。在图中,它们似乎被绘制为 u/v。所以前向的边缘容量可以不同于后向的边缘容量。

    为了将最小顶点切割问题伪装成最小边切割问题,我建议利用这种不对称性。首先,给所有现有的边一个巨大的容量——例如图中节点数量的 100 倍。现在将每个顶点 X 替换为两个顶点 Xi 和 Xo,我将它们称为传入和传出顶点。对于 X 和 Y 之间的每条边,在 Xo 和 Yi 之间创建一条边,现有容量向前但 0 容量向后 - 这些是单向边。现在为每个 X 在 Xi 和 Xo 之间创建一条边,容量 1 向前,容量 0 向后。

    现在在结果图上运行 max-flow min-cut。因为所有的原始链接都有巨大的容量,所以最小切割必须全部由容量为1的链接组成(实际上最小切割定义为将节点集一分为二:你真正想要的是一组对节点 Xi,Xo,一半是 Xi,另一半是 Xo,但你可以很容易地从另一半得到一个)。如果您断开这些链接,您会将图形分成两部分,就像标准的最大流最小切割一样,因此删除这些节点将断开源与目标的连接。因为你有最小割,所以这是最小的这样一组节点。

    如果你能找到 max-flow min-cut 的代码,例如 http://www.cs.sunysb.edu/~algorith/files/network-flow.shtml 所指的那些,我希望它会给你最小切割。如果不是,例如,如果您通过解决线性规划问题来解决这个问题,因为您碰巧有一个线性规划求解器,请注意例如 http://www.cse.yorku.ca/~aaw/Wang/MaxFlowMinCutAlg.html 的一半是从源可到达的节点集该图已被修改以减去解决方案实际使用的边缘容量 - 因此仅考虑在最大流量下使用的边缘容量,您可以很容易地找到它。

    【讨论】:

    • 感谢您的建议。在接下来的几个小时里,我会尽量完全消化。一般(愚蠢)问题 - 在我的图表是 Facebook 朋友的示例中,“容量”代表什么?认为这是我对 Max Flow 最小切割内容最难理解的部分。您是否偶然知道已经这样做的python库?我查看了networkX,但它返回了最小切割/最大流量的数字,而不是列表。这个数字代表什么?谢谢(如果这些问题真的很愚蠢,我很抱歉——我正在努力尽快跟上进度)。
    • 在我的回答中,容量是达到目的的一种手段,并设置为使答案有效所需的值。对于不同节点对之间的链接,它实际上是无限的。对于形成一对的两个节点之间的链接,它被设置为一个,这样最小割的值就是它所代表的节点删除的数量。我不熟悉 NetworkX,但查看它的文档,我可能会尝试使用 ford_fulkerson_flow_and_auxiliary,然后通过查看答案中描述的辅助网络中的源可访问哪些节点来获得最小切割。
    • 感谢您的帮助。它使我找到了这些功能(执行您所描述的操作):networkx.github.io/documentation/latest/reference/…
    【解决方案2】:

    如果没有提供路径作为问题的一部分,我同意应该有某种方法通过http://en.wikipedia.org/wiki/Max-flow_min-cut_theorem 来做到这一点,因为网络结构足够巧妙。但是,因为您没有给出任何关于什么是合理路径和什么不是合理路径的指示,所以我担心足够恶意的对手可能会找到不来自任何可能网络的奇怪路径集合。

    在最坏的情况下,这可能会使您的问题像http://en.wikipedia.org/wiki/Set_cover_problem 一样困难,因为有人在 Set Cover 中遇到问题,可能能够找到一组路径和节点来产生路径切割问题的解决方案可以转化为原始 Set Cover 问题的解决方案。

    如果是这样——我什至没有试图证明——你的问题是 NP-Complete,但由于你只有 100 个节点,你可以在 Set Cover 上找到的许多论文中的一些可能会指向一个可以在实践中使用的方法,或者可以为您提供足够好的近似值。除了 Wikipedia 文章之外,http://www.cs.sunysb.edu/~algorith/files/set-cover.shtml 还为您指出了两种实现方式,快速搜索可以在http://www.ise.ufl.edu/glan/files/2011/12/EJORpaper.pdf 的论文开头找到以下摘要:

    SCP 是一个严格意义上的 NP-hard 问题(Garey and Johnson, 1979)和许多算法 已被开发用于解决 SCP。精确算法(Fisher 和 Kedia,1990;Beasley 和 约恩斯滕,1992; Balas and Carrera, 1996) 主要基于分支定界和分支切割。 卡普拉拉等人。 (2000) 比较了 SCP 的不同精确算法。他们表明,最好的精确 SCP 的算法是 CPLEX。由于精确的方法需要大量的计算工作来解决 在大规模 SCP 实例中,启发式算法通常用于在 合理的时间。贪心算法可能是快速解决大型问题的最自然的启发式方法 组合问题。至于SCP,最简单的方法是Chvatal的贪心算法 (1979)。尽管简单、快速且易于编码,但贪心算法很少能产生好的解决方案 质量....

    【讨论】:

    • 我只添加了路径部分,因为我认为这有助于解决问题。我确实有一个完整的图表(类似于来自 Twitter 或 Facebook 的典型社交网络图表)。这会改变你的答案吗?
    • 它改变了我的答案,以至于我将其作为单独的答案提交。
    【解决方案3】:

    编辑:如果您实际上想要销毁所有路径,而不是给定列表中的路径,那么 mcdowella 解释的最大流技术比这种方法要好得多。

    正如 mcdowella 所提到的,这个问题通常是 NP 难的。但是,根据您的示例的外观,一种精确的方法可能是可行的。

    首先,您可以从无法删除的路径中删除所有顶点。然后,通过消除支配顶点来减少实例。例如,每条包含 15 的路径也包含 2,因此删除 15 永远没有意义。在示例中,如果所有顶点都可用,则 2、3、9 和 35 支配所有其他顶点,所以你会遇到问题减少到 4 个顶点。

    然后从最短路径中取出一个顶点并递归分支成两种情况:删除它(删除所有包含它的路径)或不删除它(从所有路径中删除它)。 (如果路径长度为 1,则忽略第二种情况。)然后您可以再次检查支配地位。

    这在最坏的情况下是指数级的,但对于您的示例来说可能就足够了。

    【讨论】:

    • 感谢您的建议。我会考虑进行这些更改。我只是将函数重新设计为不递归。它也使编码更清洁、更快。我会尽快发布新版本。
    猜你喜欢
    • 2017-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-15
    • 1970-01-01
    相关资源
    最近更新 更多