Wednesday, April 26, 2017

4-opt* – general idea, part 2

The second crucial part of 4-opt* idea is to examine only a specific cases of potentially improving moves.

Any k-opt move that improves a tour move must fulfill the condition:

addLength < delLength

Sum of length of links added must be less than sum of links removed from tour. It is therefore necessary (though insufficient) that at least one of added links is shorter than one of removed links. Lengths of all four links removed in 4-opt* procedure are (see schemes below):

delLen_1 = distance(tour[i], tour[(i+1) mod N])
delLen_2 = distance(tour[j], tour[(j+1) mod N])
delLen_3 = distance(tour[k], tour[(k+1) mod N])
delLen_4 = distance((k+1) mod N], tour[(k+2) mod N])

We take the length of the longest of them:

delLenMax = max(delLen_1, max (delLen_2, max(delLen_3, delLen_4)))

We determine that to reconnect the cities we always add the link to city in position k+1 either from city in position i+1 or from city in j and that we take the shorter of these two links:

addLen_1 = distance(tour[(i+1) mod N], tour[(k+1) mod N])
addLen_2 = distance(tour[(j) mod N], tour[(k+1) mod N])

addLenMin = min(addLen1, addLen2)

Possible cases of reconnection and gain from them are tested only if:

addLenMin < delLenMax

This test guarantees that at least this added link is shorter than at least one of the links removed. If the condition is not met, then cases of 4-opt* move are not examined. This way we examine only one of two classes of moves:

Fixed links in 4-opt* moves
Class A Class B
i i+1 j j+1 k k+1 k+2 i i+1 j j+1 k k+1 k+2

So for given i, j, k we consider only one class of reconnections: A or B. Either class have only 8 cases of reconnection (see below), as opposed to 48 cases in standard 4-opt. Moreover, since the scheme of class B is symmetrical to the scheme of class A, then cases of reconnection for class B will be also symmetrical to cases for class A.

4-opt* move: reconnection cases of class A
2-opt move
3-opt moves
4-opt moves

Tuesday, April 25, 2017

4-opt* – general idea, part 1

Since full 4-opt is rather complex and time comsuming, Renaud, Boctor and Laporte (1996) proposed a tour improving procedure caled 4-opt*, based on using a specific subset of 4-opt moves. Its main goal is to extend searching for improving moves beyond 2-opt moves, but keep this searching fast.

Elements of 4-opt move Elements of 4-opt* move
i j m k i j k

As we can see, the first difference between standard 4-opt and 4-opt* is that while the scheme of general 4-opt move involves four non-overlapping segments with eight endpoints, the scheme of 4-opt* involves only three segments and seven points; fourth segment is reduced to one point between endpoints of two other segments. The layout for standard 4-opt requires four parameters (i, j, k, m), but for 4-opt* it is only three parameters (i, j, k). This allows us to reduce number of corresponding nested loops in searching for an improvement, each of O(N). (There is additional advantage: reduction of possible cases of reconnection. In 4-opt move they result from its four parameters, but 4-opt* cases result from only three parameters. Therefore number of cases (types of reconnection) of a move in 4-opt* is reduced compared to the number of cases in standard 4-opt. For example double bridge cannot be made in 4-opt*).

Moreover, value of j is obtained from: j = i + u, where u <= w and w is some arbitrary number chosen by the developer or a user and usually relatively small compared to N (for example: 10, 15 or 20). Therefore for given u the procedure considers only O(N2) choices, changing values of i and k. This way we consider links at the ends of two not overlaping chains: from position i to i+u+1, and from position k to k+2.

i i+1 i+u i+u+1 k k+1 k+2

The 4-opt* algorithm starts with u=1 and considers all choices of such two chains of cities. If an improvement is found then it is immediately applied and searching is repeated. When no improvement is found then u is incremented by 1 and the same process is repeated. When the process of improvimg for u==w is finished, then u is reset to 1 and the same procedure is applied once again, until there is no improvement in such full round.

improved = false
while not improved:
  u = 1
  improved = false
  while u <= w:
    block two_loops:
      for i in ... :
        j = i + u
        ...
        for k in ... :
          ...
          # additional speed-up
          ...
          for case_of_move in ... :
            gain = Gain_From_4optAsterisk(i, j, k, case_of_move)
            if gain > 0:
              Make_4optAsterisk_Move(i, j, k, case_of_move)
              improved = true
              break two_loops
    #end_block two_loops
    u = u + 1
  #end_while u loop

Saturday, April 22, 2017

4-opt and Double Bridge

Replacing 3-optic moves in the local optimization with 4-opt moves increases the time complexity the algorithm from O(N3) to O(N4), or, in the case of using lists of candidates (neighbors), from O(N * m2) to O (N * m3), where m is number of neighbors in the list.

However, the problem does not lie only in much greater time of execution. Applying 4-opt moves gives tours only slightly shorter than those from 3-optimization. Moreover, while for 3-opt there are 8 types (cases) of move, for 4-opt this number increases to 48, so considering all of them becomes much more complicated. As in 3-opt, one of them gives the tour identical with the tour before the move. Analogically to 3-opt, many of them are equal to a single 2-opt or a single 3-opt move; there are only 25 pure 4-opt moves. But something new appears in 4-opt moves: most known 4-opt move, called double bridge move.

The simplicity and symmetry of the double bridge scheme make it particularly easy to recognize and remember:

Double bridge, as the simplest non-sequential move (idea of so called sequenctial moves is described later), plays important role even in algorithms that do not use full 4-optimization. By definition given tour after full 3-optimization cannot be further improved by 3-opt, but it does not mean that there are no similar but shorter tours. Instead of trying to search or build such tours from scratch, we often use 3-optimized tour and perturb it to obtain a new good initial tour, that we can then optimize, perhaps getting better result. Double bridge is used as simple and effective perturbation.

Double bridge and other non-sequential 4-opt moves are discussed in detail later.

Wednesday, April 19, 2017

Dynasearch – the idea of set of independent moves

So far we have been dealing with the simplest methods in which a single optimization step is a single improving move of given type (for example 2-opt, 2.5-opt, 3-opt). In more advanced methods, a single step is an improving set of moves. An example of such algorithms is the dynasearch method.

In the dynasearch approach we use dynamic programming algorithm to look for the best set consisting of independent improving moves. Two moves are called independent when, regardless of the order in which they are applied, the resulting tour is the same. For example two 2-opt moves are independent when they do not overlap (their ranges do not overlap):

independent movesnon-independent moves

This means that none of the endpoints of the second move cannot be between the endpoints of the first move.

So for pairs in which the two positions are in ascending order:

0 <= i1 < ... < j1 <= N-1, where i1+1 != j1 and j1+1 != i1

and

0 <= i2 < ... < j2 <= N-1, where i2+1 != j2 and j2+1 != i2

the two 2-opt moves are independent if:

j1 < i2 or j2 < i1

(reversed segment) i1 j1 i2 (reversed segment) i1 j1 j2

2-opt dynasearch on average is faster than traditional best improvement approach and gives slightly better results. Unfortunatelly, running times are far longer than those for 3-opt with neighbor lists, which gives better results and is much simpler to implement.

Beware

But what when the endpoints of a move are not in ascending order of indices? Let us consider the following:

0 N-1 N-2 1 3 4

If the first move is defined by the pair (3, N-2) then the moves are not independent, because all links involved in second move would be affected by changes made by the first move (case A). But if the first move is defined by the pair (N-2, 3) then the two moves are independent (case B):

case A
reversing the segment
without the city on position 0
case B
reversing the segment
with the city on position 0
0 N-1 j1 i1 0 N-1 i1 j1

How is it possible that in one case the moves are independent and in the other they are not, when they seem to produce the same tour, differing only in orientation?

The main difference and the problem with case A is that the first move by reversing a segment simultaneously changes the tour positions (indices) of the cities it contains, so after this move i2, i2+1, j2, j2+1 no longer point at the same cities and links they pointed at before the move. So if we found the second move and set up the values for i2, j2 independently from any other move, not assuming that the move (i1, j1) would be apllied before, then after this first move these values are wrong. This does not happen in case B.

But on the other hand, unfortunatelly in case B we cannot apply the rule:

j1 < i2 or j2 < i1

to check independence of the two moves, because the inequity i1 < j1 is not satisfied.

More general condition for independent 2-opt moves is:

Two moves are independend when every city involved in one move comes before every city involved in second move or vice versa.

Monday, April 17, 2017

Alternative for DLB

Instead of don't look bits speedup we can use an array indexed by city numbers, in which each element contains length of tour after improving from given city. The algorithm using has the following scheme:

var
    tourLen: tourLen  # current length of tour
    tourLenArr: array[City_Number, tourLen]

  # calculate initial length of tour
  tourLen = distance(tour[N-1], tour[0])
  for pos in 0 .. N-2:
    tourLen = tourLen + distance(tour[pos], tour[pos+1])

  # initialise array of tour lengths with some arbitrary
  # value different from current value of 'tourLen'
  for city in 0 .. N-1:
    tourLenArr[city] = 1 # 

  # main loop
  for pos in 0 .. N-1:
    baseCity = tour[pos]
    if (tourLen == tourLenArr[baseCity]):
       continue

    improved = Improve(baseCity)  # MUST update 'tourLen' value!

    tourLenArr[baseCity] = tourLen
  #end_for

Saturday, April 15, 2017

Stop search after encountering the successor

Less popular speed-up is using search cut after encountering a successor of given city when iterating on its neighbors. The idea is based on the observation that lead to 2-opt fixed radius search.

We consider neighbours of city X1 to find a good candidate for Y1, the first of endpoints of second link to remove. If we make a 2-opt move, then Y1 would be the new tour successor of X1, so it should not be in distance from X1 greater then is X2, the current successor. Otherwise, the new link would be longer than the current one. Since we consider neighbors of X1 in ascending order of their distance from this city, encountering X2 during search means that all subsequent neighbors would give longer links to X1.

...
  for neighbor_1 in 0 .. numberOfNeigbors-1:
    Y2 = neighbor[X1][neighbor_1]
    j  = (position[Y2] + N - 1) mod N
    Y1 = tour[j]

    if (Y1 == X2):  # speed-up
      break
...

Analogous condition can be used in 3-opt, also for Z1, although justification in the case of 3-opt is not so strong, even when forcing only Y1 != X2.

Note that this condition rejects some valid 3-opt moves, Node Shifts that are used in 2.5-opt:

X1 Y1=X2 Z1
X1 Y1=X2 Z1

This speed-up is very effective and can be used instead of popular don’t look bits solution.

The full code for simple 3-opt and the speed-up in one loop only is as follows:

proc One_City_3_Opt_NC(tour: var Tour_Array;
                       basePos: Tour_Index;
                       neighbor: Neighbor_Matrix;
                       position: var City_Position_In_Tour;
                       numberOfNeigbors: int): bool =
  type
    Move3 = object
      i, j, k: Tour_Index
      optCase: Reconnection_3_optCase
      gain: Length_Gain
  var
    improved: bool
    i, j, k: Tour_Index
    X1, X2, Y1, Y2, Z1, Z2: City_Number
    k_6, k_7:  Tour_Index
    Z1_6, Z2_6, Z1_7, Z2_7: City_Number
    gainExpected: Length_Gain
    goodMove: Move3
  
  improved = false
  goodMove.gain = 0
  block three_loops:
    
    for direction in [forward, backward]:
      if direction == forward:
        i  = basePos
      else:
        i  = (N + basePos - 1) mod N
      X1 = tour[i]
      X2 = tour[(i+1) mod N]
 
      for neighbor_1 in 0 .. numberOfNeigbors-1:
        # 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  = (position[Y2] + N - 1) mod N
        Y1 = tour[j]

        if (Y1 != X1) and (Y1 != X2):
          gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
          if gainExpected > goodMove.gain:
            improved = true
            goodMove = Move3(i: i,  j: j,  k: j,
                             optCase: opt3_case_1, gain: gainExpected)

        if (Y1 == X2):  # NOTE
          break

        for neighbor_2 in 0 .. numberOfNeigbors-1:
          # new edges in optCase=6: X1-Y2, *Y1-Z1*, X2-Z2
          Z1_6 = neighbor[Y1][neighbor_2]
          k_6  = position[Z1_6]
          Z2_6 = tour[(k_6 + 1) mod N]
          # new edges in optCase=7: X1-Y2, X2-Z1, *Y1-Z2*
          Z2_7 = neighbor[Y1][neighbor_2]
          k_7  = (position[Z2_7] + N - 1) mod 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 gainExpected > goodMove.gain:
              improved = true
              goodMove = Move3(i: i,  j: j,  k: k_6,
                               optCase: opt3_case_6, gain: gainExpected)

          if Between(i, j, k_7):
            gainExpected = Gain_From_3_Opt(X1, X2, Y1, Y2, Z1_7, Z2_7,
                                           opt3_case_7)
            if gainExpected > goodMove.gain:
              improved = true
              goodMove = Move3(i: i,  j: j,  k: k_7,
                               optCase: opt3_case_7, gain: gainExpected)

  if improved:
    Make_3_Opt_Move(tour,
                    goodMove.i,  goodMove.j,  goodMove.k,
                    goodMove.optCase,  position)
  result = improved
proc LS_3_Opt_NC(tour: var Tour_Array;
                 neighbor: Neighbor_Matrix;
                 neighborListLen: int = N-1) =
  var
    locallyOptimal: bool = false
    improved: bool
    baseCity: City_Number
    position: City_Position_In_Tour

  let numberOfNeigbors = min(neighborListLen, len(neighbor[0]))
  position = Create_Position_In_Tour(tour)
  
  while not locallyOptimal:
    locallyOptimal = true
    for basePos in 0 .. N-1:
      baseCity = tour[basePos]
      improved = One_City_3_Opt_NC(tour, basePos, neighbor, position,
                                   numberOfNeigbors)
      if improved:
        locallyOptimal = false

Some simple statistics

Relative time, best length, average length and worst length for all tours created by NN heuristic and improved by some 2-opt and 3-opt algorithms. Random planar problems, N=200.
Problem # Method Time Best Avg Worst
1NN100100.00106.52114.46
NN + 2opt472188.1790.8994.52
NN + 2opt Take Best511987.5389.0991.25
NN + 3opt-ND(24)585186.4487.9489.65
NN + 3opt-NC(24)358985.7687.8989.86
2NN100100.00104.27110.57
NN + 2opt362390.3092.4595.40
NN + 2opt Take Best388989.0790.6792.78
NN + 3opt-ND(24)555188.3189.5992.20
NN + 3opt-NC(24)295988.3689.4691.41
3NN100100.00110.24122.10
NN + 2opt383991.0394.6197.58
NN + 2opt Take Best445090.3491.7094.47
NN + 3opt-ND(24)580888.7290.6392.96
NN + 3opt-NC(24)363488.8290.7192.56
4NN100100.00106.87114.35
NN + 2opt395587.5390.6494.81
NN + 2opt Take Best452187.1089.3391.84
NN + 3opt-ND(24)578585.5387.3590.17
NN + 3opt-NC(24)342385.9987.4489.56
5NN100100.00106.86113.89
NN + 2opt372390.1693.6196.94
NN + 2opt Take Best442189.7992.1895.17
NN + 3opt-ND(24)598288.6190.6192.40
NN + 3opt-NC(24)369188.5790.6694.46
6NN100100.00106.80114.76
NN + 2opt485385.0388.1692.81
NN + 2opt Take Best508785.0786.9591.02
NN + 3opt-ND(24)588283.1984.8687.59
NN + 3opt-NC(24)382383.6985.1188.18
7NN100100.00104.23110.78
NN + 2opt448786.0088.6792.58
NN + 2opt Take Best528784.9286.9388.92
NN + 3opt-ND(24)588283.9285.5988.45
NN + 3opt-NC(24)392383.6385.6088.26
8NN100100.00109.86116.72
NN + 2opt528586.0189.9293.72
NN + 2opt Take Best595186.0489.0691.54
NN + 3opt-ND(24)598584.5786.8890.18
NN + 3opt-NC(24)352384.8386.7489.65
9NN100100.00104.99110.15
NN + 2opt432187.5090.4293.91
NN + 2opt Take Best472187.0188.9690.70
NN + 3opt-ND(24)595185.9787.4189.39
NN + 3opt-NC(24)405585.5187.3789.05
10NN100100.00106.83115.85
NN + 2opt432190.2992.6896.71
NN + 2opt Take Best472189.2690.5092.66
NN + 3opt-ND(24)571789.0890.3592.09
NN + 3opt-NC(24)382389.1590.2491.92

Tuesday, April 11, 2017

3-opt with min-max lenght of link – example

proc One_City_3_Opt_NDM(tour: var Tour_Array;
                        basePos: Tour_Index;
                        neighbor: Neighbor_Matrix;
                        position: var City_Position_In_Tour;
                        numberOfNeigbors: int;
                        DontLook: var DLB_Array;
                        minLenInSet: Length;
                        maxLenInTour: var Length): bool =
  type
    Move3 = object
      i, j, k: Tour_Index
      optCase: Reconnection_3_optCase
      gain: Length_Gain
  var
    improved: bool
    i, j, k: Tour_Index
    ii, jj, kk: Tour_Index
    X1, X2, Y1, Y2, Z1, Z2: City_Number
    A1, A2, B1, B2, C1, C2: City_Number
    gainExpected: Length_Gain
    goodMove: Move3
    d1, d2: Length
  
  improved = false
  goodMove.gain = 0
  block three_loops:
    
    for direction in [forward, backward]:
      if direction == forward:
        i  = basePos
      else:
        i  = (N + basePos - 1) mod N
      X1 = tour[i]
      X2 = tour[(i+1) mod N]
 
      for neighbor_1 in 0 .. numberOfNeigbors-1:
        Y2 = neighbor[X1][neighbor_1]
        j  = (position[Y2] + N - 1) mod N
        Y1 = tour[j]

        if (Y1 != X1) and (Y1 != X2):
          gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
          if gainExpected > goodMove.gain:
            improved = true
            goodMove = Move3(i: i,  j: j,  k: j,
                             optCase: opt3_case_1, gain: gainExpected)
        else:
          continue
          
        d2 = distance(X1, Y1) + minLenInSet
        if d2 + minLenInSet > distance(X1, X2) + 2 * maxLenInTour:
           break
        d1 = distance(X1, X2) + distance(Y1, Y2) + maxLenInTour
        if d2 + minLenInSet > d1:
           continue

        for neighbor_2 in 0 .. numberOfNeigbors-1:
          Z2 = neighbor[X1][neighbor_2]
          k  = (position[Z2] + N - 1) mod N
          Z1 = tour[k]

          if (Z2 == Y2) or (Z1 == Y2) or
             (Z1 == X1) or (Z1 == X2) or
             (Y1 == X1) or (Y1 == X2):
              continue

          if d2 + distance(Y1, Z1) > d1:
            break

          if Between(i, j, k):
            jj = j
            kk = k
            (A1, A2, B1, B2, C1, C2) = (X1, X2, Y1, Y2, Z1, Z2)
          else:
            jj = k
            kk = j
            (A1, A2, B1, B2, C1, C2) = (X1, X2, Z1, Z2, Y1, Y2)

          for optCase in opt3_case_4 .. opt3_case_7:
            gainExpected = Gain_From_3_Opt(A1, A2, B1, B2, C1, C2,
                                           optCase)
            if gainExpected > goodMove.gain:
              improved = true
              goodMove = Move3(i: i,  j: jj,  k: kk,
                               optCase: optCase, gain: gainExpected)

  if improved:
    A1 = tour[goodMove.i]
    A2 = tour[(goodMove.i + 1) mod N]
    B1 = tour[goodMove.j]
    B2 = tour[(goodMove.j + 1) mod N]
    C1 = tour[goodMove.k]
    C2 = tour[(goodMove.k + 1) mod N]
    Set_DLB_off(DontLook, [A1, A2, B1, B2, C1, C2])
    Make_3_Opt_Move(tour,
                    goodMove.i,  goodMove.j,  goodMove.k,
                    goodMove.optCase,  position)

    case goodMove.optCase
    of opt3_case_1:
      maxLenInTour = max( maxLenInTour, max(distance(A1,B1),
                          distance(A2,B2)) )
    of opt3_case_4:
      maxLenInTour = max( maxLenInTour, max(distance(A1,B1),
                          max(distance(A2,C1),
                              distance(B2,C2)) ) )
    of opt3_case_5:
      maxLenInTour = max( maxLenInTour, max(distance(A1,C1),
                          max(distance(A2,B2),
                              distance(B1,C2)) ) )
    of opt3_case_6:
      maxLenInTour = max( maxLenInTour, max(distance(A1,B2),
                          max(distance(A2,C2),
                              distance(B1,C1)) ) )
    of opt3_case_7:
      maxLenInTour = max( maxLenInTour, max(distance(A1,B2),
                          max(distance(A2,C1),
                              distance(B1,C2)) ) )
    else:
      echo "Should not happen: ", goodMove.optCase
      maxLenInTour = distance(tour[0], tour[N-1])
      for pos in 0 .. N-2:
        maxLenInTour = max(maxLenInTour,
                           distance(tour[pos], tour[pos+1]))
    #end goodMove.optCase

  result = improved
proc LS_3_Opt_NDM(tour: var Tour_Array;
                  neighbor: Neighbor_Matrix;
                  neighborListLen: int = N-1) =
  ## Optimizes the given tour using 3-opt with neighbor lists
  ## Don't Look Bits (DLB) and min-max Links
  var
    locallyOptimal: bool = false
    improved: bool
    DontLook: DLB_Array
    baseCity: City_Number
    position: City_Position_In_Tour
    maxLenInTour: Length

  let numberOfNeigbors = min(neighborListLen, len(neighbor[0]))
  position = Create_Position_In_Tour(tour)

#[
  # NOTE: minLenInSet is constant for the dataset
  minLenInSet = high(Length)
  for city_A in 0 .. N-1:
    for city_B in (city_A+1) .. N-1:
      minLenInSet = min(minLenInSet, distance(city_A, city_B))
]#

  maxLenInTour = distance(tour[0], tour[N-1])
  for pos in 0 .. N-2:
    maxLenInTour = max(maxLenInTour, distance(tour[pos], tour[pos+1]))

  Set_DLB_off(DontLook, tour)
  
  while not locallyOptimal:
    locallyOptimal = true
    for basePos in 0 .. N-1:
      baseCity = tour[basePos]
      if isDLB_on(DontLook, baseCity):
        continue
      improved = One_City_3_Opt_NDM(tour, basePos, neighbor, position,
                                    numberOfNeigbors, DontLook,
                                    minLenInSet, maxLenInTour)
      if not improved:
        Set_DLB_on(DontLook, baseCity)
      else:
        locallyOptimal = false

Saturday, April 8, 2017

Minimum and maximum length of link as a speed-up

Any 2-opt or 3-opt move that improves a tour move must fulfill the condition:

addLength < delLength

Sum of length of links added must be less than sum of links removed from tour.

In the case of 2-opt the inquity can be written as:

We can speed-up the search for improving moves by avoiding considering all elements of this inequity.

One method of doing this is fixed radius search. We search for Y1 (or for Y2) inside a the circle centered at X1 and radius to equal the distance between X1 and X2. Moves with both Y's outside the circle do not improve the tour. Unfortunatelly, this method requires symmetrical searching, which could be inconvenient, especiallly when used with some other speed-ups.

However still: in an improving move one or two longer links are removed and then one or two shorter links are added. Suppose that we know the two distances:

then the inquity has the form:

distance(X1, Y1) + link_added_len < distance(X1, X2) + link_deleted_len

Situation is the better, the bigger link_deleted_len is and the smaller link_added_len is. The maximum value for link_deleted_len is equal to maxLenInTour, which is the length of the longest link in current tour. The mininum value for link_added_len is equal to minLenInSet, which is the length of the shortest of all possible links beetwen cities in the problem (for simplicity we do not exclude links already in current tour, this way minLenInSet is a constant value for the dataset).

var
  minLenInSet, maxLenInTour: Length

  # NOTE: minLenInSet is constant for the dataset
  minLenInSet = high(Length)
  for city_A in 0 .. N-1:
    for city_B in (city_A+1) .. N-1:
      minLenInSet = min(minLenInSet, distance(city_A, city_B))

  maxLenInTour = distance(tour[0], tour[N-1])
  for pos in 0 .. N-2:
    maxLenInTour = max(maxLenInTour, distance(tour[pos], tour[pos+1]))

So if for some Y1 we have got:

distance(X1, Y1) + minLenInSet > distance(X1, X2) + maxLenInTour

then there is no possibility that any 2-opt move with X1, X2 and Y1 could improve the tour, no matter which of the two Y1's tour neighbours we would like to choose for Y2.

Similarilly, we can ommit candidates for 3-opt moves. Consider for example:

distance(X1, Y1) + distance(Y1, Z1) + minLenInSet >
    distance(X1, X2) + distance(Y1, Y2) + maxLenInTour
distance(X1, Y1) + 2 * minLenInSet > 
    distance(X1, X2) + distance(Y1, Y2) + maxLenInTour
distance(X1, Y1) + 2 * minLenInSet >
    distance(X1, X2) + 2 * maxLenInTour

Wednesday, April 5, 2017

3-opt with Neighbor Lists and DLB – simple

Compare the code below with the code for 2-opt with neighbor lists and DLB and notice that this algorithm is simplified, not truly bi-directional version.

proc One_City_3_Opt_ND(tour: var Tour_Array;
                       basePos: Tour_Index;
                       neighbor: Neighbor_Matrix;
                       position: var City_Position_In_Tour;
                       numberOfNeigbors: int;
                       DontLook: var DLB_Array): bool =
  type
    Move3 = object
      i, j, k: Tour_Index
      optCase: Reconnection_3_optCase
      gain: Length_Gain
  var
    improved: bool
    i, j, k: Tour_Index
    X1, X2, Y1, Y2, Z1, Z2: City_Number
    k_6, k_7:  Tour_Index
    Z1_6, Z2_6, Z1_7, Z2_7: City_Number
    gainExpected: Length_Gain
    goodMove: Move3
  
  improved = false
  goodMove.gain = 0
  block three_loops:
    
    for direction in [forward, backward]:
      if direction == forward:
        i  = basePos
      else:
        i  = (N + basePos - 1) mod N
      X1 = tour[i]
      X2 = tour[(i+1) mod N]
 
      for neighbor_1 in 0 .. numberOfNeigbors-1:
        # 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  = (position[Y2] + N - 1) mod N
        Y1 = tour[j]

        if (Y1 != X1) and (Y1 != X2):
          gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
          if gainExpected > goodMove.gain:
            improved = true
            goodMove = Move3(i: i,  j: j,  k: j,
                             optCase: opt3_case_1, gain: gainExpected)

        for neighbor_2 in 0 .. numberOfNeigbors-1:
          # new edges in optCase=6: X1-Y2, *Y1-Z1*, X2-Z2
          Z1_6 = neighbor[Y1][neighbor_2]
          k_6  = position[Z1_6]
          Z2_6 = tour[(k_6 + 1) mod N]
          # new edges in optCase=7: X1-Y2, X2-Z1, *Y1-Z2*
          Z2_7 = neighbor[Y1][neighbor_2]
          k_7  = (position[Z2_7] + N - 1) mod 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 gainExpected > goodMove.gain:
              improved = true
              goodMove = Move3(i: i,  j: j,  k: k_6,
                               optCase: opt3_case_6, gain: gainExpected)

          if Between(i, j, k_7):
            gainExpected = Gain_From_3_Opt(X1, X2, Y1, Y2, Z1_7, Z2_7,
                                           opt3_case_7)
            if gainExpected > goodMove.gain:
              improved = true
              goodMove = Move3(i: i,  j: j,  k: k_7,
                               optCase: opt3_case_7, gain: gainExpected)

  if improved:
    Set_DLB_off(DontLook,
                  [ tour[goodMove.i], tour[(goodMove.i + 1) mod N],
                    tour[goodMove.j], tour[(goodMove.j + 1) mod N],
                    tour[goodMove.k], tour[(goodMove.k + 1) mod N] ])
    Make_3_Opt_Move(tour,
                    goodMove.i,  goodMove.j,  goodMove.k,
                    goodMove.optCase,  position)
  result = improved

Some simple statistics

Relative time, best length, average length and worst length for all tours created by NN heuristic and improved by some 2-opt and 3-opt algorithms. Random planar problems, N=100.
Problem # Method Time Best Avg Worst
1NN100100.00111.12122.44
NN + 2opt136088.9492.5898.91
NN + 2opt Take Best114089.4791.7595.40
NN + 3opt7916687.9989.6192.82
NN + 3opt-ND(24)458687.9989.8694.22
2NN100100.00107.46119.61
NN + 2opt114690.8594.70100.03
NN + 2opt Take Best104688.5992.0997.39
NN + 3opt6947387.2289.8994.23
NN + 3opt-ND(24)458687.2289.8793.04
3NN100100.00106.81117.56
NN + 2opt68188.9293.3497.90
NN + 2opt Take Best88189.3892.2896.22
NN + 3opt6611287.8589.5393.19
NN + 3opt-ND(24)410687.6889.0792.68
4NN100100.00108.58116.96
NN + 2opt146085.6088.5292.28
NN + 2opt Take Best125385.4086.8789.81
NN + 3opt7322683.1484.8087.22
NN + 3opt-ND(24)458683.1585.0187.43
5NN100100.00110.59124.43
NN + 2opt114692.7395.4298.30
NN + 2opt Take Best94091.4393.5897.02
NN + 3opt7239390.9192.3294.66
NN + 3opt-ND(24)416690.9192.2793.67
6NN100100.00107.15118.12
NN + 2opt125389.3592.7198.25
NN + 2opt Take Best93988.7490.8192.73
NN + 3opt6937387.9489.6391.73
NN + 3opt-ND(24)437388.2090.0092.09
7NN100100.00110.20121.48
NN + 2opt125392.3095.3198.94
NN + 2opt Take Best93991.5393.9497.52
NN + 3opt6530690.4191.7494.38
NN + 3opt-ND(24)427390.4191.6393.30
8NN100100.00105.35112.99
NN + 2opt125388.2092.0297.83
NN + 2opt Take Best83388.6990.7295.39
NN + 3opt6698087.6688.7794.75
NN + 3opt-ND(24)427387.6689.3492.54
9NN100100.00107.15115.57
NN + 2opt78192.0895.79100.95
NN + 2opt Take Best87590.9494.0698.73
NN + 3opt7168189.7691.3594.37
NN + 3opt-ND(24)439389.7691.3293.54
10NN100100.00106.56113.89
NN + 2opt125387.0790.8794.98
NN + 2opt Take Best104086.6688.6391.83
NN + 3opt7135386.0187.6490.86
NN + 3opt-ND(24)427386.0187.4191.10

Tuesday, April 4, 2017

2-opt with Neighbor Lists and Best from given City – alternative

Note

This example of code does not contain any new ideas and can be skipped by a reader.

proc One_City_2_Opt_N_Best_From_City(tour: var Tour_Array;
                                     basePos: Tour_Index;
                                     neighbor: Neighbor_Matrix;
                                     position: var City_Position_In_Tour;
                                     numberOfNeigbors: int): bool =
  ## Makes the best of improving moves starting from given city
  type
    Move2 = object
      i, j: Tour_Index
      gain: Length_Gain
  var
    improved: bool
    i, i_pred, i_succ, j, j_pred, j_succ: Tour_Index
    X0, X0_pred, X0_succ, Y0, Y0_pred, Y0_succ: City_Number
    gainExpected: Length_Gain
    bestMove: Move2

  improved = false
  bestMove.gain = 0
  
  i   = basePos
  X0  = tour[i]
  i_pred = (N + i - 1) mod N
  i_succ = (i + 1) mod N
  X0_pred = tour[i_pred]
  X0_succ = tour[i_succ]

  for neighbor_number in 0 .. numberOfNeigbors-1:
    Y0 = neighbor[X0][neighbor_number]    
    j  = position[Y0]
    j_pred = (N + j - 1) mod N
    j_succ = (j + 1) mod N
    Y0_pred = tour[j_pred]
    Y0_succ = tour[j_succ]

    if (X0_succ != Y0) and (Y0_succ != X0):
      gainExpected  = Gain_From_2_Opt(X0, X0_succ, Y0, Y0_succ)
      if gainExpected > bestMove.gain:
        bestMove = Move2(i: i,  j: j,  gain: gainExpected)
        improved = true
    if (X0_pred != Y0) and (Y0_pred != X0):
      gainExpected = Gain_From_2_Opt(X0_pred, X0, Y0_pred, Y0)
      if gainExpected > bestMove.gain:
        bestMove = Move2(i: i_pred,  j: j_pred,  gain: gainExpected)
        improved = true
  if improved:
    Make_2_Opt_Move(tour, bestMove.i, bestMove.j, position)
  result = improved

Monday, April 3, 2017

2-opt with Neighbor Lists and Best from given City

proc One_City_2_Opt_N_Best_From_City(tour: var Tour_Array;
                                     basePos: Tour_Index;
                                     neighbor: Neighbor_Matrix;
                                     position: var City_Position_In_Tour;
                                     numberOfNeigbors: int): bool =
  ## Makes the best of improving moves starting from given city
  type
    Move2 = object
      i, j: Tour_Index
      gain: Length_Gain
  var
    improved: bool
    pos_Y2, i, j: Tour_Index
    X1, X2, Y1, Y2: City_Number
    gainExpected: Length_Gain
    bestMove: Move2

  improved = false
  bestMove.gain = 0
  block two_loops:
    for direction in [forward, backward]:
      if direction == forward:
        i  = basePos
        X1 = tour[i]
        X2 = tour[(i+1) mod N]
      else:
        i  = (N + basePos - 1) mod N
        X2 = tour[i]
        X1 = tour[(i+1) mod N]  # == tour[basePos]

      for neighbor_number in 0 .. numberOfNeigbors-1:
        Y1 = neighbor[X1][neighbor_number]
        if direction == forward:
          j = position[Y1]
          Y2 = tour[(j+1) mod N]
        else:
          j  = (N + position[Y1] - 1) mod N  # pos[Y1] == j+1
          Y2 = tour[j]

        if (X2 == Y1) or (Y2 == X1):
          continue

        gainExpected = Gain_From_2_Opt(X1, X2, Y1, Y2)
        if  gainExpected > bestMove.gain:
          bestMove = Move2(i: i,  j: j,  gain: gainExpected)
          improved = true
  if improved:
    Make_2_Opt_Move(tour, bestMove.i, bestMove.j, position)
  result = improved
proc LS_2_Opt_N_BC(tour: var Tour_Array;
                   neighbor: Neighbor_Matrix;
                   neighborListLen: int = N-1) =
  ## Optimizes tour using 2-opt with neighbor lists and fixed radius,
  ## for each city makes the best of improving moves that starts from it
  var
    locallyOptimal: bool = false
    improved: bool
    position: City_Position_In_Tour

  let numberOfNeigbors = min(neighborListLen, len(neighbor[0]))
  position = Create_Position_In_Tour(tour)

  while not locallyOptimal:
    locallyOptimal = true
    for basePos in 0 .. N-1:
      improved = One_City_2_Opt_N_Best_From_City(tour, basePos,
                                                 neighbor, position,
                                                 numberOfNeigbors)
      if improved:
        locallyOptimal = false

Some simple statistics

Relative time, best length, average length and worst length for all tours created by NN heuristic and improved by some 2-opt algorithms. Random planar problems, N=200.
Problem # Method Time Best Avg Worst
1NN100100.00109.58116.87
NN + 2opt442189.6692.5096.98
NN + 2opt Take Best495387.9390.7792.79
NN + 2opt-NR(24)26588.9491.3295.01
NN + 2opt-ND(24)23188.7292.7796.39
NN + 2opt-NDR(24)13489.3692.0595.94
NN + 2opt-N-BC(24)49789.2092.1896.75
NN + 2opt-NR-BC(24)23488.4290.9095.06
2NN100100.00106.22113.13
NN + 2opt428988.2791.2095.07
NN + 2opt Take Best505388.5390.1292.64
NN + 2opt-NR(24)26587.7890.2693.83
NN + 2opt-ND(24)26588.9691.5194.93
NN + 2opt-NDR(24)13187.8390.7993.90
NN + 2opt-N-BC(24)49987.8390.7493.97
NN + 2opt-NR-BC(24)23187.1189.9193.20
3NN100100.00110.81122.10
NN + 2opt350390.5894.0999.85
NN + 2opt Take Best425990.3193.0196.21
NN + 2opt-NR(24)20190.7693.9297.50
NN + 2opt-ND(24)17590.5094.74100.37
NN + 2opt-NDR(24)12591.1094.4799.50
NN + 2opt-N-BC(24)40390.6994.0999.06
NN + 2opt-NR-BC(24)17790.2693.5497.36
4NN100100.00108.19115.51
NN + 2opt415589.1692.4897.91
NN + 2opt Take Best482188.6391.1394.83
NN + 2opt-NR(24)33188.1891.5796.27
NN + 2opt-ND(24)23489.3693.0896.79
NN + 2opt-NDR(24)13188.7491.8296.09
NN + 2opt-N-BC(24)59788.4492.3396.40
NN + 2opt-NR-BC(24)19988.2391.6695.12
5NN100100.00105.47111.66
NN + 2opt310088.4491.2094.82
NN + 2opt Take Best312587.9390.8393.19
NN + 2opt-NR(24)19887.5590.3393.73
NN + 2opt-ND(24)19888.0491.8095.37
NN + 2opt-NDR(24)12388.0790.9895.03
NN + 2opt-N-BC(24)32288.0691.2194.08
NN + 2opt-NR-BC(24)17387.4590.1194.13
6NN100100.00106.99115.27
NN + 2opt511988.6191.7095.36
NN + 2opt Take Best555386.7489.3692.40
NN + 2opt-NR(24)29787.6790.3993.91
NN + 2opt-ND(24)23487.9991.1494.90
NN + 2opt-NDR(24)13187.6390.7593.93
NN + 2opt-N-BC(24)50087.4990.5593.80
NN + 2opt-NR-BC(24)23186.6989.7091.95
7NN100100.00109.06119.98
NN + 2opt485387.4390.8695.27
NN + 2opt Take Best475386.1489.4194.34
NN + 2opt-NR(24)26585.7489.6695.14
NN + 2opt-ND(24)26587.5891.1296.98
NN + 2opt-NDR(24)13486.6490.1596.67
NN + 2opt-N-BC(24)56586.8390.7495.90
NN + 2opt-NR-BC(24)33185.8389.6895.53
8NN100100.00106.79113.15
NN + 2opt495387.2691.3095.33
NN + 2opt Take Best565187.6589.5392.11
NN + 2opt-NR(24)23188.0790.2793.96
NN + 2opt-ND(24)23488.3191.6095.67
NN + 2opt-NDR(24)13188.1390.9995.28
NN + 2opt-N-BC(24)50087.8190.1194.61
NN + 2opt-NR-BC(24)23187.3689.2092.07
9NN100100.00104.58109.98
NN + 2opt408987.8691.8095.52
NN + 2opt Take Best501987.1889.6691.63
NN + 2opt-NR(24)39987.9690.7394.13
NN + 2opt-ND(24)23188.5391.9494.86
NN + 2opt-NDR(24)36588.2491.0694.24
NN + 2opt-N-BC(24)92988.0391.0094.26
NN + 2opt-NR-BC(24)53187.7390.2993.77
10NN100100.00106.10113.14
NN + 2opt668288.4690.9094.87
NN + 2opt Take Best558587.8289.7291.80
NN + 2opt-NR(24)29787.9490.7395.79
NN + 2opt-ND(24)23487.7091.5194.74
NN + 2opt-NDR(24)16587.9691.1896.28
NN + 2opt-N-BC(24)56387.8890.5693.03
NN + 2opt-NR-BC(24)23486.4489.7992.67

Saturday, April 1, 2017

2-opt with Neighbor Lists and optional DLB and FR

Note

This example of code does not contain any new ideas and can be skipped by a reader.

proc One_City_2_Opt_NDR(tour: var Tour_Array;
                        basePos: Tour_Index;
                        neighbor: Neighbor_Matrix;
                        position: var City_Position_In_Tour;
                        numberOfNeigbors: int;
                        DontLook: var DLB_Array;
                        useDLB: bool;
                        useFixedRadius: bool): bool =
  var
    improved: bool
    pos_Y2, i, j: Tour_Index
    X1, X2, Y1, Y2: City_Number
    d_X1_X2, d_X1_Y1: Length

  improved = false
  block two_loops:
    for direction in [forward, backward]:
      if direction == forward:
        i  = basePos
        X1 = tour[i]
        X2 = tour[(i+1) mod N]
      else:
        i  = (N + basePos - 1) mod N
        X2 = tour[i]
        X1 = tour[(i+1) mod N]  # == tour[basePos]
      d_X1_X2 = distance(X1, X2)

      for neighbor_number in 0 .. numberOfNeigbors-1:
        # after 2-opt move new tour would contain direct link X1-Y1,
        # so we look for city Y1 among cities that are close to city X1
        Y1 = neighbor[X1][neighbor_number]
        if direction == forward:
          j = position[Y1]
          Y2 = tour[(j+1) mod N]
        else:
          j  = (N + position[Y1] - 1) mod N  # pos[Y1] == j+1
          Y2 = tour[j]

        if (X2 == Y1) or (Y2 == X1):
          # 2-opt is for non-adjacent links only
          # by constuction X1 != Y1
          continue
          
        d_X1_Y1 = distance(X1, Y1)
          
        if useFixedRadius:
          if d_X1_Y1 > d_X1_X2:
            break

        # the same as: Gain_From_2_Opt(X1, X2, Y1, Y2) > 0
        if (d_X1_X2 + distance(Y1, Y2)) -
           (d_X1_Y1 + distance(X2, Y2)) > 0:
          if useDLB:
            Set_DLB_off(DontLook, [X1, X2, Y1, Y2])
          Make_2_Opt_Move(tour, i, j, position)
          improved = true
          break two_loops
  result = improved
proc LS_2_Opt_NDR(tour: var Tour_Array;
                  neighbor: Neighbor_Matrix;
                  neighborListLen: int = N-1;
                  useDLB: bool = true;
                  useFixedRadius: bool = true) =
  ## Optimizes the given tour using 2-opt with neighbor lists
  ## and with optional Don't Look Bits (DLB) and fixed radis
  var
    locallyOptimal: bool = false
    improved: bool
    DontLook: DLB_Array
    baseCity: City_Number
    position: City_Position_In_Tour

  let numberOfNeigbors = min(neighborListLen, len(neighbor[0]))
  position = Create_Position_In_Tour(tour)

  if useDLB:
    Set_DLB_off(DontLook, tour)  
  while not locallyOptimal:
    locallyOptimal = true
    for basePos in 0 .. N-1:
      baseCity = tour[basePos]
      if useDLB and isDLB_on(DontLook, baseCity):
        continue
      improved = One_City_2_Opt_NDR(tour, basePos, neighbor, position,
                                    numberOfNeigbors, DontLook,
                                    useDLB, useFixedRadius)
      if improved:
        locallyOptimal = false
      else:
        if useDLB:
          Set_DLB_on(DontLook, baseCity)