4913 | | Given a graph (resp. a digraph) `G` with weighted edges, |
4914 | | the traveling salesman problem consists in finding a |
4915 | | Hamiltonian cycle (resp. circuit) of the graph of |
4916 | | minimum cost. |
4917 | | |
4918 | | This TSP is one of the most famous NP-Complete problems, |
4919 | | this function can thus be expected to take some time |
4920 | | before returning its result. |
4921 | | |
4922 | | INPUT: |
4923 | | |
4924 | | - ``weighted`` (boolean) -- whether to consider the |
4925 | | weights of the edges. |
4926 | | |
4927 | | - If set to ``False`` (default), all edges are |
4928 | | assumed to weight `1` |
4929 | | |
4930 | | - If set to ``True``, the weights are taken into |
4931 | | account, and the edges whose weight is ``None`` |
4932 | | are assumed to be set to `1` |
| 4914 | Given a graph (resp. a digraph) `G` with weighted edges, the traveling |
| 4915 | salesman problem consists in finding a hamiltonian cycle (resp. circuit) |
| 4916 | of the graph of minimum cost. |
| 4917 | |
| 4918 | This TSP is one of the most famous NP-Complete problems, this function |
| 4919 | can thus be expected to take some time before returning its result. |
| 4920 | |
| 4921 | INPUT: |
| 4922 | |
| 4923 | - ``weighted`` (boolean) -- whether to consider the weights of the |
| 4924 | edges. |
| 4925 | |
| 4926 | - If set to ``False`` (default), all edges are assumed to weight |
| 4927 | `1` |
| 4928 | |
| 4929 | - If set to ``True``, the weights are taken into account, and the |
| 4930 | edges whose weight is ``None`` are assumed to be set to `1` |
5027 | | """ |
5028 | | |
5029 | | from sage.numerical.mip import MixedIntegerLinearProgram, Sum |
5030 | | |
5031 | | p = MixedIntegerLinearProgram(maximization = False, solver = solver) |
5032 | | |
5033 | | f = p.new_variable() |
5034 | | r = p.new_variable() |
5035 | | |
5036 | | # If the graph has multiple edges |
| 5028 | TESTS: |
| 5029 | |
| 5030 | Comparing the results returned according to the value of |
| 5031 | ``constraint_generation``. First, for graphs:: |
| 5032 | |
| 5033 | sage: from operator import itemgetter |
| 5034 | sage: n = 20 |
| 5035 | sage: for i in range(20): |
| 5036 | ... g = Graph() |
| 5037 | ... g.allow_multiple_edges(False) |
| 5038 | ... for u,v in graphs.RandomGNP(n,.2).edges(labels = False): |
| 5039 | ... g.add_edge(u,v,round(random(),5)) |
| 5040 | ... for u,v in graphs.CycleGraph(n).edges(labels = False): |
| 5041 | ... if not g.has_edge(u,v): |
| 5042 | ... g.add_edge(u,v,round(random(),5)) |
| 5043 | ... v1 = g.traveling_salesman_problem(constraint_generation = False) |
| 5044 | ... v2 = g.traveling_salesman_problem() |
| 5045 | ... c1 = sum(map(itemgetter(2), v1.edges())) |
| 5046 | ... c2 = sum(map(itemgetter(2), v2.edges())) |
| 5047 | ... if c1 != c2: |
| 5048 | ... print "Erreur !",c1,c2 |
| 5049 | ... break |
| 5050 | |
| 5051 | Then for digraphs:: |
| 5052 | |
| 5053 | sage: from operator import itemgetter |
| 5054 | sage: n = 20 |
| 5055 | sage: for i in range(20): |
| 5056 | ... g = DiGraph() |
| 5057 | ... g.allow_multiple_edges(False) |
| 5058 | ... for u,v in digraphs.RandomDirectedGNP(n,.2).edges(labels = False): |
| 5059 | ... g.add_edge(u,v,round(random(),5)) |
| 5060 | ... for u,v in digraphs.Circuit(n).edges(labels = False): |
| 5061 | ... if not g.has_edge(u,v): |
| 5062 | ... g.add_edge(u,v,round(random(),5)) |
| 5063 | ... v2 = g.traveling_salesman_problem() |
| 5064 | ... v1 = g.traveling_salesman_problem(constraint_generation = False) |
| 5065 | ... c1 = sum(map(itemgetter(2), v1.edges())) |
| 5066 | ... c2 = sum(map(itemgetter(2), v2.edges())) |
| 5067 | ... if c1 != c2: |
| 5068 | ... print "Erreur !",c1,c2 |
| 5069 | ... print "Avec generation de contrainte :",c2 |
| 5070 | ... print "Sans generation de contrainte :",c1 |
| 5071 | ... break |
| 5072 | |
| 5073 | """ |
| 5074 | ############################### |
| 5075 | # Quick cheks of connectivity # |
| 5076 | ############################### |
| 5077 | |
| 5078 | # TODO : Improve it by checking vertex-connectivity instead of |
| 5079 | # edge-connectivity.... But calling the vertex_connectivity (which |
| 5080 | # builds a LP) is way too slow. These tests only run traversals written |
| 5081 | # in Cython --> hence FAST |
| 5082 | |
| 5083 | if self.is_directed(): |
| 5084 | if not self.is_strongly_connected(): |
| 5085 | raise ValueError("The given graph is not hamiltonian") |
| 5086 | |
| 5087 | else: |
| 5088 | # Checks whether the graph is 2-connected |
| 5089 | if not self.strong_orientation().is_strongly_connected(): |
| 5090 | raise ValueError("The given graph is not hamiltonian") |
| 5091 | |
| 5092 | |
| 5093 | |
| 5094 | ############################ |
| 5095 | # Deal with multiple edges # |
| 5096 | ############################ |
| 5097 | |
| 5124 | from sage.numerical.mip import MixedIntegerLinearProgram, Sum |
| 5125 | from sage.numerical.mip import MIPSolverException |
| 5126 | |
| 5127 | weight = lambda l : l if (l is not None and l) else 1 |
| 5128 | |
| 5129 | #################################################### |
| 5130 | # Constraint-generation formulation of the problem # |
| 5131 | #################################################### |
| 5132 | |
| 5133 | if constraint_generation: |
| 5134 | |
| 5135 | p = MixedIntegerLinearProgram(maximization = False, |
| 5136 | solver = solver, |
| 5137 | constraint_generation = True) |
| 5138 | |
| 5139 | |
| 5140 | # Directed Case # |
| 5141 | ################# |
| 5142 | if g.is_directed(): |
| 5143 | |
| 5144 | from sage.graphs.all import DiGraph |
| 5145 | b = p.new_variable(binary = True, dim = 2) |
| 5146 | |
| 5147 | # Objective function |
| 5148 | if weighted: |
| 5149 | p.set_objective(Sum([ weight(l)*b[u][v] |
| 5150 | for u,v,l in g.edges()])) |
| 5151 | |
| 5152 | # All the vertices have in-degree 1 and out-degree 1 |
| 5153 | for v in g: |
| 5154 | p.add_constraint(Sum([b[u][v] for u in g.neighbors_in(v)]), |
| 5155 | min = 1, |
| 5156 | max = 1) |
| 5157 | p.add_constraint(Sum([b[v][u] for u in g.neighbors_out(v)]), |
| 5158 | min = 1, |
| 5159 | max = 1) |
| 5160 | |
| 5161 | # Initial Solve |
| 5162 | try: |
| 5163 | p.solve(log = verbose) |
| 5164 | except MIPSolverException: |
| 5165 | raise ValueError("The given graph is not hamiltonian") |
| 5166 | |
| 5167 | |
| 5168 | while True: |
| 5169 | # We build the DiGraph representing the current solution |
| 5170 | h = DiGraph() |
| 5171 | for u,v,l in g.edges(): |
| 5172 | if p.get_values(b[u][v]) > .5: |
| 5173 | h.add_edge(u,v,l) |
| 5174 | |
| 5175 | # If there is only one circuit, we are done ! |
| 5176 | cc = h.connected_components() |
| 5177 | if len(cc) == 1: |
| 5178 | break |
| 5179 | |
| 5180 | # Adding the corresponding constraint |
| 5181 | if verbose_constraints: |
| 5182 | print "Adding a constraint on set",cc[0] |
| 5183 | |
| 5184 | |
| 5185 | p.add_constraint(Sum(b[u][v] for u,v in |
| 5186 | g.edge_boundary(cc[0], labels = False)), |
| 5187 | min = 1) |
| 5188 | |
| 5189 | try: |
| 5190 | p.solve(log = verbose) |
| 5191 | except MIPSolverException: |
| 5192 | raise ValueError("The given graph is not hamiltonian") |
| 5193 | |
| 5194 | # Undirected Case # |
| 5195 | ################### |
| 5196 | else: |
| 5197 | |
| 5198 | from sage.graphs.all import Graph |
| 5199 | b = p.new_variable(binary = True) |
| 5200 | B = lambda u,v : b[(u,v)] if u<v else b[(v,u)] |
| 5201 | |
| 5202 | # Objective function |
| 5203 | if weighted: |
| 5204 | p.set_objective(Sum([ weight(l)*B(u,v) |
| 5205 | for u,v,l in g.edges()]) ) |
| 5206 | |
| 5207 | # All the vertices have degree 2 |
| 5208 | for v in g: |
| 5209 | p.add_constraint(Sum([ B(u,v) for u in g.neighbors(v)]), |
| 5210 | min = 2, |
| 5211 | max = 2) |
| 5212 | |
| 5213 | # Initial Solve |
| 5214 | try: |
| 5215 | p.solve(log = verbose) |
| 5216 | except MIPSolverException: |
| 5217 | raise ValueError("The given graph is not hamiltonian") |
| 5218 | |
| 5219 | while True: |
| 5220 | # We build the DiGraph representing the current solution |
| 5221 | h = Graph() |
| 5222 | for u,v,l in g.edges(): |
| 5223 | if p.get_values(B(u,v)) > .5: |
| 5224 | h.add_edge(u,v,l) |
| 5225 | |
| 5226 | # If there is only one circuit, we are done ! |
| 5227 | cc = h.connected_components() |
| 5228 | if len(cc) == 1: |
| 5229 | break |
| 5230 | |
| 5231 | # Adding the corresponding constraint |
| 5232 | if verbose_constraints: |
| 5233 | print "Adding a constraint on set",cc[0] |
| 5234 | |
| 5235 | p.add_constraint(Sum(B(u,v) for u,v in |
| 5236 | g.edge_boundary(cc[0], labels = False)), |
| 5237 | min = 2) |
| 5238 | |
| 5239 | try: |
| 5240 | p.solve(log = verbose) |
| 5241 | except MIPSolverException: |
| 5242 | raise ValueError("The given graph is not hamiltonian") |
| 5243 | |
| 5244 | |
| 5245 | |
| 5246 | # We can now return the TSP ! |
| 5247 | answer = self.subgraph(edges = h.edges()) |
| 5248 | answer.set_pos(self.get_pos()) |
| 5249 | answer.name("TSP from "+g.name()) |
| 5250 | return answer |
| 5251 | |
| 5252 | ################################################# |
| 5253 | # ILP formulation without constraint generation # |
| 5254 | ################################################# |
| 5255 | |
| 5256 | |
| 5257 | |
| 5258 | p = MixedIntegerLinearProgram(maximization = False, solver = solver) |
| 5259 | |
| 5260 | f = p.new_variable() |
| 5261 | r = p.new_variable() |
| 5262 | |