stoerwagner.py 5.3 KB

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