TheAlgorithms-Python
292 строки · 10.6 Кб
1"""
2This is pure Python implementation of Tabu search algorithm for a Travelling Salesman
3Problem, that the distances between the cities are symmetric (the distance between city
4'a' and city 'b' is the same between city 'b' and city 'a').
5The TSP can be represented into a graph. The cities are represented by nodes and the
6distance between them is represented by the weight of the ark between the nodes.
7
8The .txt file with the graph has the form:
9
10node1 node2 distance_between_node1_and_node2
11node1 node3 distance_between_node1_and_node3
12...
13
14Be careful node1, node2 and the distance between them, must exist only once. This means
15in the .txt file should not exist:
16node1 node2 distance_between_node1_and_node2
17node2 node1 distance_between_node2_and_node1
18
19For pytests run following command:
20pytest
21
22For manual testing run:
23python tabu_search.py -f your_file_name.txt -number_of_iterations_of_tabu_search \
24-s size_of_tabu_search
25e.g. python tabu_search.py -f tabudata2.txt -i 4 -s 3
26"""
27
28import argparse
29import copy
30
31
32def generate_neighbours(path):
33"""
34Pure implementation of generating a dictionary of neighbors and the cost with each
35neighbor, given a path file that includes a graph.
36
37:param path: The path to the .txt file that includes the graph (e.g.tabudata2.txt)
38:return dict_of_neighbours: Dictionary with key each node and value a list of lists
39with the neighbors of the node and the cost (distance) for each neighbor.
40
41Example of dict_of_neighbours:
42>>) dict_of_neighbours[a]
43[[b,20],[c,18],[d,22],[e,26]]
44
45This indicates the neighbors of node (city) 'a', which has neighbor the node 'b'
46with distance 20, the node 'c' with distance 18, the node 'd' with distance 22 and
47the node 'e' with distance 26.
48"""
49
50dict_of_neighbours = {}
51
52with open(path) as f:
53for line in f:
54if line.split()[0] not in dict_of_neighbours:
55_list = []
56_list.append([line.split()[1], line.split()[2]])
57dict_of_neighbours[line.split()[0]] = _list
58else:
59dict_of_neighbours[line.split()[0]].append(
60[line.split()[1], line.split()[2]]
61)
62if line.split()[1] not in dict_of_neighbours:
63_list = []
64_list.append([line.split()[0], line.split()[2]])
65dict_of_neighbours[line.split()[1]] = _list
66else:
67dict_of_neighbours[line.split()[1]].append(
68[line.split()[0], line.split()[2]]
69)
70
71return dict_of_neighbours
72
73
74def generate_first_solution(path, dict_of_neighbours):
75"""
76Pure implementation of generating the first solution for the Tabu search to start,
77with the redundant resolution strategy. That means that we start from the starting
78node (e.g. node 'a'), then we go to the city nearest (lowest distance) to this node
79(let's assume is node 'c'), then we go to the nearest city of the node 'c', etc.
80till we have visited all cities and return to the starting node.
81
82:param path: The path to the .txt file that includes the graph (e.g.tabudata2.txt)
83:param dict_of_neighbours: Dictionary with key each node and value a list of lists
84with the neighbors of the node and the cost (distance) for each neighbor.
85:return first_solution: The solution for the first iteration of Tabu search using
86the redundant resolution strategy in a list.
87:return distance_of_first_solution: The total distance that Travelling Salesman
88will travel, if he follows the path in first_solution.
89"""
90
91with open(path) as f:
92start_node = f.read(1)
93end_node = start_node
94
95first_solution = []
96
97visiting = start_node
98
99distance_of_first_solution = 0
100while visiting not in first_solution:
101minim = 10000
102for k in dict_of_neighbours[visiting]:
103if int(k[1]) < int(minim) and k[0] not in first_solution:
104minim = k[1]
105best_node = k[0]
106
107first_solution.append(visiting)
108distance_of_first_solution = distance_of_first_solution + int(minim)
109visiting = best_node
110
111first_solution.append(end_node)
112
113position = 0
114for k in dict_of_neighbours[first_solution[-2]]:
115if k[0] == start_node:
116break
117position += 1
118
119distance_of_first_solution = (
120distance_of_first_solution
121+ int(dict_of_neighbours[first_solution[-2]][position][1])
122- 10000
123)
124return first_solution, distance_of_first_solution
125
126
127def find_neighborhood(solution, dict_of_neighbours):
128"""
129Pure implementation of generating the neighborhood (sorted by total distance of
130each solution from lowest to highest) of a solution with 1-1 exchange method, that
131means we exchange each node in a solution with each other node and generating a
132number of solution named neighborhood.
133
134:param solution: The solution in which we want to find the neighborhood.
135:param dict_of_neighbours: Dictionary with key each node and value a list of lists
136with the neighbors of the node and the cost (distance) for each neighbor.
137:return neighborhood_of_solution: A list that includes the solutions and the total
138distance of each solution (in form of list) that are produced with 1-1 exchange
139from the solution that the method took as an input
140
141Example:
142>>> find_neighborhood(['a', 'c', 'b', 'd', 'e', 'a'],
143... {'a': [['b', '20'], ['c', '18'], ['d', '22'], ['e', '26']],
144... 'c': [['a', '18'], ['b', '10'], ['d', '23'], ['e', '24']],
145... 'b': [['a', '20'], ['c', '10'], ['d', '11'], ['e', '12']],
146... 'e': [['a', '26'], ['b', '12'], ['c', '24'], ['d', '40']],
147... 'd': [['a', '22'], ['b', '11'], ['c', '23'], ['e', '40']]}
148... ) # doctest: +NORMALIZE_WHITESPACE
149[['a', 'e', 'b', 'd', 'c', 'a', 90],
150['a', 'c', 'd', 'b', 'e', 'a', 90],
151['a', 'd', 'b', 'c', 'e', 'a', 93],
152['a', 'c', 'b', 'e', 'd', 'a', 102],
153['a', 'c', 'e', 'd', 'b', 'a', 113],
154['a', 'b', 'c', 'd', 'e', 'a', 119]]
155"""
156
157neighborhood_of_solution = []
158
159for n in solution[1:-1]:
160idx1 = solution.index(n)
161for kn in solution[1:-1]:
162idx2 = solution.index(kn)
163if n == kn:
164continue
165
166_tmp = copy.deepcopy(solution)
167_tmp[idx1] = kn
168_tmp[idx2] = n
169
170distance = 0
171
172for k in _tmp[:-1]:
173next_node = _tmp[_tmp.index(k) + 1]
174for i in dict_of_neighbours[k]:
175if i[0] == next_node:
176distance = distance + int(i[1])
177_tmp.append(distance)
178
179if _tmp not in neighborhood_of_solution:
180neighborhood_of_solution.append(_tmp)
181
182index_of_last_item_in_the_list = len(neighborhood_of_solution[0]) - 1
183
184neighborhood_of_solution.sort(key=lambda x: x[index_of_last_item_in_the_list])
185return neighborhood_of_solution
186
187
188def tabu_search(
189first_solution, distance_of_first_solution, dict_of_neighbours, iters, size
190):
191"""
192Pure implementation of Tabu search algorithm for a Travelling Salesman Problem in
193Python.
194
195:param first_solution: The solution for the first iteration of Tabu search using
196the redundant resolution strategy in a list.
197:param distance_of_first_solution: The total distance that Travelling Salesman will
198travel, if he follows the path in first_solution.
199:param dict_of_neighbours: Dictionary with key each node and value a list of lists
200with the neighbors of the node and the cost (distance) for each neighbor.
201:param iters: The number of iterations that Tabu search will execute.
202:param size: The size of Tabu List.
203:return best_solution_ever: The solution with the lowest distance that occurred
204during the execution of Tabu search.
205:return best_cost: The total distance that Travelling Salesman will travel, if he
206follows the path in best_solution ever.
207"""
208count = 1
209solution = first_solution
210tabu_list = []
211best_cost = distance_of_first_solution
212best_solution_ever = solution
213
214while count <= iters:
215neighborhood = find_neighborhood(solution, dict_of_neighbours)
216index_of_best_solution = 0
217best_solution = neighborhood[index_of_best_solution]
218best_cost_index = len(best_solution) - 1
219
220found = False
221while not found:
222i = 0
223while i < len(best_solution):
224if best_solution[i] != solution[i]:
225first_exchange_node = best_solution[i]
226second_exchange_node = solution[i]
227break
228i = i + 1
229
230if [first_exchange_node, second_exchange_node] not in tabu_list and [
231second_exchange_node,
232first_exchange_node,
233] not in tabu_list:
234tabu_list.append([first_exchange_node, second_exchange_node])
235found = True
236solution = best_solution[:-1]
237cost = neighborhood[index_of_best_solution][best_cost_index]
238if cost < best_cost:
239best_cost = cost
240best_solution_ever = solution
241else:
242index_of_best_solution = index_of_best_solution + 1
243best_solution = neighborhood[index_of_best_solution]
244
245if len(tabu_list) >= size:
246tabu_list.pop(0)
247
248count = count + 1
249
250return best_solution_ever, best_cost
251
252
253def main(args=None):
254dict_of_neighbours = generate_neighbours(args.File)
255
256first_solution, distance_of_first_solution = generate_first_solution(
257args.File, dict_of_neighbours
258)
259
260best_sol, best_cost = tabu_search(
261first_solution,
262distance_of_first_solution,
263dict_of_neighbours,
264args.Iterations,
265args.Size,
266)
267
268print(f"Best solution: {best_sol}, with total distance: {best_cost}.")
269
270
271if __name__ == "__main__":
272parser = argparse.ArgumentParser(description="Tabu Search")
273parser.add_argument(
274"-f",
275"--File",
276type=str,
277help="Path to the file containing the data",
278required=True,
279)
280parser.add_argument(
281"-i",
282"--Iterations",
283type=int,
284help="How many iterations the algorithm should perform",
285required=True,
286)
287parser.add_argument(
288"-s", "--Size", type=int, help="Size of the tabu list", required=True
289)
290
291# Pass the arguments to main method
292main(parser.parse_args())
293