-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathFuncTSP.py
1331 lines (1089 loc) · 51 KB
/
FuncTSP.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# coding: utf-8
# # load_problem
# Given a `namefile` of a tsp file, like "st70.tsp", collocated in the right path, the function returns the `problem` object, containing relevant informations about that tsp file, like the list of the nodes with their own coordinates.
# In[1]:
import tsplib95
global problem, counter_call_Make_2_Opt_Move, counter_call_Gain_From_2_Opt, counter_call_One_City_2_Opt
def load_problem(namefile):
global problem
problem = tsplib95.load("Network Optimization\\ALL_tsp\\"+str(namefile)+"\\"+str(namefile))
return problem
# # distance
# Given two nodes, the function returns the euclidean distance between their coordinates, that is the weight of the arc connecting the two nodes
# In[ ]:
def distance(cityOne, cityTwo):
global problem
edge = cityOne, cityTwo
weight = problem.get_weight(*edge) #distance between the two
return weight
# # totalDistance
# Given a `tour`, that is a list of nodes, the function call the distance function for each couple of adjacent nodes in the list, summing all the results together to obtain the total distance of the tour
# In[ ]:
def totalDistance(tour):
totDist = 0
for i in range(len(tour)-1):
totDist += distance(tour[i], tour[i+1])
return totDist
# # nearest_neighbor
# Find the city in the list `cities` that is nearest to city `A`.
# In[ ]:
def nearest_neighbor(A, cities):
return min(cities, key=lambda c: distance(c, A))
# # Build_Nearest_Neighbor_Tour
# Start the tour at the first city `firstCity`. At each step of the while cycle extend the tour by moving from the previous city to its nearest neighbor that has not yet been visited.
# In[ ]:
def Build_Nearest_Neighbor_Tour(firstCity, cities):
start = firstCity
tour = [start]
unvisited = set(cities)
unvisited.remove(start)
while unvisited:
C = nearest_neighbor(tour[-1], unvisited)
tour.append(C)
unvisited.remove(C)
tour.append(firstCity) #add as last city the first one of the tour, useful to calculate correctly the total distance of the tour
return tour
# # Build_Random_Tour
# Start from an empty tour. At each step of the while cycle, extend the tour by adding a random city that has not yet been visited.
# In[ ]:
import random
def Build_Random_Tour(nodes):
listNodes = []
listNodes[:] = nodes[:]
tour = []
while len(listNodes) > 0:
nextCity = (int)(random.random()*len(listNodes))
tour.append(listNodes[nextCity])
del listNodes[nextCity]
tour.append(tour[0]) #add as last city the first one of the tour, useful to calculate correctly the total distance of the tour
return tour
# # Gain_From_2_Opt
# Gain of tour length that can be obtained by performing given 2-opt move: it returns a positive number if it is convenient to perform a 2-opt move, a negative number otherwise, checking if the distance between `X1` and `X2` plus the distance between `Y1` and `Y2` is greater than the distance between `X1` and `Y1` plus the distance between `X2` and `Y2`.
# <br>
# `X2` is the successor of `X1`.
# <br>
# `Y2` is the successor of `Y1`.
# In[ ]:
def Gain_From_2_Opt(X1, X2, Y1, Y2):
global counter_call_Gain_From_2_Opt
del_Length = distance(X1, X2) + distance(Y1, Y2)
add_Length = distance(X1, Y1) + distance(X2, Y2)
result = del_Length - add_Length
counter_call_Gain_From_2_Opt += 1 #to check how many times this function is called
return result
# # Reverse_Segment
# Reverses order of elements in segment starting from startIndex and ending to endIndex of tour.
# <br>
# Final and initial part of tour make one segment.
# <br>
# While the order of arguments (`startIndex` and `endIndex`) can be safely changed when reversing segment in 2-optimization, it makes difference for 3-opt move: it isn't the same to reverse (x,z) and (z,x) in 3-opt.
# In[ ]:
def Reverse_Segment(tour, startIndex, endIndex):
N = len(tour)
inversionSize = (int)(((N + endIndex - startIndex + 1) % N) / 2)
left = startIndex
right = endIndex
for counter in range(inversionSize): #swap tour[left] with tour[right] for each cities between startIndex and endIndex
aux = tour[left]
tour[left] = tour[right]
tour[right] = aux
left = (left + 1) % N
right = (len(tour) + right - 1) % N
# # Make_2_Opt_Move
# Performs given 2-opt move on array representation of the tour.
# <br>
# The cyclic tour is cut in 2 places, by removing 2 links:
# <br>
# L1: from `t[i]` to `t[i+1]` and L2: from `t[j]` to `t[j+1]` and replacing them with
# <br>
# L1': from `t[i]` to `t[j]` and L2': from `t[i+1]` to `t[j+1]`
# <br>
# This is equivalent to reverse the order in segment from `i+1` to `j`
# In[ ]:
def Make_2_Opt_Move(tour, i, j):
global counter_call_Make_2_Opt_Move
Reverse_Segment(tour, (i+1) % len(tour), j)
counter_call_Make_2_Opt_Move += 1 #to check how many times this function is called
# # One_City_2_Opt
# Shortens the tour by repeating 2-opt moves until no improvement can be done.
# <br>
# In every iteration the function looks for and applies the move that gives maximal length gain.
# <br>
# It is the basic version, without any speedup.
# <br>
# The parameters passed to it are the list of cities, `tour`, the index `basePos` of the city that is used to search if there is a substitute better than its successor to decrease the total tour distance, the `improvement` chosen (First or Best taken).
# In[ ]:
def One_City_2_Opt(tour, basePos, improvement):
global counter_call_One_City_2_Opt # used to count how many times this function is called
counter_call_One_City_2_Opt += 1
improved = False
if improvement == "Best":
locallyOptimal = True
bestMove = {"gain":0,"i":0,"j":0} # structure useful for the best improvement keeping trace of the last better gain and the positions of the cities in the tour giving that gain
N = len(tour)
i = basePos # index of the city for which it is done the local search in the tour
X1 = tour[i] # city with index i in the tour
X2 = tour[(i+1) % N] # the successor of X1 in the tour
if i == 0:
counter_2_Limit = N-1 #in this way we don't get in the cycle Y2 equal to X1
else:
counter_2_Limit = N
for counter_2 in range((i+2), counter_2_Limit):
j = counter_2 # index of another city different from X1 and X2
Y1 = tour[j] # city with index j in the tour
Y2 = tour[(j+1) % N] # the successor of Y1 in the tour
gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
if improvement == "First":
if gainExpected > 0:
Make_2_Opt_Move(tour, i, j)
improved = True
return improved # for the First Improvement, it has been found a better solution, so we break here the research
else:
if gainExpected > bestMove["gain"]:
bestMove = {"gain":gainExpected,"i":i,"j":j} # for the Best Improvement, it has been found a better solution, so we save it and we continue the research
locallyOptimal = False
if improvement == "Best":
if not locallyOptimal: # only out from the cycle, if it has been found a good solution with the Best Improvement, we use the best solution found
Make_2_Opt_Move(tour, bestMove["i"], bestMove["j"])
improved = True
return improved
# # LS_2_Opt_NoSpeedup
# Optimizes the given tour using 2-opt without speedup.
# <br>
# While it can be found an improvement, the function try to do the 2-opt move starting from the city called basePos and the cities after it.
# <br>
# The city called `basePos` goes from the first one of the tour till the last one minus one, because the last one hasn't followers, so it is useless.
# In[ ]:
def LS_2_Opt_NoSpeedup(tour,improvement):
del tour[len(tour)-1]
N = len(tour)
locallyOptimal = False
while not locallyOptimal:
locallyOptimal = True
for basePos in range(0,N-2):
improved = One_City_2_Opt(tour, basePos, improvement)
if improved:
locallyOptimal = False
tour.append(tour[0])
return tour
# # sortSecond
# Used to sort the list of neighbors depending on the distance.
# <br>
# `val` is an element of the list of neighbors with two elements: the first one is the number of the city and the second one is its distance from a certain node to which we are finding its closest neighbors.
# In[ ]:
def sortSecond(val):
return val[1]
# # Build_Neighbors_Matrix
# Builds lists of `No_Of_Neighbors` nearest neighbors of each city.
# <br>
# `neighbor[i][j]` is the j-th nearest neighbour of city number `i`.
# In[ ]:
def Build_Neighbors_Matrix(No_Of_Neighbors,N):
neighbor = [] #matrix
neighbor.append("EMPTY ROW") # the first row is empty and not used because there is no city with number 0. The cities start from 1.
for currentCity in range(1,N+1):
neighbors_of_currentCity_with_distance = []
# create the list of all neighbors of currentCity:
for otherCity in range(1,N+1):
if otherCity != currentCity:
neighbors_of_currentCity_with_distance.append([otherCity, distance(currentCity,otherCity)])
# Sort elements in neighbors_of_currentCity by ascending distance
neighbors_of_currentCity_with_distance.sort(key=sortSecond, reverse = False)
neighbors_of_currentCity_with_distance = neighbors_of_currentCity_with_distance[0:No_Of_Neighbors]
neighbors_of_currentCity = []
for i in neighbors_of_currentCity_with_distance:
neighbors_of_currentCity.append(i[0]) #it mantains only the neighbors, not the distances, useful only for ordering the neighbors
neighbor.append(neighbors_of_currentCity)
return neighbor
# # isDLB_on
# Checks if the DLB is on or off for a certain city passed as parameter
# In[ ]:
def isDLB_on(DontLook, city):
return DontLook[city] #return True or False
# # Set_DLB_on
# Sets the DLB to on for a certain city passed as parameter
# In[ ]:
def Set_DLB_on(DontLook, city):
DontLook[city] = True
# # Set_DLB_off
# Turns off the DLB for all the cities passed inside a list passed as parameter
# In[ ]:
def Set_DLB_off(DontLook, cities):
# turns off DLB for given cities
for city in range(0,len(cities)):
DontLook[cities[city]] = False
# # One_City_2_Opt_DR
# Shortens the tour by repeating 2-opt moves until no improvement can be done with DLB or Fixed Radius.
# <br>
# In every iteration the function looks for and applies the move that gives maximal length gain.
# <br>
# This is the version with speedup DLB or Fixed Radius.
# <br>
# Its role is the same as `One_City_2_Opt()`, but here there is the speedup, so there are some modifications for it.
# <br>
# The parameter `DontLook` is used to pass the structure containing the information about the DLB, in the case we use DLB as speedup, or it is a variable at False in the other case.
# <br>
# `Fraction_Radius` is used if we use Fixed Radius as speedup, to enlarge or restrict the radius, that is the distance between two consecutive cities.
# In[ ]:
def One_City_2_Opt_DR(tour, basePos, DontLook, improvement, speedup, Fraction_Radius):
N = len(tour)
improved = False
if improvement == "Best":
locallyOptimal = True
if "DLB" in speedup:
bestMove = {"gain":0,"i":0,"j":0,"X1":0,"X2":0,"Y1":0,"Y2":0} # the structure with DLB as speedup has to remember more things (X1,X2,Y1,Y2) because they have to be turned off if chosen for the 2-opt move
else:
bestMove = {"gain":0,"i":0,"j":0}
for direction in ["forward", "backward"]: # both tour neighbors considered: link between t[i] and t[i+1], then between t[i-1] and t[i]
if "DLB" in speedup:
if direction == "forward":
i = basePos
else:
i = (N + basePos - 1) % N
X1 = tour[i]
X2 = tour[(i+1) % N]
else:
if direction == "forward":
i = basePos
X1 = tour[i]
X2 = tour[(i+1) % N]
else:
i = (N + basePos - 1) % N
X2 = tour[i]
X1 = tour[basePos] # it is equal to tour[(i+1) % N]
radius = distance(X1, X2) #the distance chosen as radius can be fixed a priori as a constant or depending on the actual distance between two consecutive cities. Here we consider the second case
for counter_2 in range(0,N):
j = counter_2
Y1 = tour[j]
Y2 = tour[(j+1) % N]
if "FixedRadius" in speedup:
if direction == "backward":
aux = Y1 #swap between Y1 and Y2 in the case of backward direction of the tour
Y1 = Y2
Y2 = aux
if (X1 == Y1) or (X2 == Y1) or (Y2 == X1):
continue
if "FixedRadius" in speedup:
if distance(X2,Y2) > radius*Fraction_Radius: # only if the distance between X2 and Y2 is less than radius multiplied by Fraction_Radius we check if it is good to do the 2-opt move.
continue
gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
if improvement == "First":
if gainExpected > 0:
if "DLB" in speedup:
Set_DLB_off(DontLook, [X1, X2, Y1, Y2])
Make_2_Opt_Move(tour, i, j)
improved = True
return improved
else:
if gainExpected > bestMove["gain"]: #if it is greater than zero!
if "DLB" in speedup:
bestMove = {"gain":gainExpected,"i":i,"j":j,"X1":X1,"X2":X2,"Y1":Y1,"Y2":Y2}
else:
bestMove = {"gain":gainExpected,"i":i,"j":j}
locallyOptimal = False
if improvement == "Best":
if not locallyOptimal:
if "DLB" in speedup:
Set_DLB_off(DontLook, [bestMove["X1"], bestMove["X2"], bestMove["Y1"], bestMove["Y2"]])
Make_2_Opt_Move(tour, bestMove["i"], bestMove["j"])
improved = True
return improved
# # One_City_2_Opt_NDR
# Shortens the tour by repeating 2-opt moves until no improvement can be done with Neighbor List (with or without DLB and/or Fixed Radius).
# <br>
# In every iteration the function looks for and applies the move that gives maximal length gain.
# <br>
# This is the version with speedup Neighbor List (with or without DLB and/or Fixed Radius).
# <br>
# Its role is the same as `One_City_2_Opt()`, but here there is the Neighbor List, so there are some modifications for it, in particular for the research of `Y1` and `Y2`.
# <br>
# The parameter `neighbor` is the matrix of neighbors of all the cities in the tour.
# <br>
# The parameter `numberOfNeigbors` is the length of the rows of the matrix neighbor.
# <br>
# The other parameters are the same of `One_City_2_Opt()` and `One_City_2_Opt_DR()`.
# In[ ]:
def One_City_2_Opt_NDR(tour, basePos, neighbor, numberOfNeigbors, DontLook, improvement, speedup, Fraction_Radius):
N = len(tour)
improved = False
if improvement == "Best":
locallyOptimal = True
if "DLB" in speedup:
bestMove = {"gain":0,"i":0,"j":0,"X1":0,"X2":0,"Y1":0,"Y2":0}
else:
bestMove = {"gain":0,"i":0,"j":0}
for direction in ["forward", "backward"]:
if direction == "forward":
i = basePos
X1 = tour[i]
X2 = tour[(i+1) % N]
else:
i = (N + basePos - 1) % N
X2 = tour[i]
X1 = tour[(i+1) % N] # == tour[basePos]
if "FixedRadius" in speedup:
radius = distance(X1,X2)
for neighbor_number in range(0, numberOfNeigbors): #this is the part that changes using NeighborList. It isn't compactable because the final part, that is equal even without the NeighborList, is still inside the cycle for
Y1 = neighbor[X1][neighbor_number]
if direction == "forward":
j = tour.index(Y1)
Y2 = tour[(j+1) % N]
else:
j = (N + tour.index(Y1) - 1) % N # pos[Y1] == j+1
Y2 = tour[j]
if (X2 == Y1) or (Y2 == X1):
continue
if "FixedRadius" in speedup:
if distance(X1, Y1) > radius*Fraction_Radius:
break # we break the cycle here because the other Y1 will all have greater distances from X1 because we are using here the matrix of neighbors of X1, ordered in ascending order
gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
if improvement == "First":
if gainExpected > 0:
if "DLB" in speedup:
Set_DLB_off(DontLook, [X1, X2, Y1, Y2])
Make_2_Opt_Move(tour, i, j)
improved = True
return improved
else:
if gainExpected > bestMove["gain"]:
if "DLB" in speedup:
bestMove = {"gain":gainExpected,"i":i,"j":j,"X1":X1,"X2":X2,"Y1":Y1,"Y2":Y2}
else:
bestMove = {"gain":gainExpected,"i":i,"j":j}
locallyOptimal = False
if improvement == "Best":
if not locallyOptimal:
if "DLB" in speedup:
Set_DLB_off(DontLook, [bestMove["X1"], bestMove["X2"], bestMove["Y1"], bestMove["Y2"]])
Make_2_Opt_Move(tour, bestMove["i"], bestMove["j"])
improved = True
return improved
# # LS_2_Opt
# Optimizes the given tour using 2-opt with speedup.
# <br>
# While it can be found an improvement, the function try to do the 2-opt move starting from the city `basePos` and the cities after it.
# <br>
# The parameters `No_Of_Neigbors` and `Fraction_Radius`, if not passed, are setted with their default values.
# In[ ]:
def LS_2_Opt(tour,improvement, speedup, No_Of_Neigbors = False, Fraction_Radius = 1):
del tour[len(tour)-1]
N = len(tour)
locallyOptimal = False
if "NeighborList" in speedup:
if No_Of_Neigbors == False:
No_Of_Neigbors = (int)(len(tour)/10)
neighborListLen = min(No_Of_Neigbors, len(tour)-1)#neighbors mustn't exceed the number of cities in the tour
neighbor = Build_Neighbors_Matrix(neighborListLen,N)#neighbor is the matrix of the neighbors
if "DLB" in speedup:
DontLook = {} #it is a dictionary where the key is the city and the value is True or False
for i in range(1,len(tour)+1):
DontLook[i] = False#set DLB off
else:
DontLook = False #if we don't use DLB we don't need any structure for it, but we have in any case something to pass as DontLook to the function (because it consider either the cases with and without DLB to be more compact)
while not locallyOptimal:
locallyOptimal = True
for basePos in range(0,N):
if "DLB" in speedup:
baseCity = tour[basePos]
if isDLB_on(DontLook, baseCity):
continue
if "NeighborList" in speedup: # here there are all the possible cases in which there is NeighborList
improved = One_City_2_Opt_NDR(tour, basePos, neighbor, neighborListLen, DontLook, improvement, speedup, Fraction_Radius)
else: # here there are the other cases with speedup but without NeighborList
improved = One_City_2_Opt_DR(tour, basePos, DontLook, improvement, speedup, Fraction_Radius)
if not improved:
if "DLB" in speedup:
Set_DLB_on(DontLook, baseCity)
else:
locallyOptimal = False
tour.append(tour[0])
return tour
# # Between
# Returns true if `x` is between `a` and `b` in cyclic sequence of the tour.
# <br>
# It is used if used the 3-opt method.
# In[ ]:
def Between(a, x, b):
if (b > a):
result = (x > a) and (x < b)
elif (b < a):
result = (x > a) or (x < b)
else:
result = False
# # Gain_From_3_Opt
# Gain of tour length which can be obtained by performing 3-opt move.
# <br>
# `X2` is the successor of `X1`, `Y2` of `Y1`, `Z2` of `Z1`.
# <br>
# The cyclic order must be `X1`,`Y1`,`Z1`, so we can have `X1`->`Y1`->`Z1` or `Y1`->`Z1`->`X1` or `Z1`->`X1`->`Y1`, but not `Z1`->`Y1`->`X1`.
# <br>
# The cyclic tour is built from three segments: `abc`
# <br>
# segment `a` = `Z2`..`X1` (could be that `X1` is towards the end of the list `tour` and `Z2` at the beginning of it)
# <br>
# segment `b` = `X2`..`Y1`
# <br>
# segment `c` = `Y2`..`Z1`
# <br>
# `a'` means reversed segment `a`, same thing for `b'` and `c'`
# In[ ]:
def Gain_From_3_Opt(X1, X2, Y1, Y2, Z1, Z2, optCase):
if optCase == "opt3_case_0":
return 0 # original tour remains without changes
# 2-OPT MOVES
# the move is identical to a single 2-opt move
elif optCase == "opt3_case_1": # a'bc; 2-opt (i,k)
del_Length = distance(X1, X2) + distance(Z1, Z2)
add_Length = distance(X1, Z1) + distance(X2, Z2)
elif optCase == "opt3_case_2": # abc'; 2-opt (j,k)
del_Length = distance(Y1, Y2) + distance(Z1, Z2)
add_Length = distance(Y1, Z1) + distance(Y2, Z2)
elif optCase == "opt3_case_3": # ab'c; 2-opt (i,j)
del_Length = distance(X1, X2) + distance(Y1, Y2)
add_Length = distance(X1, Y1) + distance(X2, Y2)
# PURE 3-OPT MOVES
# all 3 edges must be removed, so they all have the same formula for del_Length
# CASE A) move equal to two subsequent 2-opt moves
elif optCase == "opt3_case_4": # ab'c'
add_Length = distance(X1, Y1) + distance(X2, Z1) + distance(Y2, Z2)
del_Length = distance(X1, X2) + distance(Y1, Y2) + distance(Z1, Z2)
elif optCase == "opt3_case_5": # a'b'c
add_Length = distance(X1, Z1) + distance(Y2, X2) + distance(Y1, Z2)
del_Length = distance(X1, X2) + distance(Y1, Y2) + distance(Z1, Z2)
elif optCase == "opt3_case_6": # a'bc'
add_Length = distance(X1, Y2) + distance(Z1, Y1) + distance(X2, Z2)
del_Length = distance(X1, X2) + distance(Y1, Y2) + distance(Z1, Z2)
# CASE B) move equal to three subsequent 2-opt moves
elif optCase == "opt3_case_7": # a'b'c'
add_Length = distance(X1, Y2) + distance(Z1, X2) + distance(Y1, Z2)
del_Length = distance(X1, X2) + distance(Y1, Y2) + distance(Z1, Z2)
result = del_Length - add_Length
return result
# # Make_3_Opt_Move
# Performs given 3-opt move on the tour.
# <br>
# The cyclic order must be: `k`>`j`>`i` or `i`>`k`>`j` or `j`>`k`>`i`, but not `i`>`j`>`k` nor `j`>`k`>`i` nor `k`>`i`>`j`
# <br>
# The cyclic tour is built from three segments: `abc`
# <br>
# segment `a` = `[k+1 .. i]` (could be `[0..i]` & `[k+1..N-1]`)
# <br>
# segment `b` = `[i+1 .. j]`
# <br>
# segment `c` = `[j+1 .. k]`
# <br>
# `a'` means reversed segment `a`, same thing for `b'` and `c'`
# In[ ]:
def Make_3_Opt_Move(tour,i, j, k, optCase):
N = len(tour)
# IDENTITY
# nothing to do, the tour remains without changes
if optCase == "opt3_case_0":
return
# 2-OPT MOVES
# one of the three links is removed and added again
elif optCase == "opt3_case_1":# a'bc = a[bc]'
Reverse_Segment(tour, (k+1) % N, i)
elif optCase == "opt3_case_2": # abc'
Reverse_Segment(tour, (j+1) % N, k)
elif optCase == "opt3_case_3": # ab'c
Reverse_Segment(tour, (i+1) % N, j)
# PURE 3-OPT MOVES
# all three links are removed, then other links between cities added
# A) moves equal to two subsequent 2-opt moves:
elif optCase == "opt3_case_4": # ab'c'
Reverse_Segment(tour, (j+1) % N, k)
Reverse_Segment(tour, (i+1) % N, j)
elif optCase == "opt3_case_5": # a'b'c
Reverse_Segment(tour, (k+1) % N, i)
Reverse_Segment(tour, (i+1) % N, j)
elif optCase == "opt3_case_6": # a'bc'
Reverse_Segment(tour, (k+1) % N, i)
Reverse_Segment(tour, (j+1) % N, k)
# B) move equal to three subsequent 2-opt moves
elif optCase == "opt3_case_7": # a'b'c' (=acb)
# this move can be implemented by reversing all segments
# without changing their order (a'b'c'), that is as a sequence
# of three 2-opt moves:
Reverse_Segment(tour, (k+1) % N, i)
Reverse_Segment(tour, (i+1) % N, j)
Reverse_Segment(tour, (j+1) % N, k)
# # LS_3_Opt
# Optimizes the given tour using 3-opt with or without speedup (only cases with DLB and NeighborList+DLB).
# <br>
# The parameters have the same meaning of the ones in `LS_2_Opt()`.
# In[2]:
def LS_3_Opt(tour, improvement, speedup, No_Of_Neigbors = False):
del tour[len(tour)-1]
N = len(tour)
locallyOptimal = False
if "NeighborList" in speedup:
if No_Of_Neigbors == False:
No_Of_Neigbors = (int)(len(tour)/10)
neighborListLen = min(No_Of_Neigbors, len(tour)-1)#neighbors mustn't exceed the number of cities in the tour
neighbor = Build_Neighbors_Matrix(neighborListLen,N)#neighbor is the matrix of the neighbors
if "DLB" in speedup:
DontLook = {} #it is a dictionary where the key is the city and the value is True or False
for i in range(1,len(tour)+1):
DontLook[i] = False#set DLB off
while not locallyOptimal:
locallyOptimal = True
for basePos in range(0,N):
if "DLB" in speedup:
baseCity = tour[basePos]
if isDLB_on(DontLook, baseCity):
continue
# here there is the call to the function depending on the speedup used
if "NeighborList+DLB" in speedup:
improved = One_City_3_Opt_ND(tour, basePos, neighbor, neighborListLen, DontLook, improvement)
elif "DLB" in speedup:
improved = One_City_3_Opt_DLB(tour, basePos, DontLook, improvement)
elif "False" in speedup:
improved = One_City_3_Opt(tour, basePos, improvement)
if not improved:
if "DLB" in speedup:
Set_DLB_on(DontLook, baseCity)
else:
locallyOptimal = False
tour.append(tour[0])
return tour
# # One_City_3_Opt
# Shortens the tour by repeating 3-opt moves until no improvement can be done: in every iteration looks for and applies the move that gives maximal length gain (if used the Best improvement) or apply the first move that is acceptable (if used the First improvement).
# <br>
# It is the basic version, without any speedup.
# <br>
# The idea is the same used in `One_City_2_Opt()`, but taking into account another couple of cities, `Z1` and `Z2`, (obtained with another cycle for), to use `Gain_From_3_Opt()` and `Make_3_Opt_Move()`.
# In[ ]:
def One_City_3_Opt(tour, basePos, improvement):
improved = False
if improvement == "Best":
locallyOptimal = True
bestMove = {"gain":0,"i":0,"j":0,"k":0,"optCase":0} # it has to save also the optCase for which the gain is better than the last one, to understand how to do, at the end, the 3-opt move.
N = len(tour)
i = basePos
X1 = tour[i]
X2 = tour[(i+1) % N]
for counter_2 in range(1, N-2):
j = (i + counter_2) % N
Y1 = tour[j]
Y2 = tour[(j+1) % N]
for counter_3 in range(counter_2+1,N):
k = (i + counter_3) % N
Z1 = tour[k]
Z2 = tour[(k+1) % N]
for optCase in ["opt3_case_3", "opt3_case_6", "opt3_case_7"]: # it checks only these three cases, because the others are equals to some of these.
gainExpected = Gain_From_3_Opt(X1, X2, Y1, Y2, Z1, Z2, optCase)
if improvement == "First":
if gainExpected > 0:
Make_3_Opt_Move(tour, i, j, k, optCase)
improved = True
return improved
else:
if gainExpected > bestMove["gain"]:
bestMove = {"gain":gainExpected,"i":i,"j":j,"k":k,"optCase":optCase}
locallyOptimal = False
if improvement == "Best":
if not locallyOptimal:
Make_3_Opt_Move(tour, bestMove["i"], bestMove["j"], bestMove["k"], bestMove["optCase"])
improved = True
return improved
# # One_City_3_Opt_DLB
# Shortens the tour by repeating 3-opt moves until no improvement can be done.
# <br>
# This is the version using the DLB speedup, so there is as parameter also `DontLook`, with the same meaning of the 2-opt case.
# In[ ]:
def One_City_3_Opt_DLB(tour, basePos, DontLook, improvement):
N = len(tour)
improved = False
if improvement == "Best":
locallyOptimal = True
bestMove = {"gain":0,"i":0,"j":0,"k":0,"optCase":0}
for direction in ["forward", "backward"]:
if direction == "forward":
i = basePos
else:
i = (N + basePos - 1) % N
X1 = tour[i]
X2 = tour[(i+1) % N]
if isDLB_on(DontLook,X1) or isDLB_on(DontLook,X2):
continue
for counter_2 in range(0, N):
j = counter_2 # second cut after j
Y1 = tour[j]
Y2 = tour[(j+1) % N]
if (X1 == Y1) or (X2 == Y1) or (Y2 == X1):
continue
# examine 2-opt move (opt3_case_1, opt3_case_2 and opt3_case_3, that are all equal to opt3_case_1, so it saves at the end it)
gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
if improvement == "First":
if gainExpected > 0:
Set_DLB_off(DontLook, [X1, X2, Y1, Y2])
Make_3_Opt_Move(tour, i, j, j, "opt3_case_1")
improved = True
return improved
else:
if gainExpected > bestMove["gain"]:
bestMove = {"gain":gainExpected,"i":i,"j":j,"k":j,"optCase":"opt3_case_1"}
locallyOptimal = False
for counter_3 in range(0,N):
k = counter_3
Z1 = tour[k]
Z2 = tour[(k+1) % N]
if (X1 == Z1) or (Y1 == Z1):
continue
if not Between(i, j, k):
continue
# examine pure 3-opt cases
for optCase in ["opt3_case_6", "opt3_case_7"]:
gainExpected = Gain_From_3_Opt(X1, X2, Y1, Y2, Z1, Z2, optCase)
if improvement == "First":
if gainExpected > 0:
improved = True
Set_DLB_off(DontLook, [X1, X2, Y1, Y2, Z1, Z2])
Make_3_Opt_Move(tour, i, j, k, optCase)
return improved
else:
if gainExpected > bestMove["gain"]:
bestMove = {"gain":gainExpected,"i":i,"j":j,"k":k,"optCase":optCase}
locallyOptimal = False
if improvement == "Best":
if notLocallyOptimal:
improved = True
Set_DLB_off(DontLook, [X1, X2, Y1, Y2])
if bestMove["optCase"] == "opt3_case_6" or bestMove["optCase"] == "opt3_case_7":
Set_DLB_off(DontLook, [Z1, Z2])
Make_3_Opt_Move(tour, bestMove["i"], bestMove["j"], bestMove["k"], bestMove["optCase"])
return improved
# # One_City_3_Opt_ND
# Shortens the tour by repeating 3-opt moves until no improvement can be done.
# <br>
# This is the version using the NeighborList+DLB speedup, with some semplifications.
# <br>
# The parameters has the same meaning that they have in `One_City_2_Opt_NDR()`.
# In[ ]:
def One_City_3_Opt_ND(tour, basePos, neighbor, numberOfNeigbors, DontLook, improvement):
N = len(tour)
improved = False
if improvement == "Best":
locallyOptimal = True
bestMove = {"gain":0,"i":0,"j":0,"k":0,"optCase":0}
for direction in ["forward", "backward"]:
if direction == "forward":
i = basePos
else:
i = (N + basePos - 1) % N
X1 = tour[i]
X2 = tour[(i+1) % N]
for neighbor_1 in range(0, numberOfNeigbors):
# new edges in optCase=6: *X1-Y2*, Y1-Z1, X2-Z2
# new edges in optCase=7: *X1-Y2*, X2-Z1, Y1-Z2
Y2 = neighbor[X1][neighbor_1]
j = (tour.index(Y2) + N - 1) % N
Y1 = tour[j]
if (Y1 != X1) and (Y1 != X2):
gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
if improvement == "First":
if gainExpected > 0:
improved = True
Set_DLB_off(DontLook, [X1, X2, Y1, Y2])
Make_3_Opt_Move(tour, i, j, j, "opt3_case_1")
return improved
else:
if gainExpected > bestMove["gain"]:
locallyOptimal = True
bestMove = {"gain":gainExpected,"i":i,"j":j,"k":j,"optCase":"opt3_case_1"}
for neighbor_2 in range(0, numberOfNeigbors):
# new edges in optCase=6: X1-Y2, *Y1-Z1*, X2-Z2
Z1_6 = neighbor[Y1][neighbor_2]
k_6 = tour.index(Z1_6)
Z2_6 = tour[(k_6 + 1) % N]
# new edges in optCase=7: X1-Y2, X2-Z1, *Y1-Z2*
Z2_7 = neighbor[Y1][neighbor_2]
k_7 = (tour.index(Z2_7) + N - 1) % N
Z1_7 = tour[k_7]
if Between(i, j, k_6):
gainExpected = Gain_From_3_Opt(X1, X2, Y1, Y2, Z1_6, Z2_6,"opt3_case_6")
if improvement == "First":
if gainExpected > 0:
improved = True
Set_DLB_off(DontLook, [X1, X2, Y1, Y2, Z1_6, Z2_6])
Make_3_Opt_Move(tour, i, j, k_6, "opt3_case_6")
return improved
else:
if gainExpected > bestMove["gain"]:
locallyOptimal = False
bestMove = {"gain":gainExpected,"i":i,"j":j,"k":k_6,"optCase":"opt3_case_6"}
if Between(i, j, k_7):
gainExpected = Gain_From_3_Opt(X1, X2, Y1, Y2, Z1_7, Z2_7,"opt3_case_7")
if improvement == "First":
if gainExpected > 0:
improved = True
Set_DLB_off(DontLook, [X1, X2, Y1, Y2, Z1_7, Z2_7])
Make_3_Opt_Move(tour, i, j, k_7, "opt3_case_7")
return improved
else:
if gainExpected > bestMove["gain"]:
locallyOptimal = False
bestMove = {"gain":gainExpected,"i":i,"j":j,"k":k_7,"optCase":"opt3_case_7"}
if improvement == "Best":
if notLocallyOptimal:
improved = True
Set_DLB_off(DontLook,
[ tour[bestMove["i"]], tour[(bestMove["i"] + 1) % N],
tour[bestMove["j"]], tour[(bestMove["j"] + 1) % N],
tour[bestMove["k"]], tour[(bestMove["k"] + 1) % N] ])
Make_3_Opt_Move(tour, bestMove["i"], bestMove["j"], bestMove["k"],bestMove["optCase"])
return improved
# # Make_Segment_Shift_Move
# Shifts the segment of `tour`: cities from `tour[i+1]` to `tour[j]` from their current position to position after current city `tour[k]`, that is between cities `tour[k]` and `tour[k+1]`.
# <br>
# `k`, `k+1` must not be within the segment `[i+1..j]`
# <br>
# (it is the same that to do the opt_case_7 of 3Opt).
# <br>
# It is used if used the Or-opt method.
# In[ ]:
def Make_Segment_Shift_Move(tour,i, j, k):
N = len(tour)
Reverse_Segment(tour, (k+1) % N, i)
Reverse_Segment(tour, (i+1) % N, j)
Reverse_Segment(tour, (j+1) % N, k)
# # Gain_From_Segment_Shift
# Gain of tour length which can be obtained by performing Segment Shift.
# <br>
# Cities from `X2` to `Y1` would be moved from its current position, between `X1` and `Y2`, to position between cities `Z1` and `Z2`.
# <br>
# `X2` is the successor of `X1`, `Y2` of `Y1`, `Z2` of `Z1`.
# <br>
# It is used if used the Or-opt method.
# In[ ]:
def Gain_From_Segment_Shift(X1, X2, Y1, Y2, Z1, Z2):
del_Length = distance(X1, X2) + distance(Y1, Y2) + distance(Z1, Z2)
add_Length = distance(X1, Y2) + distance(Z1, X2) + distance(Y1, Z2)
result = del_Length - add_Length
return result
# # LS_Or_Opt
# Optimizes the given tour using Or-opt without speedup
# In[ ]:
def LS_Or_Opt(tour, improvement):
del tour[len(tour)-1]
N = len(tour)
locallyOptimal = False
while not locallyOptimal:
locallyOptimal = True
for basePos in range(0,N):
baseCity = tour[basePos]
improved = One_City_Or_Opt(tour, basePos, improvement)
if improved:
locallyOptimal = False
tour.append(tour[0])
return tour
# # One_City_Or_Opt
# Optimizes the given tour using Or-opt.
# <br>
# Shortens the tour by repeating Segment Shift moves for segment with length equal 3, 2, 1 until no improvement can by done.
# In[ ]:
def One_City_Or_Opt(tour, basePos, improvement):
improved = False
if improvement == "Best":
locallyOptimal = True
bestMove = {"gain":0,"i":0,"j":0,"k":0}
N = len(tour)
for segmentLen in [3,2,1]:
i = basePos
X1 = tour[i]
X2 = tour[(i + 1) % N]
j = (i + segmentLen) % N
Y1 = tour[j]
Y2 = tour[(j + 1) % N]
for shift in range(segmentLen+1, N):
k = (i + shift) % N
Z1 = tour[k]
Z2 = tour[(k + 1) % N]
gainExpected = Gain_From_Segment_Shift(X1, X2, Y1, Y2, Z1, Z2)
if improvement == "First":
if gainExpected > 0:
Make_Segment_Shift_Move(tour, i, j, k)
improved = True
return improved
else: