123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- """
- Capacity scaling minimum cost flow algorithm.
- """
- __all__ = ["capacity_scaling"]
- from itertools import chain
- from math import log
- import networkx as nx
- from ...utils import BinaryHeap, arbitrary_element, not_implemented_for
- def _detect_unboundedness(R):
- """Detect infinite-capacity negative cycles."""
- G = nx.DiGraph()
- G.add_nodes_from(R)
- # Value simulating infinity.
- inf = R.graph["inf"]
- # True infinity.
- f_inf = float("inf")
- for u in R:
- for v, e in R[u].items():
- # Compute the minimum weight of infinite-capacity (u, v) edges.
- w = f_inf
- for k, e in e.items():
- if e["capacity"] == inf:
- w = min(w, e["weight"])
- if w != f_inf:
- G.add_edge(u, v, weight=w)
- if nx.negative_edge_cycle(G):
- raise nx.NetworkXUnbounded(
- "Negative cost cycle of infinite capacity found. "
- "Min cost flow may be unbounded below."
- )
- @not_implemented_for("undirected")
- def _build_residual_network(G, demand, capacity, weight):
- """Build a residual network and initialize a zero flow."""
- if sum(G.nodes[u].get(demand, 0) for u in G) != 0:
- raise nx.NetworkXUnfeasible("Sum of the demands should be 0.")
- R = nx.MultiDiGraph()
- R.add_nodes_from(
- (u, {"excess": -G.nodes[u].get(demand, 0), "potential": 0}) for u in G
- )
- inf = float("inf")
- # Detect selfloops with infinite capacities and negative weights.
- for u, v, e in nx.selfloop_edges(G, data=True):
- if e.get(weight, 0) < 0 and e.get(capacity, inf) == inf:
- raise nx.NetworkXUnbounded(
- "Negative cost cycle of infinite capacity found. "
- "Min cost flow may be unbounded below."
- )
- # Extract edges with positive capacities. Self loops excluded.
- if G.is_multigraph():
- edge_list = [
- (u, v, k, e)
- for u, v, k, e in G.edges(data=True, keys=True)
- if u != v and e.get(capacity, inf) > 0
- ]
- else:
- edge_list = [
- (u, v, 0, e)
- for u, v, e in G.edges(data=True)
- if u != v and e.get(capacity, inf) > 0
- ]
- # Simulate infinity with the larger of the sum of absolute node imbalances
- # the sum of finite edge capacities or any positive value if both sums are
- # zero. This allows the infinite-capacity edges to be distinguished for
- # unboundedness detection and directly participate in residual capacity
- # calculation.
- inf = (
- max(
- sum(abs(R.nodes[u]["excess"]) for u in R),
- 2
- * sum(
- e[capacity]
- for u, v, k, e in edge_list
- if capacity in e and e[capacity] != inf
- ),
- )
- or 1
- )
- for u, v, k, e in edge_list:
- r = min(e.get(capacity, inf), inf)
- w = e.get(weight, 0)
- # Add both (u, v) and (v, u) into the residual network marked with the
- # original key. (key[1] == True) indicates the (u, v) is in the
- # original network.
- R.add_edge(u, v, key=(k, True), capacity=r, weight=w, flow=0)
- R.add_edge(v, u, key=(k, False), capacity=0, weight=-w, flow=0)
- # Record the value simulating infinity.
- R.graph["inf"] = inf
- _detect_unboundedness(R)
- return R
- def _build_flow_dict(G, R, capacity, weight):
- """Build a flow dictionary from a residual network."""
- inf = float("inf")
- flow_dict = {}
- if G.is_multigraph():
- for u in G:
- flow_dict[u] = {}
- for v, es in G[u].items():
- flow_dict[u][v] = {
- # Always saturate negative selfloops.
- k: (
- 0
- if (
- u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0
- )
- else e[capacity]
- )
- for k, e in es.items()
- }
- for v, es in R[u].items():
- if v in flow_dict[u]:
- flow_dict[u][v].update(
- (k[0], e["flow"]) for k, e in es.items() if e["flow"] > 0
- )
- else:
- for u in G:
- flow_dict[u] = {
- # Always saturate negative selfloops.
- v: (
- 0
- if (u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0)
- else e[capacity]
- )
- for v, e in G[u].items()
- }
- flow_dict[u].update(
- (v, e["flow"])
- for v, es in R[u].items()
- for e in es.values()
- if e["flow"] > 0
- )
- return flow_dict
- def capacity_scaling(
- G, demand="demand", capacity="capacity", weight="weight", heap=BinaryHeap
- ):
- r"""Find a minimum cost flow satisfying all demands in digraph G.
- This is a capacity scaling successive shortest augmenting path algorithm.
- G is a digraph with edge costs and capacities and in which nodes
- have demand, i.e., they want to send or receive some amount of
- flow. A negative demand means that the node wants to send flow, a
- positive demand means that the node want to receive flow. A flow on
- the digraph G satisfies all demand if the net flow into each node
- is equal to the demand of that node.
- Parameters
- ----------
- G : NetworkX graph
- DiGraph or MultiDiGraph on which a minimum cost flow satisfying all
- demands is to be found.
- demand : string
- Nodes of the graph G are expected to have an attribute demand
- that indicates how much flow a node wants to send (negative
- demand) or receive (positive demand). Note that the sum of the
- demands should be 0 otherwise the problem in not feasible. If
- this attribute is not present, a node is considered to have 0
- demand. Default value: 'demand'.
- capacity : string
- Edges of the graph G are expected to have an attribute capacity
- that indicates how much flow the edge can support. If this
- attribute is not present, the edge is considered to have
- infinite capacity. Default value: 'capacity'.
- weight : string
- Edges of the graph G are expected to have an attribute weight
- that indicates the cost incurred by sending one unit of flow on
- that edge. If not present, the weight is considered to be 0.
- Default value: 'weight'.
- heap : class
- Type of heap to be used in the algorithm. It should be a subclass of
- :class:`MinHeap` or implement a compatible interface.
- If a stock heap implementation is to be used, :class:`BinaryHeap` is
- recommended over :class:`PairingHeap` for Python implementations without
- optimized attribute accesses (e.g., CPython) despite a slower
- asymptotic running time. For Python implementations with optimized
- attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better
- performance. Default value: :class:`BinaryHeap`.
- Returns
- -------
- flowCost : integer
- Cost of a minimum cost flow satisfying all demands.
- flowDict : dictionary
- If G is a digraph, a dict-of-dicts keyed by nodes such that
- flowDict[u][v] is the flow on edge (u, v).
- If G is a MultiDiGraph, a dict-of-dicts-of-dicts keyed by nodes
- so that flowDict[u][v][key] is the flow on edge (u, v, key).
- Raises
- ------
- NetworkXError
- This exception is raised if the input graph is not directed,
- not connected.
- NetworkXUnfeasible
- This exception is raised in the following situations:
- * The sum of the demands is not zero. Then, there is no
- flow satisfying all demands.
- * There is no flow satisfying all demand.
- NetworkXUnbounded
- This exception is raised if the digraph G has a cycle of
- negative cost and infinite capacity. Then, the cost of a flow
- satisfying all demands is unbounded below.
- Notes
- -----
- This algorithm does not work if edge weights are floating-point numbers.
- See also
- --------
- :meth:`network_simplex`
- Examples
- --------
- A simple example of a min cost flow problem.
- >>> G = nx.DiGraph()
- >>> G.add_node("a", demand=-5)
- >>> G.add_node("d", demand=5)
- >>> G.add_edge("a", "b", weight=3, capacity=4)
- >>> G.add_edge("a", "c", weight=6, capacity=10)
- >>> G.add_edge("b", "d", weight=1, capacity=9)
- >>> G.add_edge("c", "d", weight=2, capacity=5)
- >>> flowCost, flowDict = nx.capacity_scaling(G)
- >>> flowCost
- 24
- >>> flowDict
- {'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}}
- It is possible to change the name of the attributes used for the
- algorithm.
- >>> G = nx.DiGraph()
- >>> G.add_node("p", spam=-4)
- >>> G.add_node("q", spam=2)
- >>> G.add_node("a", spam=-2)
- >>> G.add_node("d", spam=-1)
- >>> G.add_node("t", spam=2)
- >>> G.add_node("w", spam=3)
- >>> G.add_edge("p", "q", cost=7, vacancies=5)
- >>> G.add_edge("p", "a", cost=1, vacancies=4)
- >>> G.add_edge("q", "d", cost=2, vacancies=3)
- >>> G.add_edge("t", "q", cost=1, vacancies=2)
- >>> G.add_edge("a", "t", cost=2, vacancies=4)
- >>> G.add_edge("d", "w", cost=3, vacancies=4)
- >>> G.add_edge("t", "w", cost=4, vacancies=1)
- >>> flowCost, flowDict = nx.capacity_scaling(
- ... G, demand="spam", capacity="vacancies", weight="cost"
- ... )
- >>> flowCost
- 37
- >>> flowDict
- {'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}}
- """
- R = _build_residual_network(G, demand, capacity, weight)
- inf = float("inf")
- # Account cost of negative selfloops.
- flow_cost = sum(
- 0
- if e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0
- else e[capacity] * e[weight]
- for u, v, e in nx.selfloop_edges(G, data=True)
- )
- # Determine the maximum edge capacity.
- wmax = max(chain([-inf], (e["capacity"] for u, v, e in R.edges(data=True))))
- if wmax == -inf:
- # Residual network has no edges.
- return flow_cost, _build_flow_dict(G, R, capacity, weight)
- R_nodes = R.nodes
- R_succ = R.succ
- delta = 2 ** int(log(wmax, 2))
- while delta >= 1:
- # Saturate Δ-residual edges with negative reduced costs to achieve
- # Δ-optimality.
- for u in R:
- p_u = R_nodes[u]["potential"]
- for v, es in R_succ[u].items():
- for k, e in es.items():
- flow = e["capacity"] - e["flow"]
- if e["weight"] - p_u + R_nodes[v]["potential"] < 0:
- flow = e["capacity"] - e["flow"]
- if flow >= delta:
- e["flow"] += flow
- R_succ[v][u][(k[0], not k[1])]["flow"] -= flow
- R_nodes[u]["excess"] -= flow
- R_nodes[v]["excess"] += flow
- # Determine the Δ-active nodes.
- S = set()
- T = set()
- S_add = S.add
- S_remove = S.remove
- T_add = T.add
- T_remove = T.remove
- for u in R:
- excess = R_nodes[u]["excess"]
- if excess >= delta:
- S_add(u)
- elif excess <= -delta:
- T_add(u)
- # Repeatedly augment flow from S to T along shortest paths until
- # Δ-feasibility is achieved.
- while S and T:
- s = arbitrary_element(S)
- t = None
- # Search for a shortest path in terms of reduce costs from s to
- # any t in T in the Δ-residual network.
- d = {}
- pred = {s: None}
- h = heap()
- h_insert = h.insert
- h_get = h.get
- h_insert(s, 0)
- while h:
- u, d_u = h.pop()
- d[u] = d_u
- if u in T:
- # Path found.
- t = u
- break
- p_u = R_nodes[u]["potential"]
- for v, es in R_succ[u].items():
- if v in d:
- continue
- wmin = inf
- # Find the minimum-weighted (u, v) Δ-residual edge.
- for k, e in es.items():
- if e["capacity"] - e["flow"] >= delta:
- w = e["weight"]
- if w < wmin:
- wmin = w
- kmin = k
- emin = e
- if wmin == inf:
- continue
- # Update the distance label of v.
- d_v = d_u + wmin - p_u + R_nodes[v]["potential"]
- if h_insert(v, d_v):
- pred[v] = (u, kmin, emin)
- if t is not None:
- # Augment Δ units of flow from s to t.
- while u != s:
- v = u
- u, k, e = pred[v]
- e["flow"] += delta
- R_succ[v][u][(k[0], not k[1])]["flow"] -= delta
- # Account node excess and deficit.
- R_nodes[s]["excess"] -= delta
- R_nodes[t]["excess"] += delta
- if R_nodes[s]["excess"] < delta:
- S_remove(s)
- if R_nodes[t]["excess"] > -delta:
- T_remove(t)
- # Update node potentials.
- d_t = d[t]
- for u, d_u in d.items():
- R_nodes[u]["potential"] -= d_u - d_t
- else:
- # Path not found.
- S_remove(s)
- delta //= 2
- if any(R.nodes[u]["excess"] != 0 for u in R):
- raise nx.NetworkXUnfeasible("No flow satisfying all demands.")
- # Calculate the flow cost.
- for u in R:
- for v, es in R_succ[u].items():
- for e in es.values():
- flow = e["flow"]
- if flow > 0:
- flow_cost += flow * e["weight"]
- return flow_cost, _build_flow_dict(G, R, capacity, weight)
|