def hamiltonian(edges, directed=False, constraint_generation=True): """ Calculates shortest path that traverses each node exactly once. Convert Hamiltonian path problem to TSP by adding one dummy point that has a distance of zero to all your other points. Solve the TSP and get rid of the dummy point - what remains is the Hamiltonian Path. >>> g = [(1,2), (2,3), (3,4), (4,2), (3,5)] >>> hamiltonian(g) [1, 2, 4, 3, 5] >>> g = [(1,2), (2,3), (1,4), (2,5), (3,6)] >>> hamiltonian(g) """ edges = populate_edge_weights(edges) incident, nodes = node_to_edge(edges, directed=False) if not directed: # Make graph symmetric dual_edges = edges[:] for a, b, w in edges: dual_edges.append((b, a, w)) edges = dual_edges DUMMY = "DUMMY" dummy_edges = edges + [(DUMMY, x, 0) for x in nodes] + \ [(x, DUMMY, 0) for x in nodes] #results = tsp(dummy_edges, constraint_generation=constraint_generation) results = tsp_gurobi(dummy_edges) if results: results = [x for x in results if DUMMY not in x] results = edges_to_path(results) if not directed: results = min(results, results[::-1]) return results
def path(edges, source, sink, flavor="longest"): """ Calculates shortest/longest path from list of edges in a graph >>> g = [(1,2,1),(2,3,9),(2,4,3),(2,5,2),(3,6,8),(4,6,10),(4,7,4)] >>> g += [(6,8,7),(7,9,5),(8,9,6),(9,10,11)] >>> path(g, 1, 8, flavor="shortest") ([1, 2, 4, 6, 8], 21) >>> path(g, 1, 8, flavor="longest") ([1, 2, 3, 6, 8], 25) """ outgoing, incoming, nodes = node_to_edge(edges) nedges = len(edges) L = LPInstance() assert flavor in ("longest", "shortest") objective = MAXIMIZE if flavor == "longest" else MINIMIZE L.add_objective(edges, objective=objective) # Balancing constraint, incoming edges equal to outgoing edges except # source and sink constraints = [] for v in nodes: incoming_edges = incoming[v] outgoing_edges = outgoing[v] icc = summation(incoming_edges) occ = summation(outgoing_edges) if v == source: if not outgoing_edges: return None constraints.append("{0} = 1".format(occ)) elif v == sink: if not incoming_edges: return None constraints.append("{0} = 1".format(icc)) else: # Balancing constraints.append("{0}{1} = 0".format(icc, occ.replace("+", "-"))) # Simple path if incoming_edges: constraints.append("{0} <= 1".format(icc)) if outgoing_edges: constraints.append("{0} <= 1".format(occ)) L.constraints = constraints L.add_vars(nedges) selected, obj_val = L.lpsolve() results = ( sorted(x for i, x in enumerate(edges) if i in selected) if selected else None ) results = edges_to_path(results) return results, obj_val
def path(edges, source, sink, flavor="longest"): """ Calculates shortest/longest path from list of edges in a graph >>> g = [(1,2,1),(2,3,9),(2,4,3),(2,5,2),(3,6,8),(4,6,10),(4,7,4)] >>> g += [(6,8,7),(7,9,5),(8,9,6),(9,10,11)] >>> path(g, 1, 8, flavor="shortest") ([1, 2, 4, 6, 8], 21) >>> path(g, 1, 8, flavor="longest") ([1, 2, 3, 6, 8], 25) """ outgoing, incoming, nodes = node_to_edge(edges) nedges = len(edges) L = LPInstance() assert flavor in ("longest", "shortest") objective = MAXIMIZE if flavor == "longest" else MINIMIZE L.add_objective(edges, objective=objective) # Balancing constraint, incoming edges equal to outgoing edges except # source and sink constraints = [] for v in nodes: incoming_edges = incoming[v] outgoing_edges = outgoing[v] icc = summation(incoming_edges) occ = summation(outgoing_edges) if v == source: if not outgoing_edges: return None constraints.append("{0} = 1".format(occ)) elif v == sink: if not incoming_edges: return None constraints.append("{0} = 1".format(icc)) else: # Balancing constraints.append("{0}{1} = 0".format(icc, occ.replace('+', '-'))) # Simple path if incoming_edges: constraints.append("{0} <= 1".format(icc)) if outgoing_edges: constraints.append("{0} <= 1".format(occ)) L.constraints = constraints L.add_vars(nedges) selected, obj_val = L.lpsolve() results = sorted(x for i, x in enumerate(edges) if i in selected) \ if selected else None results = edges_to_path(results) return results, obj_val
def tsp(edges, constraint_generation=False): """ Calculates shortest cycle that traverses each node exactly once. Also known as the Traveling Salesman Problem (TSP). """ edges = populate_edge_weights(edges) incoming, outgoing, nodes = node_to_edge(edges) nedges, nnodes = len(edges), len(nodes) L = LPInstance() L.add_objective(edges, objective=MINIMIZE) balance = [] # For each node, select exactly 1 incoming and 1 outgoing edge for v in nodes: incoming_edges = incoming[v] outgoing_edges = outgoing[v] icc = summation(incoming_edges) occ = summation(outgoing_edges) balance.append("{0} = 1".format(icc)) balance.append("{0} = 1".format(occ)) # Subtour elimination - Miller-Tucker-Zemlin (MTZ) formulation # <http://en.wikipedia.org/wiki/Travelling_salesman_problem> # Desrochers and laporte, 1991 (DFJ) has a stronger constraint # See also: # G. Laporte / The traveling salesman problem: Overview of algorithms start_step = nedges + 1 u0 = nodes[0] nodes_to_steps = dict((n, start_step + i) for i, n in enumerate(nodes[1:])) edge_store = dict((e[:2], i) for i, e in enumerate(edges)) mtz = [] for i, e in enumerate(edges): a, b = e[:2] if u0 in (a, b): continue na, nb = nodes_to_steps[a], nodes_to_steps[b] con_ab = " x{0} - x{1} + {2}x{3}".format(na, nb, nnodes - 1, i + 1) if (b, a) in edge_store: # This extra term is the stronger DFJ formulation j = edge_store[(b, a)] con_ab += " + {0}x{1}".format(nnodes - 3, j + 1) con_ab += " <= {0}".format(nnodes - 2) mtz.append(con_ab) # Step variables u_i bound between 1 and n, as additional variables bounds = [] for i in xrange(start_step, nedges + nnodes): bounds.append(" 1 <= x{0} <= {1}".format(i, nnodes - 1)) L.add_vars(nedges) """ Constraint generation seek to find 'cuts' in the LP problem, by solving the relaxed form. The subtours were then incrementally added to the constraints. """ if constraint_generation: L.constraints = balance subtours = [] while True: selected, obj_val = L.lpsolve() results = sorted(x for i, x in enumerate(edges) if i in selected) \ if selected else None if not results: break G = edges_to_graph(results) cycles = list(nx.simple_cycles(G)) if len(cycles) == 1: break for c in cycles: incident = [edge_store[a, b] for a, b in pairwise(c + [c[0]])] icc = summation(incident) subtours.append("{0} <= {1}".format(icc, len(incident) - 1)) L.constraints = balance + subtours else: L.constraints = balance + mtz L.add_vars(nnodes - 1, offset=start_step, binary=False) L.bounds = bounds selected, obj_val = L.lpsolve() results = sorted(x for i, x in enumerate(edges) if i in selected) \ if selected else None return results
def tsp_gurobi(edges): """ Modeled using GUROBI python example. """ from gurobipy import Model, GRB, quicksum edges = populate_edge_weights(edges) incoming, outgoing, nodes = node_to_edge(edges) idx = dict((n, i) for i, n in enumerate(nodes)) nedges = len(edges) n = len(nodes) m = Model() step = lambda x: "u_{0}".format(x) # Create variables vars = {} for i, (a, b, w) in enumerate(edges): vars[i] = m.addVar(obj=w, vtype=GRB.BINARY, name=str(i)) for u in nodes[1:]: u = step(u) vars[u] = m.addVar(obj=0, vtype=GRB.INTEGER, name=u) m.update() # Bounds for step variables for u in nodes[1:]: u = step(u) vars[u].lb = 1 vars[u].ub = n - 1 # Add degree constraint for v in nodes: incoming_edges = incoming[v] outgoing_edges = outgoing[v] m.addConstr(quicksum(vars[x] for x in incoming_edges) == 1) m.addConstr(quicksum(vars[x] for x in outgoing_edges) == 1) # Subtour elimination edge_store = dict(((idx[a], idx[b]), i) for i, (a, b, w) in enumerate(edges)) # Given a list of edges, finds the shortest subtour def subtour(s_edges): visited = [False] * n cycles = [] lengths = [] selected = [[] for i in range(n)] for x, y in s_edges: selected[x].append(y) while True: current = visited.index(False) thiscycle = [current] while True: visited[current] = True neighbors = [x for x in selected[current] if not visited[x]] if len(neighbors) == 0: break current = neighbors[0] thiscycle.append(current) cycles.append(thiscycle) lengths.append(len(thiscycle)) if sum(lengths) == n: break return cycles[lengths.index(min(lengths))] def subtourelim(model, where): if where != GRB.callback.MIPSOL: return selected = [] # make a list of edges selected in the solution sol = model.cbGetSolution([model._vars[i] for i in range(nedges)]) selected = [edges[i] for i, x in enumerate(sol) if x > .5] selected = [(idx[a], idx[b]) for a, b, w in selected] # find the shortest cycle in the selected edge list tour = subtour(selected) if len(tour) == n: return # add a subtour elimination constraint c = tour incident = [edge_store[a, b] for a, b in pairwise(c + [c[0]])] model.cbLazy(quicksum(model._vars[x] for x in incident) <= len(tour) - 1) m.update() m._vars = vars m.params.LazyConstraints = 1 m.optimize(subtourelim) selected = [v.varName for v in m.getVars() if v.x > .5] selected = [int(x) for x in selected if x[:2] != "u_"] results = sorted(x for i, x in enumerate(edges) if i in selected) \ if selected else None return results