stoerwagner.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. """
  2. Stoer-Wagner minimum cut algorithm.
  3. """
  4. from itertools import islice
  5. import networkx as nx
  6. from ...utils import BinaryHeap, arbitrary_element, not_implemented_for
  7. __all__ = ["stoer_wagner"]
  8. @not_implemented_for("directed")
  9. @not_implemented_for("multigraph")
  10. def stoer_wagner(G, weight="weight", heap=BinaryHeap):
  11. r"""Returns the weighted minimum edge cut using the Stoer-Wagner algorithm.
  12. Determine the minimum edge cut of a connected graph using the
  13. Stoer-Wagner algorithm. In weighted cases, all weights must be
  14. nonnegative.
  15. The running time of the algorithm depends on the type of heaps used:
  16. ============== =============================================
  17. Type of heap Running time
  18. ============== =============================================
  19. Binary heap $O(n (m + n) \log n)$
  20. Fibonacci heap $O(nm + n^2 \log n)$
  21. Pairing heap $O(2^{2 \sqrt{\log \log n}} nm + n^2 \log n)$
  22. ============== =============================================
  23. Parameters
  24. ----------
  25. G : NetworkX graph
  26. Edges of the graph are expected to have an attribute named by the
  27. weight parameter below. If this attribute is not present, the edge is
  28. considered to have unit weight.
  29. weight : string
  30. Name of the weight attribute of the edges. If the attribute is not
  31. present, unit weight is assumed. Default value: 'weight'.
  32. heap : class
  33. Type of heap to be used in the algorithm. It should be a subclass of
  34. :class:`MinHeap` or implement a compatible interface.
  35. If a stock heap implementation is to be used, :class:`BinaryHeap` is
  36. recommended over :class:`PairingHeap` for Python implementations without
  37. optimized attribute accesses (e.g., CPython) despite a slower
  38. asymptotic running time. For Python implementations with optimized
  39. attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better
  40. performance. Default value: :class:`BinaryHeap`.
  41. Returns
  42. -------
  43. cut_value : integer or float
  44. The sum of weights of edges in a minimum cut.
  45. partition : pair of node lists
  46. A partitioning of the nodes that defines a minimum cut.
  47. Raises
  48. ------
  49. NetworkXNotImplemented
  50. If the graph is directed or a multigraph.
  51. NetworkXError
  52. If the graph has less than two nodes, is not connected or has a
  53. negative-weighted edge.
  54. Examples
  55. --------
  56. >>> G = nx.Graph()
  57. >>> G.add_edge("x", "a", weight=3)
  58. >>> G.add_edge("x", "b", weight=1)
  59. >>> G.add_edge("a", "c", weight=3)
  60. >>> G.add_edge("b", "c", weight=5)
  61. >>> G.add_edge("b", "d", weight=4)
  62. >>> G.add_edge("d", "e", weight=2)
  63. >>> G.add_edge("c", "y", weight=2)
  64. >>> G.add_edge("e", "y", weight=3)
  65. >>> cut_value, partition = nx.stoer_wagner(G)
  66. >>> cut_value
  67. 4
  68. """
  69. n = len(G)
  70. if n < 2:
  71. raise nx.NetworkXError("graph has less than two nodes.")
  72. if not nx.is_connected(G):
  73. raise nx.NetworkXError("graph is not connected.")
  74. # Make a copy of the graph for internal use.
  75. G = nx.Graph(
  76. (u, v, {"weight": e.get(weight, 1)}) for u, v, e in G.edges(data=True) if u != v
  77. )
  78. for u, v, e in G.edges(data=True):
  79. if e["weight"] < 0:
  80. raise nx.NetworkXError("graph has a negative-weighted edge.")
  81. cut_value = float("inf")
  82. nodes = set(G)
  83. contractions = [] # contracted node pairs
  84. # Repeatedly pick a pair of nodes to contract until only one node is left.
  85. for i in range(n - 1):
  86. # Pick an arbitrary node u and create a set A = {u}.
  87. u = arbitrary_element(G)
  88. A = {u}
  89. # Repeatedly pick the node "most tightly connected" to A and add it to
  90. # A. The tightness of connectivity of a node not in A is defined by the
  91. # of edges connecting it to nodes in A.
  92. h = heap() # min-heap emulating a max-heap
  93. for v, e in G[u].items():
  94. h.insert(v, -e["weight"])
  95. # Repeat until all but one node has been added to A.
  96. for j in range(n - i - 2):
  97. u = h.pop()[0]
  98. A.add(u)
  99. for v, e in G[u].items():
  100. if v not in A:
  101. h.insert(v, h.get(v, 0) - e["weight"])
  102. # A and the remaining node v define a "cut of the phase". There is a
  103. # minimum cut of the original graph that is also a cut of the phase.
  104. # Due to contractions in earlier phases, v may in fact represent
  105. # multiple nodes in the original graph.
  106. v, w = h.min()
  107. w = -w
  108. if w < cut_value:
  109. cut_value = w
  110. best_phase = i
  111. # Contract v and the last node added to A.
  112. contractions.append((u, v))
  113. for w, e in G[v].items():
  114. if w != u:
  115. if w not in G[u]:
  116. G.add_edge(u, w, weight=e["weight"])
  117. else:
  118. G[u][w]["weight"] += e["weight"]
  119. G.remove_node(v)
  120. # Recover the optimal partitioning from the contractions.
  121. G = nx.Graph(islice(contractions, best_phase))
  122. v = contractions[best_phase][1]
  123. G.add_node(v)
  124. reachable = set(nx.single_source_shortest_path_length(G, v))
  125. partition = (list(reachable), list(nodes - reachable))
  126. return cut_value, partition