aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py1501
1 files changed, 1501 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py
new file mode 100644
index 00000000..2080c99a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/approximation/traveling_salesman.py
@@ -0,0 +1,1501 @@
+"""
+=================================
+Travelling Salesman Problem (TSP)
+=================================
+
+Implementation of approximate algorithms
+for solving and approximating the TSP problem.
+
+Categories of algorithms which are implemented:
+
+- Christofides (provides a 3/2-approximation of TSP)
+- Greedy
+- Simulated Annealing (SA)
+- Threshold Accepting (TA)
+- Asadpour Asymmetric Traveling Salesman Algorithm
+
+The Travelling Salesman Problem tries to find, given the weight
+(distance) between all points where a salesman has to visit, the
+route so that:
+
+- The total distance (cost) which the salesman travels is minimized.
+- The salesman returns to the starting point.
+- Note that for a complete graph, the salesman visits each point once.
+
+The function `travelling_salesman_problem` allows for incomplete
+graphs by finding all-pairs shortest paths, effectively converting
+the problem to a complete graph problem. It calls one of the
+approximate methods on that problem and then converts the result
+back to the original graph using the previously found shortest paths.
+
+TSP is an NP-hard problem in combinatorial optimization,
+important in operations research and theoretical computer science.
+
+http://en.wikipedia.org/wiki/Travelling_salesman_problem
+"""
+
+import math
+
+import networkx as nx
+from networkx.algorithms.tree.mst import random_spanning_tree
+from networkx.utils import not_implemented_for, pairwise, py_random_state
+
+__all__ = [
+ "traveling_salesman_problem",
+ "christofides",
+ "asadpour_atsp",
+ "greedy_tsp",
+ "simulated_annealing_tsp",
+ "threshold_accepting_tsp",
+]
+
+
+def swap_two_nodes(soln, seed):
+ """Swap two nodes in `soln` to give a neighbor solution.
+
+ Parameters
+ ----------
+ soln : list of nodes
+ Current cycle of nodes
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ list
+ The solution after move is applied. (A neighbor solution.)
+
+ Notes
+ -----
+ This function assumes that the incoming list `soln` is a cycle
+ (that the first and last element are the same) and also that
+ we don't want any move to change the first node in the list
+ (and thus not the last node either).
+
+ The input list is changed as well as returned. Make a copy if needed.
+
+ See Also
+ --------
+ move_one_node
+ """
+ a, b = seed.sample(range(1, len(soln) - 1), k=2)
+ soln[a], soln[b] = soln[b], soln[a]
+ return soln
+
+
+def move_one_node(soln, seed):
+ """Move one node to another position to give a neighbor solution.
+
+ The node to move and the position to move to are chosen randomly.
+ The first and last nodes are left untouched as soln must be a cycle
+ starting at that node.
+
+ Parameters
+ ----------
+ soln : list of nodes
+ Current cycle of nodes
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ list
+ The solution after move is applied. (A neighbor solution.)
+
+ Notes
+ -----
+ This function assumes that the incoming list `soln` is a cycle
+ (that the first and last element are the same) and also that
+ we don't want any move to change the first node in the list
+ (and thus not the last node either).
+
+ The input list is changed as well as returned. Make a copy if needed.
+
+ See Also
+ --------
+ swap_two_nodes
+ """
+ a, b = seed.sample(range(1, len(soln) - 1), k=2)
+ soln.insert(b, soln.pop(a))
+ return soln
+
+
+@not_implemented_for("directed")
+@nx._dispatchable(edge_attrs="weight")
+def christofides(G, weight="weight", tree=None):
+ """Approximate a solution of the traveling salesman problem
+
+ Compute a 3/2-approximation of the traveling salesman problem
+ in a complete undirected graph using Christofides [1]_ algorithm.
+
+ Parameters
+ ----------
+ G : Graph
+ `G` should be a complete weighted undirected graph.
+ The distance between all pairs of nodes should be included.
+
+ weight : string, optional (default="weight")
+ Edge data key corresponding to the edge weight.
+ If any edge does not have this attribute the weight is set to 1.
+
+ tree : NetworkX graph or None (default: None)
+ A minimum spanning tree of G. Or, if None, the minimum spanning
+ tree is computed using :func:`networkx.minimum_spanning_tree`
+
+ Returns
+ -------
+ list
+ List of nodes in `G` along a cycle with a 3/2-approximation of
+ the minimal Hamiltonian cycle.
+
+ References
+ ----------
+ .. [1] Christofides, Nicos. "Worst-case analysis of a new heuristic for
+ the travelling salesman problem." No. RR-388. Carnegie-Mellon Univ
+ Pittsburgh Pa Management Sciences Research Group, 1976.
+ """
+ # Remove selfloops if necessary
+ loop_nodes = nx.nodes_with_selfloops(G)
+ try:
+ node = next(loop_nodes)
+ except StopIteration:
+ pass
+ else:
+ G = G.copy()
+ G.remove_edge(node, node)
+ G.remove_edges_from((n, n) for n in loop_nodes)
+ # Check that G is a complete graph
+ N = len(G) - 1
+ # This check ignores selfloops which is what we want here.
+ if any(len(nbrdict) != N for n, nbrdict in G.adj.items()):
+ raise nx.NetworkXError("G must be a complete graph.")
+
+ if tree is None:
+ tree = nx.minimum_spanning_tree(G, weight=weight)
+ L = G.copy()
+ L.remove_nodes_from([v for v, degree in tree.degree if not (degree % 2)])
+ MG = nx.MultiGraph()
+ MG.add_edges_from(tree.edges)
+ edges = nx.min_weight_matching(L, weight=weight)
+ MG.add_edges_from(edges)
+ return _shortcutting(nx.eulerian_circuit(MG))
+
+
+def _shortcutting(circuit):
+ """Remove duplicate nodes in the path"""
+ nodes = []
+ for u, v in circuit:
+ if v in nodes:
+ continue
+ if not nodes:
+ nodes.append(u)
+ nodes.append(v)
+ nodes.append(nodes[0])
+ return nodes
+
+
+@nx._dispatchable(edge_attrs="weight")
+def traveling_salesman_problem(
+ G, weight="weight", nodes=None, cycle=True, method=None, **kwargs
+):
+ """Find the shortest path in `G` connecting specified nodes
+
+ This function allows approximate solution to the traveling salesman
+ problem on networks that are not complete graphs and/or where the
+ salesman does not need to visit all nodes.
+
+ This function proceeds in two steps. First, it creates a complete
+ graph using the all-pairs shortest_paths between nodes in `nodes`.
+ Edge weights in the new graph are the lengths of the paths
+ between each pair of nodes in the original graph.
+ Second, an algorithm (default: `christofides` for undirected and
+ `asadpour_atsp` for directed) is used to approximate the minimal Hamiltonian
+ cycle on this new graph. The available algorithms are:
+
+ - christofides
+ - greedy_tsp
+ - simulated_annealing_tsp
+ - threshold_accepting_tsp
+ - asadpour_atsp
+
+ Once the Hamiltonian Cycle is found, this function post-processes to
+ accommodate the structure of the original graph. If `cycle` is ``False``,
+ the biggest weight edge is removed to make a Hamiltonian path.
+ Then each edge on the new complete graph used for that analysis is
+ replaced by the shortest_path between those nodes on the original graph.
+ If the input graph `G` includes edges with weights that do not adhere to
+ the triangle inequality, such as when `G` is not a complete graph (i.e
+ length of non-existent edges is infinity), then the returned path may
+ contain some repeating nodes (other than the starting node).
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ A possibly weighted graph
+
+ nodes : collection of nodes (default=G.nodes)
+ collection (list, set, etc.) of nodes to visit
+
+ weight : string, optional (default="weight")
+ Edge data key corresponding to the edge weight.
+ If any edge does not have this attribute the weight is set to 1.
+
+ cycle : bool (default: True)
+ Indicates whether a cycle should be returned, or a path.
+ Note: the cycle is the approximate minimal cycle.
+ The path simply removes the biggest edge in that cycle.
+
+ method : function (default: None)
+ A function that returns a cycle on all nodes and approximates
+ the solution to the traveling salesman problem on a complete
+ graph. The returned cycle is then used to find a corresponding
+ solution on `G`. `method` should be callable; take inputs
+ `G`, and `weight`; and return a list of nodes along the cycle.
+
+ Provided options include :func:`christofides`, :func:`greedy_tsp`,
+ :func:`simulated_annealing_tsp` and :func:`threshold_accepting_tsp`.
+
+ If `method is None`: use :func:`christofides` for undirected `G` and
+ :func:`asadpour_atsp` for directed `G`.
+
+ **kwargs : dict
+ Other keyword arguments to be passed to the `method` function passed in.
+
+ Returns
+ -------
+ list
+ List of nodes in `G` along a path with an approximation of the minimal
+ path through `nodes`.
+
+ Raises
+ ------
+ NetworkXError
+ If `G` is a directed graph it has to be strongly connected or the
+ complete version cannot be generated.
+
+ Examples
+ --------
+ >>> tsp = nx.approximation.traveling_salesman_problem
+ >>> G = nx.cycle_graph(9)
+ >>> G[4][5]["weight"] = 5 # all other weights are 1
+ >>> tsp(G, nodes=[3, 6])
+ [3, 2, 1, 0, 8, 7, 6, 7, 8, 0, 1, 2, 3]
+ >>> path = tsp(G, cycle=False)
+ >>> path in ([4, 3, 2, 1, 0, 8, 7, 6, 5], [5, 6, 7, 8, 0, 1, 2, 3, 4])
+ True
+
+ While no longer required, you can still build (curry) your own function
+ to provide parameter values to the methods.
+
+ >>> SA_tsp = nx.approximation.simulated_annealing_tsp
+ >>> method = lambda G, weight: SA_tsp(G, "greedy", weight=weight, temp=500)
+ >>> path = tsp(G, cycle=False, method=method)
+ >>> path in ([4, 3, 2, 1, 0, 8, 7, 6, 5], [5, 6, 7, 8, 0, 1, 2, 3, 4])
+ True
+
+ Otherwise, pass other keyword arguments directly into the tsp function.
+
+ >>> path = tsp(
+ ... G,
+ ... cycle=False,
+ ... method=nx.approximation.simulated_annealing_tsp,
+ ... init_cycle="greedy",
+ ... temp=500,
+ ... )
+ >>> path in ([4, 3, 2, 1, 0, 8, 7, 6, 5], [5, 6, 7, 8, 0, 1, 2, 3, 4])
+ True
+ """
+ if method is None:
+ if G.is_directed():
+ method = asadpour_atsp
+ else:
+ method = christofides
+ if nodes is None:
+ nodes = list(G.nodes)
+
+ dist = {}
+ path = {}
+ for n, (d, p) in nx.all_pairs_dijkstra(G, weight=weight):
+ dist[n] = d
+ path[n] = p
+
+ if G.is_directed():
+ # If the graph is not strongly connected, raise an exception
+ if not nx.is_strongly_connected(G):
+ raise nx.NetworkXError("G is not strongly connected")
+ GG = nx.DiGraph()
+ else:
+ GG = nx.Graph()
+ for u in nodes:
+ for v in nodes:
+ if u == v:
+ continue
+ GG.add_edge(u, v, weight=dist[u][v])
+
+ best_GG = method(GG, weight=weight, **kwargs)
+
+ if not cycle:
+ # find and remove the biggest edge
+ (u, v) = max(pairwise(best_GG), key=lambda x: dist[x[0]][x[1]])
+ pos = best_GG.index(u) + 1
+ while best_GG[pos] != v:
+ pos = best_GG[pos:].index(u) + 1
+ best_GG = best_GG[pos:-1] + best_GG[:pos]
+
+ best_path = []
+ for u, v in pairwise(best_GG):
+ best_path.extend(path[u][v][:-1])
+ best_path.append(v)
+ return best_path
+
+
+@not_implemented_for("undirected")
+@py_random_state(2)
+@nx._dispatchable(edge_attrs="weight", mutates_input=True)
+def asadpour_atsp(G, weight="weight", seed=None, source=None):
+ """
+ Returns an approximate solution to the traveling salesman problem.
+
+ This approximate solution is one of the best known approximations for the
+ asymmetric traveling salesman problem developed by Asadpour et al,
+ [1]_. The algorithm first solves the Held-Karp relaxation to find a lower
+ bound for the weight of the cycle. Next, it constructs an exponential
+ distribution of undirected spanning trees where the probability of an
+ edge being in the tree corresponds to the weight of that edge using a
+ maximum entropy rounding scheme. Next we sample that distribution
+ $2 \\lceil \\ln n \\rceil$ times and save the minimum sampled tree once the
+ direction of the arcs is added back to the edges. Finally, we augment
+ then short circuit that graph to find the approximate tour for the
+ salesman.
+
+ Parameters
+ ----------
+ G : nx.DiGraph
+ The graph should be a complete weighted directed graph. The
+ distance between all paris of nodes should be included and the triangle
+ inequality should hold. That is, the direct edge between any two nodes
+ should be the path of least cost.
+
+ weight : string, optional (default="weight")
+ Edge data key corresponding to the edge weight.
+ If any edge does not have this attribute the weight is set to 1.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ source : node label (default=`None`)
+ If given, return the cycle starting and ending at the given node.
+
+ Returns
+ -------
+ cycle : list of nodes
+ Returns the cycle (list of nodes) that a salesman can follow to minimize
+ the total weight of the trip.
+
+ Raises
+ ------
+ NetworkXError
+ If `G` is not complete or has less than two nodes, the algorithm raises
+ an exception.
+
+ NetworkXError
+ If `source` is not `None` and is not a node in `G`, the algorithm raises
+ an exception.
+
+ NetworkXNotImplemented
+ If `G` is an undirected graph.
+
+ References
+ ----------
+ .. [1] A. Asadpour, M. X. Goemans, A. Madry, S. O. Gharan, and A. Saberi,
+ An o(log n/log log n)-approximation algorithm for the asymmetric
+ traveling salesman problem, Operations research, 65 (2017),
+ pp. 1043–1061
+
+ Examples
+ --------
+ >>> import networkx as nx
+ >>> import networkx.algorithms.approximation as approx
+ >>> G = nx.complete_graph(3, create_using=nx.DiGraph)
+ >>> nx.set_edge_attributes(
+ ... G,
+ ... {(0, 1): 2, (1, 2): 2, (2, 0): 2, (0, 2): 1, (2, 1): 1, (1, 0): 1},
+ ... "weight",
+ ... )
+ >>> tour = approx.asadpour_atsp(G, source=0)
+ >>> tour
+ [0, 2, 1, 0]
+ """
+ from math import ceil, exp
+ from math import log as ln
+
+ # Check that G is a complete graph
+ N = len(G) - 1
+ if N < 2:
+ raise nx.NetworkXError("G must have at least two nodes")
+ # This check ignores selfloops which is what we want here.
+ if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+ raise nx.NetworkXError("G is not a complete DiGraph")
+ # Check that the source vertex, if given, is in the graph
+ if source is not None and source not in G.nodes:
+ raise nx.NetworkXError("Given source node not in G.")
+
+ opt_hk, z_star = held_karp_ascent(G, weight)
+
+ # Test to see if the ascent method found an integer solution or a fractional
+ # solution. If it is integral then z_star is a nx.Graph, otherwise it is
+ # a dict
+ if not isinstance(z_star, dict):
+ # Here we are using the shortcutting method to go from the list of edges
+ # returned from eulerian_circuit to a list of nodes
+ return _shortcutting(nx.eulerian_circuit(z_star, source=source))
+
+ # Create the undirected support of z_star
+ z_support = nx.MultiGraph()
+ for u, v in z_star:
+ if (u, v) not in z_support.edges:
+ edge_weight = min(G[u][v][weight], G[v][u][weight])
+ z_support.add_edge(u, v, **{weight: edge_weight})
+
+ # Create the exponential distribution of spanning trees
+ gamma = spanning_tree_distribution(z_support, z_star)
+
+ # Write the lambda values to the edges of z_support
+ z_support = nx.Graph(z_support)
+ lambda_dict = {(u, v): exp(gamma[(u, v)]) for u, v in z_support.edges()}
+ nx.set_edge_attributes(z_support, lambda_dict, "weight")
+ del gamma, lambda_dict
+
+ # Sample 2 * ceil( ln(n) ) spanning trees and record the minimum one
+ minimum_sampled_tree = None
+ minimum_sampled_tree_weight = math.inf
+ for _ in range(2 * ceil(ln(G.number_of_nodes()))):
+ sampled_tree = random_spanning_tree(z_support, "weight", seed=seed)
+ sampled_tree_weight = sampled_tree.size(weight)
+ if sampled_tree_weight < minimum_sampled_tree_weight:
+ minimum_sampled_tree = sampled_tree.copy()
+ minimum_sampled_tree_weight = sampled_tree_weight
+
+ # Orient the edges in that tree to keep the cost of the tree the same.
+ t_star = nx.MultiDiGraph()
+ for u, v, d in minimum_sampled_tree.edges(data=weight):
+ if d == G[u][v][weight]:
+ t_star.add_edge(u, v, **{weight: d})
+ else:
+ t_star.add_edge(v, u, **{weight: d})
+
+ # Find the node demands needed to neutralize the flow of t_star in G
+ node_demands = {n: t_star.out_degree(n) - t_star.in_degree(n) for n in t_star}
+ nx.set_node_attributes(G, node_demands, "demand")
+
+ # Find the min_cost_flow
+ flow_dict = nx.min_cost_flow(G, "demand")
+
+ # Build the flow into t_star
+ for source, values in flow_dict.items():
+ for target in values:
+ if (source, target) not in t_star.edges and values[target] > 0:
+ # IF values[target] > 0 we have to add that many edges
+ for _ in range(values[target]):
+ t_star.add_edge(source, target)
+
+ # Return the shortcut eulerian circuit
+ circuit = nx.eulerian_circuit(t_star, source=source)
+ return _shortcutting(circuit)
+
+
+@nx._dispatchable(edge_attrs="weight", mutates_input=True, returns_graph=True)
+def held_karp_ascent(G, weight="weight"):
+ """
+ Minimizes the Held-Karp relaxation of the TSP for `G`
+
+ Solves the Held-Karp relaxation of the input complete digraph and scales
+ the output solution for use in the Asadpour [1]_ ASTP algorithm.
+
+ The Held-Karp relaxation defines the lower bound for solutions to the
+ ATSP, although it does return a fractional solution. This is used in the
+ Asadpour algorithm as an initial solution which is later rounded to a
+ integral tree within the spanning tree polytopes. This function solves
+ the relaxation with the branch and bound method in [2]_.
+
+ Parameters
+ ----------
+ G : nx.DiGraph
+ The graph should be a complete weighted directed graph.
+ The distance between all paris of nodes should be included.
+
+ weight : string, optional (default="weight")
+ Edge data key corresponding to the edge weight.
+ If any edge does not have this attribute the weight is set to 1.
+
+ Returns
+ -------
+ OPT : float
+ The cost for the optimal solution to the Held-Karp relaxation
+ z : dict or nx.Graph
+ A symmetrized and scaled version of the optimal solution to the
+ Held-Karp relaxation for use in the Asadpour algorithm.
+
+ If an integral solution is found, then that is an optimal solution for
+ the ATSP problem and that is returned instead.
+
+ References
+ ----------
+ .. [1] A. Asadpour, M. X. Goemans, A. Madry, S. O. Gharan, and A. Saberi,
+ An o(log n/log log n)-approximation algorithm for the asymmetric
+ traveling salesman problem, Operations research, 65 (2017),
+ pp. 1043–1061
+
+ .. [2] M. Held, R. M. Karp, The traveling-salesman problem and minimum
+ spanning trees, Operations Research, 1970-11-01, Vol. 18 (6),
+ pp.1138-1162
+ """
+ import numpy as np
+ from scipy import optimize
+
+ def k_pi():
+ """
+ Find the set of minimum 1-Arborescences for G at point pi.
+
+ Returns
+ -------
+ Set
+ The set of minimum 1-Arborescences
+ """
+ # Create a copy of G without vertex 1.
+ G_1 = G.copy()
+ minimum_1_arborescences = set()
+ minimum_1_arborescence_weight = math.inf
+
+ # node is node '1' in the Held and Karp paper
+ n = next(G.__iter__())
+ G_1.remove_node(n)
+
+ # Iterate over the spanning arborescences of the graph until we know
+ # that we have found the minimum 1-arborescences. My proposed strategy
+ # is to find the most extensive root to connect to from 'node 1' and
+ # the least expensive one. We then iterate over arborescences until
+ # the cost of the basic arborescence is the cost of the minimum one
+ # plus the difference between the most and least expensive roots,
+ # that way the cost of connecting 'node 1' will by definition not by
+ # minimum
+ min_root = {"node": None, weight: math.inf}
+ max_root = {"node": None, weight: -math.inf}
+ for u, v, d in G.edges(n, data=True):
+ if d[weight] < min_root[weight]:
+ min_root = {"node": v, weight: d[weight]}
+ if d[weight] > max_root[weight]:
+ max_root = {"node": v, weight: d[weight]}
+
+ min_in_edge = min(G.in_edges(n, data=True), key=lambda x: x[2][weight])
+ min_root[weight] = min_root[weight] + min_in_edge[2][weight]
+ max_root[weight] = max_root[weight] + min_in_edge[2][weight]
+
+ min_arb_weight = math.inf
+ for arb in nx.ArborescenceIterator(G_1):
+ arb_weight = arb.size(weight)
+ if min_arb_weight == math.inf:
+ min_arb_weight = arb_weight
+ elif arb_weight > min_arb_weight + max_root[weight] - min_root[weight]:
+ break
+ # We have to pick the root node of the arborescence for the out
+ # edge of the first vertex as that is the only node without an
+ # edge directed into it.
+ for N, deg in arb.in_degree:
+ if deg == 0:
+ # root found
+ arb.add_edge(n, N, **{weight: G[n][N][weight]})
+ arb_weight += G[n][N][weight]
+ break
+
+ # We can pick the minimum weight in-edge for the vertex with
+ # a cycle. If there are multiple edges with the same, minimum
+ # weight, We need to add all of them.
+ #
+ # Delete the edge (N, v) so that we cannot pick it.
+ edge_data = G[N][n]
+ G.remove_edge(N, n)
+ min_weight = min(G.in_edges(n, data=weight), key=lambda x: x[2])[2]
+ min_edges = [
+ (u, v, d) for u, v, d in G.in_edges(n, data=weight) if d == min_weight
+ ]
+ for u, v, d in min_edges:
+ new_arb = arb.copy()
+ new_arb.add_edge(u, v, **{weight: d})
+ new_arb_weight = arb_weight + d
+ # Check to see the weight of the arborescence, if it is a
+ # new minimum, clear all of the old potential minimum
+ # 1-arborescences and add this is the only one. If its
+ # weight is above the known minimum, do not add it.
+ if new_arb_weight < minimum_1_arborescence_weight:
+ minimum_1_arborescences.clear()
+ minimum_1_arborescence_weight = new_arb_weight
+ # We have a 1-arborescence, add it to the set
+ if new_arb_weight == minimum_1_arborescence_weight:
+ minimum_1_arborescences.add(new_arb)
+ G.add_edge(N, n, **edge_data)
+
+ return minimum_1_arborescences
+
+ def direction_of_ascent():
+ """
+ Find the direction of ascent at point pi.
+
+ See [1]_ for more information.
+
+ Returns
+ -------
+ dict
+ A mapping from the nodes of the graph which represents the direction
+ of ascent.
+
+ References
+ ----------
+ .. [1] M. Held, R. M. Karp, The traveling-salesman problem and minimum
+ spanning trees, Operations Research, 1970-11-01, Vol. 18 (6),
+ pp.1138-1162
+ """
+ # 1. Set d equal to the zero n-vector.
+ d = {}
+ for n in G:
+ d[n] = 0
+ del n
+ # 2. Find a 1-Arborescence T^k such that k is in K(pi, d).
+ minimum_1_arborescences = k_pi()
+ while True:
+ # Reduce K(pi) to K(pi, d)
+ # Find the arborescence in K(pi) which increases the lest in
+ # direction d
+ min_k_d_weight = math.inf
+ min_k_d = None
+ for arborescence in minimum_1_arborescences:
+ weighted_cost = 0
+ for n, deg in arborescence.degree:
+ weighted_cost += d[n] * (deg - 2)
+ if weighted_cost < min_k_d_weight:
+ min_k_d_weight = weighted_cost
+ min_k_d = arborescence
+
+ # 3. If sum of d_i * v_{i, k} is greater than zero, terminate
+ if min_k_d_weight > 0:
+ return d, min_k_d
+ # 4. d_i = d_i + v_{i, k}
+ for n, deg in min_k_d.degree:
+ d[n] += deg - 2
+ # Check that we do not need to terminate because the direction
+ # of ascent does not exist. This is done with linear
+ # programming.
+ c = np.full(len(minimum_1_arborescences), -1, dtype=int)
+ a_eq = np.empty((len(G) + 1, len(minimum_1_arborescences)), dtype=int)
+ b_eq = np.zeros(len(G) + 1, dtype=int)
+ b_eq[len(G)] = 1
+ for arb_count, arborescence in enumerate(minimum_1_arborescences):
+ n_count = len(G) - 1
+ for n, deg in arborescence.degree:
+ a_eq[n_count][arb_count] = deg - 2
+ n_count -= 1
+ a_eq[len(G)][arb_count] = 1
+ program_result = optimize.linprog(
+ c, A_eq=a_eq, b_eq=b_eq, method="highs-ipm"
+ )
+ # If the constants exist, then the direction of ascent doesn't
+ if program_result.success:
+ # There is no direction of ascent
+ return None, minimum_1_arborescences
+
+ # 5. GO TO 2
+
+ def find_epsilon(k, d):
+ """
+ Given the direction of ascent at pi, find the maximum distance we can go
+ in that direction.
+
+ Parameters
+ ----------
+ k_xy : set
+ The set of 1-arborescences which have the minimum rate of increase
+ in the direction of ascent
+
+ d : dict
+ The direction of ascent
+
+ Returns
+ -------
+ float
+ The distance we can travel in direction `d`
+ """
+ min_epsilon = math.inf
+ for e_u, e_v, e_w in G.edges(data=weight):
+ if (e_u, e_v) in k.edges:
+ continue
+ # Now, I have found a condition which MUST be true for the edges to
+ # be a valid substitute. The edge in the graph which is the
+ # substitute is the one with the same terminated end. This can be
+ # checked rather simply.
+ #
+ # Find the edge within k which is the substitute. Because k is a
+ # 1-arborescence, we know that they is only one such edges
+ # leading into every vertex.
+ if len(k.in_edges(e_v, data=weight)) > 1:
+ raise Exception
+ sub_u, sub_v, sub_w = next(k.in_edges(e_v, data=weight).__iter__())
+ k.add_edge(e_u, e_v, **{weight: e_w})
+ k.remove_edge(sub_u, sub_v)
+ if (
+ max(d for n, d in k.in_degree()) <= 1
+ and len(G) == k.number_of_edges()
+ and nx.is_weakly_connected(k)
+ ):
+ # Ascent method calculation
+ if d[sub_u] == d[e_u] or sub_w == e_w:
+ # Revert to the original graph
+ k.remove_edge(e_u, e_v)
+ k.add_edge(sub_u, sub_v, **{weight: sub_w})
+ continue
+ epsilon = (sub_w - e_w) / (d[e_u] - d[sub_u])
+ if 0 < epsilon < min_epsilon:
+ min_epsilon = epsilon
+ # Revert to the original graph
+ k.remove_edge(e_u, e_v)
+ k.add_edge(sub_u, sub_v, **{weight: sub_w})
+
+ return min_epsilon
+
+ # I have to know that the elements in pi correspond to the correct elements
+ # in the direction of ascent, even if the node labels are not integers.
+ # Thus, I will use dictionaries to made that mapping.
+ pi_dict = {}
+ for n in G:
+ pi_dict[n] = 0
+ del n
+ original_edge_weights = {}
+ for u, v, d in G.edges(data=True):
+ original_edge_weights[(u, v)] = d[weight]
+ dir_ascent, k_d = direction_of_ascent()
+ while dir_ascent is not None:
+ max_distance = find_epsilon(k_d, dir_ascent)
+ for n, v in dir_ascent.items():
+ pi_dict[n] += max_distance * v
+ for u, v, d in G.edges(data=True):
+ d[weight] = original_edge_weights[(u, v)] + pi_dict[u]
+ dir_ascent, k_d = direction_of_ascent()
+ nx._clear_cache(G)
+ # k_d is no longer an individual 1-arborescence but rather a set of
+ # minimal 1-arborescences at the maximum point of the polytope and should
+ # be reflected as such
+ k_max = k_d
+
+ # Search for a cycle within k_max. If a cycle exists, return it as the
+ # solution
+ for k in k_max:
+ if len([n for n in k if k.degree(n) == 2]) == G.order():
+ # Tour found
+ # TODO: this branch does not restore original_edge_weights of G!
+ return k.size(weight), k
+
+ # Write the original edge weights back to G and every member of k_max at
+ # the maximum point. Also average the number of times that edge appears in
+ # the set of minimal 1-arborescences.
+ x_star = {}
+ size_k_max = len(k_max)
+ for u, v, d in G.edges(data=True):
+ edge_count = 0
+ d[weight] = original_edge_weights[(u, v)]
+ for k in k_max:
+ if (u, v) in k.edges():
+ edge_count += 1
+ k[u][v][weight] = original_edge_weights[(u, v)]
+ x_star[(u, v)] = edge_count / size_k_max
+ # Now symmetrize the edges in x_star and scale them according to (5) in
+ # reference [1]
+ z_star = {}
+ scale_factor = (G.order() - 1) / G.order()
+ for u, v in x_star:
+ frequency = x_star[(u, v)] + x_star[(v, u)]
+ if frequency > 0:
+ z_star[(u, v)] = scale_factor * frequency
+ del x_star
+ # Return the optimal weight and the z dict
+ return next(k_max.__iter__()).size(weight), z_star
+
+
+@nx._dispatchable
+def spanning_tree_distribution(G, z):
+ """
+ Find the asadpour exponential distribution of spanning trees.
+
+ Solves the Maximum Entropy Convex Program in the Asadpour algorithm [1]_
+ using the approach in section 7 to build an exponential distribution of
+ undirected spanning trees.
+
+ This algorithm ensures that the probability of any edge in a spanning
+ tree is proportional to the sum of the probabilities of the tress
+ containing that edge over the sum of the probabilities of all spanning
+ trees of the graph.
+
+ Parameters
+ ----------
+ G : nx.MultiGraph
+ The undirected support graph for the Held Karp relaxation
+
+ z : dict
+ The output of `held_karp_ascent()`, a scaled version of the Held-Karp
+ solution.
+
+ Returns
+ -------
+ gamma : dict
+ The probability distribution which approximately preserves the marginal
+ probabilities of `z`.
+ """
+ from math import exp
+ from math import log as ln
+
+ def q(e):
+ """
+ The value of q(e) is described in the Asadpour paper is "the
+ probability that edge e will be included in a spanning tree T that is
+ chosen with probability proportional to exp(gamma(T))" which
+ basically means that it is the total probability of the edge appearing
+ across the whole distribution.
+
+ Parameters
+ ----------
+ e : tuple
+ The `(u, v)` tuple describing the edge we are interested in
+
+ Returns
+ -------
+ float
+ The probability that a spanning tree chosen according to the
+ current values of gamma will include edge `e`.
+ """
+ # Create the laplacian matrices
+ for u, v, d in G.edges(data=True):
+ d[lambda_key] = exp(gamma[(u, v)])
+ G_Kirchhoff = nx.total_spanning_tree_weight(G, lambda_key)
+ G_e = nx.contracted_edge(G, e, self_loops=False)
+ G_e_Kirchhoff = nx.total_spanning_tree_weight(G_e, lambda_key)
+
+ # Multiply by the weight of the contracted edge since it is not included
+ # in the total weight of the contracted graph.
+ return exp(gamma[(e[0], e[1])]) * G_e_Kirchhoff / G_Kirchhoff
+
+ # initialize gamma to the zero dict
+ gamma = {}
+ for u, v, _ in G.edges:
+ gamma[(u, v)] = 0
+
+ # set epsilon
+ EPSILON = 0.2
+
+ # pick an edge attribute name that is unlikely to be in the graph
+ lambda_key = "spanning_tree_distribution's secret attribute name for lambda"
+
+ while True:
+ # We need to know that know that no values of q_e are greater than
+ # (1 + epsilon) * z_e, however changing one gamma value can increase the
+ # value of a different q_e, so we have to complete the for loop without
+ # changing anything for the condition to be meet
+ in_range_count = 0
+ # Search for an edge with q_e > (1 + epsilon) * z_e
+ for u, v in gamma:
+ e = (u, v)
+ q_e = q(e)
+ z_e = z[e]
+ if q_e > (1 + EPSILON) * z_e:
+ delta = ln(
+ (q_e * (1 - (1 + EPSILON / 2) * z_e))
+ / ((1 - q_e) * (1 + EPSILON / 2) * z_e)
+ )
+ gamma[e] -= delta
+ # Check that delta had the desired effect
+ new_q_e = q(e)
+ desired_q_e = (1 + EPSILON / 2) * z_e
+ if round(new_q_e, 8) != round(desired_q_e, 8):
+ raise nx.NetworkXError(
+ f"Unable to modify probability for edge ({u}, {v})"
+ )
+ else:
+ in_range_count += 1
+ # Check if the for loop terminated without changing any gamma
+ if in_range_count == len(gamma):
+ break
+
+ # Remove the new edge attributes
+ for _, _, d in G.edges(data=True):
+ if lambda_key in d:
+ del d[lambda_key]
+
+ return gamma
+
+
+@nx._dispatchable(edge_attrs="weight")
+def greedy_tsp(G, weight="weight", source=None):
+ """Return a low cost cycle starting at `source` and its cost.
+
+ This approximates a solution to the traveling salesman problem.
+ It finds a cycle of all the nodes that a salesman can visit in order
+ to visit many nodes while minimizing total distance.
+ It uses a simple greedy algorithm.
+ In essence, this function returns a large cycle given a source point
+ for which the total cost of the cycle is minimized.
+
+ Parameters
+ ----------
+ G : Graph
+ The Graph should be a complete weighted undirected graph.
+ The distance between all pairs of nodes should be included.
+
+ weight : string, optional (default="weight")
+ Edge data key corresponding to the edge weight.
+ If any edge does not have this attribute the weight is set to 1.
+
+ source : node, optional (default: first node in list(G))
+ Starting node. If None, defaults to ``next(iter(G))``
+
+ Returns
+ -------
+ cycle : list of nodes
+ Returns the cycle (list of nodes) that a salesman
+ can follow to minimize total weight of the trip.
+
+ Raises
+ ------
+ NetworkXError
+ If `G` is not complete, the algorithm raises an exception.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import approximation as approx
+ >>> G = nx.DiGraph()
+ >>> G.add_weighted_edges_from(
+ ... {
+ ... ("A", "B", 3),
+ ... ("A", "C", 17),
+ ... ("A", "D", 14),
+ ... ("B", "A", 3),
+ ... ("B", "C", 12),
+ ... ("B", "D", 16),
+ ... ("C", "A", 13),
+ ... ("C", "B", 12),
+ ... ("C", "D", 4),
+ ... ("D", "A", 14),
+ ... ("D", "B", 15),
+ ... ("D", "C", 2),
+ ... }
+ ... )
+ >>> cycle = approx.greedy_tsp(G, source="D")
+ >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+ >>> cycle
+ ['D', 'C', 'B', 'A', 'D']
+ >>> cost
+ 31
+
+ Notes
+ -----
+ This implementation of a greedy algorithm is based on the following:
+
+ - The algorithm adds a node to the solution at every iteration.
+ - The algorithm selects a node not already in the cycle whose connection
+ to the previous node adds the least cost to the cycle.
+
+ A greedy algorithm does not always give the best solution.
+ However, it can construct a first feasible solution which can
+ be passed as a parameter to an iterative improvement algorithm such
+ as Simulated Annealing, or Threshold Accepting.
+
+ Time complexity: It has a running time $O(|V|^2)$
+ """
+ # Check that G is a complete graph
+ N = len(G) - 1
+ # This check ignores selfloops which is what we want here.
+ if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+ raise nx.NetworkXError("G must be a complete graph.")
+
+ if source is None:
+ source = nx.utils.arbitrary_element(G)
+
+ if G.number_of_nodes() == 2:
+ neighbor = next(G.neighbors(source))
+ return [source, neighbor, source]
+
+ nodeset = set(G)
+ nodeset.remove(source)
+ cycle = [source]
+ next_node = source
+ while nodeset:
+ nbrdict = G[next_node]
+ next_node = min(nodeset, key=lambda n: nbrdict[n].get(weight, 1))
+ cycle.append(next_node)
+ nodeset.remove(next_node)
+ cycle.append(cycle[0])
+ return cycle
+
+
+@py_random_state(9)
+@nx._dispatchable(edge_attrs="weight")
+def simulated_annealing_tsp(
+ G,
+ init_cycle,
+ weight="weight",
+ source=None,
+ temp=100,
+ move="1-1",
+ max_iterations=10,
+ N_inner=100,
+ alpha=0.01,
+ seed=None,
+):
+ """Returns an approximate solution to the traveling salesman problem.
+
+ This function uses simulated annealing to approximate the minimal cost
+ cycle through the nodes. Starting from a suboptimal solution, simulated
+ annealing perturbs that solution, occasionally accepting changes that make
+ the solution worse to escape from a locally optimal solution. The chance
+ of accepting such changes decreases over the iterations to encourage
+ an optimal result. In summary, the function returns a cycle starting
+ at `source` for which the total cost is minimized. It also returns the cost.
+
+ The chance of accepting a proposed change is related to a parameter called
+ the temperature (annealing has a physical analogue of steel hardening
+ as it cools). As the temperature is reduced, the chance of moves that
+ increase cost goes down.
+
+ Parameters
+ ----------
+ G : Graph
+ `G` should be a complete weighted graph.
+ The distance between all pairs of nodes should be included.
+
+ init_cycle : list of all nodes or "greedy"
+ The initial solution (a cycle through all nodes returning to the start).
+ This argument has no default to make you think about it.
+ If "greedy", use `greedy_tsp(G, weight)`.
+ Other common starting cycles are `list(G) + [next(iter(G))]` or the final
+ result of `simulated_annealing_tsp` when doing `threshold_accepting_tsp`.
+
+ weight : string, optional (default="weight")
+ Edge data key corresponding to the edge weight.
+ If any edge does not have this attribute the weight is set to 1.
+
+ source : node, optional (default: first node in list(G))
+ Starting node. If None, defaults to ``next(iter(G))``
+
+ temp : int, optional (default=100)
+ The algorithm's temperature parameter. It represents the initial
+ value of temperature
+
+ move : "1-1" or "1-0" or function, optional (default="1-1")
+ Indicator of what move to use when finding new trial solutions.
+ Strings indicate two special built-in moves:
+
+ - "1-1": 1-1 exchange which transposes the position
+ of two elements of the current solution.
+ The function called is :func:`swap_two_nodes`.
+ For example if we apply 1-1 exchange in the solution
+ ``A = [3, 2, 1, 4, 3]``
+ we can get the following by the transposition of 1 and 4 elements:
+ ``A' = [3, 2, 4, 1, 3]``
+ - "1-0": 1-0 exchange which moves an node in the solution
+ to a new position.
+ The function called is :func:`move_one_node`.
+ For example if we apply 1-0 exchange in the solution
+ ``A = [3, 2, 1, 4, 3]``
+ we can transfer the fourth element to the second position:
+ ``A' = [3, 4, 2, 1, 3]``
+
+ You may provide your own functions to enact a move from
+ one solution to a neighbor solution. The function must take
+ the solution as input along with a `seed` input to control
+ random number generation (see the `seed` input here).
+ Your function should maintain the solution as a cycle with
+ equal first and last node and all others appearing once.
+ Your function should return the new solution.
+
+ max_iterations : int, optional (default=10)
+ Declared done when this number of consecutive iterations of
+ the outer loop occurs without any change in the best cost solution.
+
+ N_inner : int, optional (default=100)
+ The number of iterations of the inner loop.
+
+ alpha : float between (0, 1), optional (default=0.01)
+ Percentage of temperature decrease in each iteration
+ of outer loop
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ cycle : list of nodes
+ Returns the cycle (list of nodes) that a salesman
+ can follow to minimize total weight of the trip.
+
+ Raises
+ ------
+ NetworkXError
+ If `G` is not complete the algorithm raises an exception.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import approximation as approx
+ >>> G = nx.DiGraph()
+ >>> G.add_weighted_edges_from(
+ ... {
+ ... ("A", "B", 3),
+ ... ("A", "C", 17),
+ ... ("A", "D", 14),
+ ... ("B", "A", 3),
+ ... ("B", "C", 12),
+ ... ("B", "D", 16),
+ ... ("C", "A", 13),
+ ... ("C", "B", 12),
+ ... ("C", "D", 4),
+ ... ("D", "A", 14),
+ ... ("D", "B", 15),
+ ... ("D", "C", 2),
+ ... }
+ ... )
+ >>> cycle = approx.simulated_annealing_tsp(G, "greedy", source="D")
+ >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+ >>> cycle
+ ['D', 'C', 'B', 'A', 'D']
+ >>> cost
+ 31
+ >>> incycle = ["D", "B", "A", "C", "D"]
+ >>> cycle = approx.simulated_annealing_tsp(G, incycle, source="D")
+ >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+ >>> cycle
+ ['D', 'C', 'B', 'A', 'D']
+ >>> cost
+ 31
+
+ Notes
+ -----
+ Simulated Annealing is a metaheuristic local search algorithm.
+ The main characteristic of this algorithm is that it accepts
+ even solutions which lead to the increase of the cost in order
+ to escape from low quality local optimal solutions.
+
+ This algorithm needs an initial solution. If not provided, it is
+ constructed by a simple greedy algorithm. At every iteration, the
+ algorithm selects thoughtfully a neighbor solution.
+ Consider $c(x)$ cost of current solution and $c(x')$ cost of a
+ neighbor solution.
+ If $c(x') - c(x) <= 0$ then the neighbor solution becomes the current
+ solution for the next iteration. Otherwise, the algorithm accepts
+ the neighbor solution with probability $p = exp - ([c(x') - c(x)] / temp)$.
+ Otherwise the current solution is retained.
+
+ `temp` is a parameter of the algorithm and represents temperature.
+
+ Time complexity:
+ For $N_i$ iterations of the inner loop and $N_o$ iterations of the
+ outer loop, this algorithm has running time $O(N_i * N_o * |V|)$.
+
+ For more information and how the algorithm is inspired see:
+ http://en.wikipedia.org/wiki/Simulated_annealing
+ """
+ if move == "1-1":
+ move = swap_two_nodes
+ elif move == "1-0":
+ move = move_one_node
+ if init_cycle == "greedy":
+ # Construct an initial solution using a greedy algorithm.
+ cycle = greedy_tsp(G, weight=weight, source=source)
+ if G.number_of_nodes() == 2:
+ return cycle
+
+ else:
+ cycle = list(init_cycle)
+ if source is None:
+ source = cycle[0]
+ elif source != cycle[0]:
+ raise nx.NetworkXError("source must be first node in init_cycle")
+ if cycle[0] != cycle[-1]:
+ raise nx.NetworkXError("init_cycle must be a cycle. (return to start)")
+
+ if len(cycle) - 1 != len(G) or len(set(G.nbunch_iter(cycle))) != len(G):
+ raise nx.NetworkXError("init_cycle should be a cycle over all nodes in G.")
+
+ # Check that G is a complete graph
+ N = len(G) - 1
+ # This check ignores selfloops which is what we want here.
+ if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+ raise nx.NetworkXError("G must be a complete graph.")
+
+ if G.number_of_nodes() == 2:
+ neighbor = next(G.neighbors(source))
+ return [source, neighbor, source]
+
+ # Find the cost of initial solution
+ cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(cycle))
+
+ count = 0
+ best_cycle = cycle.copy()
+ best_cost = cost
+ while count <= max_iterations and temp > 0:
+ count += 1
+ for i in range(N_inner):
+ adj_sol = move(cycle, seed)
+ adj_cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(adj_sol))
+ delta = adj_cost - cost
+ if delta <= 0:
+ # Set current solution the adjacent solution.
+ cycle = adj_sol
+ cost = adj_cost
+
+ if cost < best_cost:
+ count = 0
+ best_cycle = cycle.copy()
+ best_cost = cost
+ else:
+ # Accept even a worse solution with probability p.
+ p = math.exp(-delta / temp)
+ if p >= seed.random():
+ cycle = adj_sol
+ cost = adj_cost
+ temp -= temp * alpha
+
+ return best_cycle
+
+
+@py_random_state(9)
+@nx._dispatchable(edge_attrs="weight")
+def threshold_accepting_tsp(
+ G,
+ init_cycle,
+ weight="weight",
+ source=None,
+ threshold=1,
+ move="1-1",
+ max_iterations=10,
+ N_inner=100,
+ alpha=0.1,
+ seed=None,
+):
+ """Returns an approximate solution to the traveling salesman problem.
+
+ This function uses threshold accepting methods to approximate the minimal cost
+ cycle through the nodes. Starting from a suboptimal solution, threshold
+ accepting methods perturb that solution, accepting any changes that make
+ the solution no worse than increasing by a threshold amount. Improvements
+ in cost are accepted, but so are changes leading to small increases in cost.
+ This allows the solution to leave suboptimal local minima in solution space.
+ The threshold is decreased slowly as iterations proceed helping to ensure
+ an optimum. In summary, the function returns a cycle starting at `source`
+ for which the total cost is minimized.
+
+ Parameters
+ ----------
+ G : Graph
+ `G` should be a complete weighted graph.
+ The distance between all pairs of nodes should be included.
+
+ init_cycle : list or "greedy"
+ The initial solution (a cycle through all nodes returning to the start).
+ This argument has no default to make you think about it.
+ If "greedy", use `greedy_tsp(G, weight)`.
+ Other common starting cycles are `list(G) + [next(iter(G))]` or the final
+ result of `simulated_annealing_tsp` when doing `threshold_accepting_tsp`.
+
+ weight : string, optional (default="weight")
+ Edge data key corresponding to the edge weight.
+ If any edge does not have this attribute the weight is set to 1.
+
+ source : node, optional (default: first node in list(G))
+ Starting node. If None, defaults to ``next(iter(G))``
+
+ threshold : int, optional (default=1)
+ The algorithm's threshold parameter. It represents the initial
+ threshold's value
+
+ move : "1-1" or "1-0" or function, optional (default="1-1")
+ Indicator of what move to use when finding new trial solutions.
+ Strings indicate two special built-in moves:
+
+ - "1-1": 1-1 exchange which transposes the position
+ of two elements of the current solution.
+ The function called is :func:`swap_two_nodes`.
+ For example if we apply 1-1 exchange in the solution
+ ``A = [3, 2, 1, 4, 3]``
+ we can get the following by the transposition of 1 and 4 elements:
+ ``A' = [3, 2, 4, 1, 3]``
+ - "1-0": 1-0 exchange which moves an node in the solution
+ to a new position.
+ The function called is :func:`move_one_node`.
+ For example if we apply 1-0 exchange in the solution
+ ``A = [3, 2, 1, 4, 3]``
+ we can transfer the fourth element to the second position:
+ ``A' = [3, 4, 2, 1, 3]``
+
+ You may provide your own functions to enact a move from
+ one solution to a neighbor solution. The function must take
+ the solution as input along with a `seed` input to control
+ random number generation (see the `seed` input here).
+ Your function should maintain the solution as a cycle with
+ equal first and last node and all others appearing once.
+ Your function should return the new solution.
+
+ max_iterations : int, optional (default=10)
+ Declared done when this number of consecutive iterations of
+ the outer loop occurs without any change in the best cost solution.
+
+ N_inner : int, optional (default=100)
+ The number of iterations of the inner loop.
+
+ alpha : float between (0, 1), optional (default=0.1)
+ Percentage of threshold decrease when there is at
+ least one acceptance of a neighbor solution.
+ If no inner loop moves are accepted the threshold remains unchanged.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ cycle : list of nodes
+ Returns the cycle (list of nodes) that a salesman
+ can follow to minimize total weight of the trip.
+
+ Raises
+ ------
+ NetworkXError
+ If `G` is not complete the algorithm raises an exception.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import approximation as approx
+ >>> G = nx.DiGraph()
+ >>> G.add_weighted_edges_from(
+ ... {
+ ... ("A", "B", 3),
+ ... ("A", "C", 17),
+ ... ("A", "D", 14),
+ ... ("B", "A", 3),
+ ... ("B", "C", 12),
+ ... ("B", "D", 16),
+ ... ("C", "A", 13),
+ ... ("C", "B", 12),
+ ... ("C", "D", 4),
+ ... ("D", "A", 14),
+ ... ("D", "B", 15),
+ ... ("D", "C", 2),
+ ... }
+ ... )
+ >>> cycle = approx.threshold_accepting_tsp(G, "greedy", source="D")
+ >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+ >>> cycle
+ ['D', 'C', 'B', 'A', 'D']
+ >>> cost
+ 31
+ >>> incycle = ["D", "B", "A", "C", "D"]
+ >>> cycle = approx.threshold_accepting_tsp(G, incycle, source="D")
+ >>> cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle))
+ >>> cycle
+ ['D', 'C', 'B', 'A', 'D']
+ >>> cost
+ 31
+
+ Notes
+ -----
+ Threshold Accepting is a metaheuristic local search algorithm.
+ The main characteristic of this algorithm is that it accepts
+ even solutions which lead to the increase of the cost in order
+ to escape from low quality local optimal solutions.
+
+ This algorithm needs an initial solution. This solution can be
+ constructed by a simple greedy algorithm. At every iteration, it
+ selects thoughtfully a neighbor solution.
+ Consider $c(x)$ cost of current solution and $c(x')$ cost of
+ neighbor solution.
+ If $c(x') - c(x) <= threshold$ then the neighbor solution becomes the current
+ solution for the next iteration, where the threshold is named threshold.
+
+ In comparison to the Simulated Annealing algorithm, the Threshold
+ Accepting algorithm does not accept very low quality solutions
+ (due to the presence of the threshold value). In the case of
+ Simulated Annealing, even a very low quality solution can
+ be accepted with probability $p$.
+
+ Time complexity:
+ It has a running time $O(m * n * |V|)$ where $m$ and $n$ are the number
+ of times the outer and inner loop run respectively.
+
+ For more information and how algorithm is inspired see:
+ https://doi.org/10.1016/0021-9991(90)90201-B
+
+ See Also
+ --------
+ simulated_annealing_tsp
+
+ """
+ if move == "1-1":
+ move = swap_two_nodes
+ elif move == "1-0":
+ move = move_one_node
+ if init_cycle == "greedy":
+ # Construct an initial solution using a greedy algorithm.
+ cycle = greedy_tsp(G, weight=weight, source=source)
+ if G.number_of_nodes() == 2:
+ return cycle
+
+ else:
+ cycle = list(init_cycle)
+ if source is None:
+ source = cycle[0]
+ elif source != cycle[0]:
+ raise nx.NetworkXError("source must be first node in init_cycle")
+ if cycle[0] != cycle[-1]:
+ raise nx.NetworkXError("init_cycle must be a cycle. (return to start)")
+
+ if len(cycle) - 1 != len(G) or len(set(G.nbunch_iter(cycle))) != len(G):
+ raise nx.NetworkXError("init_cycle is not all and only nodes.")
+
+ # Check that G is a complete graph
+ N = len(G) - 1
+ # This check ignores selfloops which is what we want here.
+ if any(len(nbrdict) - (n in nbrdict) != N for n, nbrdict in G.adj.items()):
+ raise nx.NetworkXError("G must be a complete graph.")
+
+ if G.number_of_nodes() == 2:
+ neighbor = list(G.neighbors(source))[0]
+ return [source, neighbor, source]
+
+ # Find the cost of initial solution
+ cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(cycle))
+
+ count = 0
+ best_cycle = cycle.copy()
+ best_cost = cost
+ while count <= max_iterations:
+ count += 1
+ accepted = False
+ for i in range(N_inner):
+ adj_sol = move(cycle, seed)
+ adj_cost = sum(G[u][v].get(weight, 1) for u, v in pairwise(adj_sol))
+ delta = adj_cost - cost
+ if delta <= threshold:
+ accepted = True
+
+ # Set current solution the adjacent solution.
+ cycle = adj_sol
+ cost = adj_cost
+
+ if cost < best_cost:
+ count = 0
+ best_cycle = cycle.copy()
+ best_cost = cost
+ if accepted:
+ threshold -= threshold * alpha
+
+ return best_cycle