123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434 |
- """
- =================================
- 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")
- 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
- def traveling_salesman_problem(G, weight="weight", nodes=None, cycle=True, method=None):
- """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.
- 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:`threshold_accepting_tsp` for directed `G`.
- To specify parameters for these provided functions, construct lambda
- functions that state the specific value. `method` must have 2 inputs.
- (See examples).
- 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
- Build (curry) your own function to provide parameter values to the methods.
- >>> SA_tsp = nx.approximation.simulated_annealing_tsp
- >>> method = lambda G, wt: SA_tsp(G, "greedy", weight=wt, 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
- """
- 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)
- 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)
- 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 eularian_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)
- 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-Aborescence 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)
- # 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()
- # 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
- 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
- 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
- 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)
- 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 undirected 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)
- 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 undirected 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
|