Quick start (Continued)

We expand the knowledge from Quick Start where the user has learned how to Install OptFrame, and how to compile and test a Simulated Annealing metaheuristic for the classic 0-1 Knapsack Problem (01KP).

We demonstrate how to update this code for other metaheuristics, for the classic Traveling Salesman Problem (TSP).

Second example: Traveling Salesman Problem, Local Search and Random Keys Genetic Algorithm

At this point, we assume the reader is familiarized with the Traveling Salesman Problem… we intend to expand this section in the future with figures and more motivation (for now, sorry, and let’s move on).

TSP Solution definition

We define a TSP solution as a permutations of $N$ cities being visited by a Traveling Salesman. In this representation, each city is represented as a number $0..N-1$, being a solution a vector of N integers (example: [0,2,3,1] means that solution starts from city 0, then follows to city 2, then city 3, then city 1, and finally comes back to city 0). Objective is to find a route that minimizes distance between the $N$ visited cities.

We may define ESolutionTSP as a pair, containing a solution and its objective value (double).

 1#pragma once
 2
 3// C++
 4#include <algorithm>
 5#include <functional>
 6#include <iostream>
 7#include <vector>
 8//
 9#include <OptFCore/FCore.hpp>
10#include <OptFrame/Core.hpp>
11#include <OptFrame/Heuristics/Heuristics.hpp>  // many metaheuristics here...
12#include <OptFrame/Scanner++/Scanner.hpp>
13#include <OptFrame/Util/Matrix.hpp>
14#include <OptFrame/printable/printable.hpp>
15
16using namespace std;        // NOLINT
17using namespace optframe;   // NOLINT
18using namespace scannerpp;  // NOLINT
19
20// define TSP solution type as 'vector<int>', using 'double' as evaluation type
21using ESolutionTSP =
22    std::pair<std::vector<int>,  // first part of search space element: solution
23                                 // (representation)
24              Evaluation<int>    // second part of search space element:
25                                 // evaluation (objective value)
26              >;

Problem Data

We read a matrix of distances between pairs of cities (considering Euclidean distance), and store in a structure named ProblemContext. Note that we round the result to int, just to allow precise value calculation (but one may use float or double, and then manage the floating-point errors). Do not forget to #include helpers from optframe namespace, such as Matrix and Scanner.

 1// TSP problem context and data reads
 2class ProblemContext {
 3 public:
 4  int n{0};          // number of clients
 5  Matrix<int> dist;  // distance matrix (Euclidean)
 6  // load data from Scanner
 7  void load(Scanner& scanner) {
 8    n = *scanner.nextInt();    // reads number of clients
 9    dist = Matrix<int>(n, n);  // initializes n x n matrix
10    //
11    vector<double> xvalues(n);
12    vector<double> yvalues(n);
13    //
14    for (int i = 0; i < n; i++) {
15      scanner.next();
16      xvalues[i] = *scanner.nextDouble();  // reads x
17      yvalues[i] = *scanner.nextDouble();  // reads y
18    }
19    // calculate distance values, for every client pair (i,j)
20    for (int i = 0; i < n; i++)
21      for (int j = 0; j < n; j++)
22        dist(i, j) = (int)::round(distance(xvalues.at(i), yvalues.at(i),
23                                           xvalues.at(j), yvalues.at(j)));
24  }
25  // euclidean distance (double as return)
26  double distance(double x1, double y1, double x2, double y2) {
27    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
28  }
29};
30// Create TSP Problem Context
31// ProblemContext pTSP;

Finally, we declare a global object ProblemContext pTSP (for simplicity), that includes all data.

Evaluation

Objective calculation can be done by using Functional Core class FEvaluator, which is a huge simplification from OptFrame Core components Evaluator (for single objectives) and GeneralEvaluator (for single and multiple objectives).

 1Evaluation<int> fevaluate(sref<ProblemContext> pTSP,
 2                          const std::vector<int>& s) {
 3  int f = 0;
 4  for (int i = 0; i < int(pTSP->n) - 1; i++) f += pTSP->dist(s[i], s[i + 1]);
 5  f += pTSP->dist(s[int(pTSP->n) - 1], s[0]);
 6  return Evaluation<int>{f};
 7}
 8
 9// Evaluate
10// FEvaluator<ESolutionTSP, MinOrMax::MINIMIZE> eval{fevaluate};

Search methods based on neighborhoods

The next components will depend on the type of search method used, we start with neighborhood-based search techniques.

Random constructive

In a similar manner with Knapsack example (on Quickstart part 1), we define a random solution generator.

1std::vector<int> frandom(sref<ProblemContext> pTSP) {
2  vector<int> v(pTSP->n, -1);  // get information from context
3  for (int i = 0; i < (int)v.size(); i++) v[i] = i;
4  std::random_shuffle(v.begin(), v.end());
5  return v;
6}
7
8// Generate random solution
9// FConstructive<std::vector<int>, ProblemContext> crand{frandom};

First move operator: Swap

We start with a Move operator capable of exchanging two elements from a given TSP solution.

 1std::pair<int, int> fApplySwap(sref<ProblemContext>,
 2                               const std::pair<int, int>& moveData,
 3                               ESolutionTSP& se) {
 4  int i = moveData.first;
 5  int j = moveData.second;
 6  // perform swap of clients i and j
 7  int aux = se.first[j];
 8  se.first[j] = se.first[i];
 9  se.first[i] = aux;
10  return std::pair<int, int>(j, i);  // return a reverse move ('undo' move)s
11}
12
13// Swap move
14using MoveSwap = FMoveP<std::pair<int, int>, ESolutionTSP, ProblemContext>;
15
16uptr<Move<ESolutionTSP>> makeMoveSwap(sref<ProblemContext> p, int i, int j) {
17  return uptr<Move<ESolutionTSP>>(new MoveSwap{p, make_pair(i, j), fApplySwap});
18}

We have four types of neighborhood definitions in OptFrame (NS, NSFind, NSSeq and NSEnum), but two major are NS and NSSeq.

NS works well for random neighborhoods, with no intent of explicitly visiting all possible neighbors (also useful for continuous or infinite neighborhoods). Swap move and NS definition can be seen below.

 1uptr<Move<ESolutionTSP>> fRandomSwap(sref<ProblemContext> pTSP,
 2                                     const ESolutionTSP& se) {
 3  int i = rand() % pTSP->n;
 4  int j = i;
 5  while (j <= i) {
 6    i = rand() % pTSP->n;
 7    j = rand() % pTSP->n;
 8  }
 9  return uptr<Move<ESolutionTSP>>(makeMoveSwap(pTSP, i, j));
10}
11
12// Swap move (NS)
13// FNS<ESolutionTSP> nsswap{fRandomSwap};

We now define the NSSeq neighborhood with a explicit iterator definition, that requires four operations: first (initializes first valid move), next (skips to next valid move), current (returns current move) and isDone (indicates if no move exists).

 1auto make_nsseq(sref<ProblemContext> p) {
 2  sref<FNSSeq<std::pair<int, int>, ESolutionTSP, ProblemContext>> nsseq2{
 3      new FNSSeq<std::pair<int, int>, ESolutionTSP, ProblemContext>{
 4          p,
 5          [](sref<ProblemContext> pTSP,
 6             const ESolutionTSP& se) -> uptr<Move<ESolutionTSP>> {
 7            int i = rand() % pTSP->n;
 8            int j = i;
 9            while (j <= i) {
10              i = rand() % pTSP->n;
11              j = rand() % pTSP->n;
12            }
13            return uptr<Move<ESolutionTSP>>(makeMoveSwap(pTSP, i, j));
14          },
15          // iterator initialization (fGenerator)
16          [](sref<ProblemContext> pTSP, const ESolutionTSP& se)
17              -> std::pair<int, int> { return make_pair(-1, -1); },
18          [](sref<ProblemContext> pTSP, std::pair<int, int>& p) -> void {
19            // void (*fFirst)(IMS&),                   // iterator.first()
20            p.first = 0;
21            p.second = 1;
22          },
23          [](sref<ProblemContext> pTSP, std::pair<int, int>& p) -> void {
24            // void (*fNext)(IMS&),                    // iterator.next()
25            if (p.second < (pTSP->n - 1))
26              p.second++;
27            else {
28              p.first++;
29              p.second = p.first + 1;
30            }
31          },
32          [](sref<ProblemContext> pTSP, std::pair<int, int>& p) -> bool {
33            // bool (*fIsDone)(IMS&),                  //
34            // iterator.isDone()
35            return p.first >= pTSP->n - 1;
36          },
37          [](sref<ProblemContext> pTSP,
38             std::pair<int, int>& p) -> uptr<Move<ESolutionTSP>> {
39            // uptr<Move<XES>> (*fCurrent)(IMS&)       //
40            // iterator.current()
41            return uptr<Move<ESolutionTSP>>(
42                makeMoveSwap(pTSP, p.first, p.second));
43          }  // FNSSeq
44      }};
45  return nsseq2;
46}
47
48class OptFrameDemoTSP {
49 public:
50  sref<ProblemContext> pTSP;
51  sref<FConstructive<std::vector<int>, ProblemContext>> randomConstructive;
52  sref<FEvaluator<ESolutionTSP, MINIMIZE, ProblemContext>> eval;
53  sref<FNS<ESolutionTSP, ProblemContext>> nsSwap;
54  sref<FNSSeq<std::pair<int, int>, ESolutionTSP, ProblemContext>> nsseqSwap;
55  sptr<DecoderRandomKeys<ESolutionTSP, double>> decoder;
56
57  explicit OptFrameDemoTSP(sref<ProblemContext> p)
58      : pTSP{p},
59        randomConstructive{
60            new FConstructive<std::vector<int>, ProblemContext>{p, frandom}},
61        eval{new FEvaluator<ESolutionTSP, MINIMIZE, ProblemContext>{p,
62                                                                    fevaluate}},
63        nsSwap{new FNS<ESolutionTSP, ProblemContext>{p, fRandomSwap}},
64        nsseqSwap{make_nsseq(p)}
65
66  {}
67};
68
69//

Hint

According to groundbreaking ideas from Variable Neighborhood Search community, the user should create multiple neighborhoods, with different ideas in each one, in order to better explore the solution space.

At this point, we quickly demonstrate how novel C++20 features, such as Coroutines, have improved OptFrame in latest versions (named FxCore library). We note that all four iterator operations (first, next, current and isDone) are made available quite naturally with a single coroutine generator that executes co_yield for each available move.

 1// THIS IS ONLY AVAILABLE WITH OptFrame FxCore and C++20 -fcoroutines
 2// Swap move (NSSeq) - with "Fancy" iterator (coroutines)
 3using NSSeqSwapFancy = FxNSSeqFancy<
 4  ESolutionTSP,
 5  [](const ESolutionTSP& se) -> uptr<Move<ESolutionTSP>> {
 6     int i = rand() % pTSP.n;
 7     int j = i;
 8     while (j <= i) {
 9        i = rand() % pTSP.n;
10        j = rand() % pTSP.n;
11     }
12     return uptr<Move<ESolutionTSP>>(new MoveSwap{ make_pair(i, j) });
13  },
14  [](const ESolutionTSP& se) -> Generator<Move<ESolutionTSP>*> {
15     for (int i = 0; i < int(pTSP.n) - 1; i++)
16        for (int j = i + 1; j < pTSP.n; j++)
17           co_yield new MoveSwap{ make_pair(i, j) }; // implicit unique_ptr requirements
18  }>;

We will explore more of these bleeding-edge tools in an advanced topic.

Complete Example for TSP Components

For simplicity, we separate the main TSP components in a file named TSP-fcore.hpp.

Hint

This example could be made in a single file, to be even simpler. However, we recommend users to have a clear separation for the header declaration of FCore components (on TSP-fcore.hpp) from the main() entrypoint depending on method used, e.g., mainTSP-fcore-ils.cpp or mainTSP-fcore-brkga.cpp.

TSP-fcore.hpp

File 'TSP-fcore.hpp' located in 'demo/03_QuickstartTSP_VNS_BRKGA/'

  1#pragma once
  2
  3// C++
  4#include <algorithm>
  5#include <functional>
  6#include <iostream>
  7#include <vector>
  8//
  9#include <OptFCore/FCore.hpp>
 10#include <OptFrame/Core.hpp>
 11#include <OptFrame/Heuristics/Heuristics.hpp>  // many metaheuristics here...
 12#include <OptFrame/Scanner++/Scanner.hpp>
 13#include <OptFrame/Util/Matrix.hpp>
 14#include <OptFrame/printable/printable.hpp>
 15
 16using namespace std;        // NOLINT
 17using namespace optframe;   // NOLINT
 18using namespace scannerpp;  // NOLINT
 19
 20// define TSP solution type as 'vector<int>', using 'double' as evaluation type
 21using ESolutionTSP =
 22    std::pair<std::vector<int>,  // first part of search space element: solution
 23                                 // (representation)
 24              Evaluation<int>    // second part of search space element:
 25                                 // evaluation (objective value)
 26              >;
 27
 28// TSP problem context and data reads
 29class ProblemContext {
 30 public:
 31  int n{0};          // number of clients
 32  Matrix<int> dist;  // distance matrix (Euclidean)
 33  // load data from Scanner
 34  void load(Scanner& scanner) {
 35    n = *scanner.nextInt();    // reads number of clients
 36    dist = Matrix<int>(n, n);  // initializes n x n matrix
 37    //
 38    vector<double> xvalues(n);
 39    vector<double> yvalues(n);
 40    //
 41    for (int i = 0; i < n; i++) {
 42      scanner.next();
 43      xvalues[i] = *scanner.nextDouble();  // reads x
 44      yvalues[i] = *scanner.nextDouble();  // reads y
 45    }
 46    // calculate distance values, for every client pair (i,j)
 47    for (int i = 0; i < n; i++)
 48      for (int j = 0; j < n; j++)
 49        dist(i, j) = (int)::round(distance(xvalues.at(i), yvalues.at(i),
 50                                           xvalues.at(j), yvalues.at(j)));
 51  }
 52  // euclidean distance (double as return)
 53  double distance(double x1, double y1, double x2, double y2) {
 54    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
 55  }
 56};
 57// Create TSP Problem Context
 58// ProblemContext pTSP;
 59
 60Evaluation<int> fevaluate(sref<ProblemContext> pTSP,
 61                          const std::vector<int>& s) {
 62  int f = 0;
 63  for (int i = 0; i < int(pTSP->n) - 1; i++) f += pTSP->dist(s[i], s[i + 1]);
 64  f += pTSP->dist(s[int(pTSP->n) - 1], s[0]);
 65  return Evaluation<int>{f};
 66}
 67
 68// Evaluate
 69// FEvaluator<ESolutionTSP, MinOrMax::MINIMIZE> eval{fevaluate};
 70std::vector<int> frandom(sref<ProblemContext> pTSP) {
 71  vector<int> v(pTSP->n, -1);  // get information from context
 72  for (int i = 0; i < (int)v.size(); i++) v[i] = i;
 73  std::random_shuffle(v.begin(), v.end());
 74  return v;
 75}
 76
 77// Generate random solution
 78// FConstructive<std::vector<int>, ProblemContext> crand{frandom};
 79
 80std::pair<int, int> fApplySwap(sref<ProblemContext>,
 81                               const std::pair<int, int>& moveData,
 82                               ESolutionTSP& se) {
 83  int i = moveData.first;
 84  int j = moveData.second;
 85  // perform swap of clients i and j
 86  int aux = se.first[j];
 87  se.first[j] = se.first[i];
 88  se.first[i] = aux;
 89  return std::pair<int, int>(j, i);  // return a reverse move ('undo' move)s
 90}
 91
 92// Swap move
 93using MoveSwap = FMoveP<std::pair<int, int>, ESolutionTSP, ProblemContext>;
 94
 95uptr<Move<ESolutionTSP>> makeMoveSwap(sref<ProblemContext> p, int i, int j) {
 96  return uptr<Move<ESolutionTSP>>(new MoveSwap{p, make_pair(i, j), fApplySwap});
 97}
 98uptr<Move<ESolutionTSP>> fRandomSwap(sref<ProblemContext> pTSP,
 99                                     const ESolutionTSP& se) {
100  int i = rand() % pTSP->n;
101  int j = i;
102  while (j <= i) {
103    i = rand() % pTSP->n;
104    j = rand() % pTSP->n;
105  }
106  return uptr<Move<ESolutionTSP>>(makeMoveSwap(pTSP, i, j));
107}
108
109// Swap move (NS)
110// FNS<ESolutionTSP> nsswap{fRandomSwap};
111
112auto make_nsseq(sref<ProblemContext> p) {
113  sref<FNSSeq<std::pair<int, int>, ESolutionTSP, ProblemContext>> nsseq2{
114      new FNSSeq<std::pair<int, int>, ESolutionTSP, ProblemContext>{
115          p,
116          [](sref<ProblemContext> pTSP,
117             const ESolutionTSP& se) -> uptr<Move<ESolutionTSP>> {
118            int i = rand() % pTSP->n;
119            int j = i;
120            while (j <= i) {
121              i = rand() % pTSP->n;
122              j = rand() % pTSP->n;
123            }
124            return uptr<Move<ESolutionTSP>>(makeMoveSwap(pTSP, i, j));
125          },
126          // iterator initialization (fGenerator)
127          [](sref<ProblemContext> pTSP, const ESolutionTSP& se)
128              -> std::pair<int, int> { return make_pair(-1, -1); },
129          [](sref<ProblemContext> pTSP, std::pair<int, int>& p) -> void {
130            // void (*fFirst)(IMS&),                   // iterator.first()
131            p.first = 0;
132            p.second = 1;
133          },
134          [](sref<ProblemContext> pTSP, std::pair<int, int>& p) -> void {
135            // void (*fNext)(IMS&),                    // iterator.next()
136            if (p.second < (pTSP->n - 1))
137              p.second++;
138            else {
139              p.first++;
140              p.second = p.first + 1;
141            }
142          },
143          [](sref<ProblemContext> pTSP, std::pair<int, int>& p) -> bool {
144            // bool (*fIsDone)(IMS&),                  //
145            // iterator.isDone()
146            return p.first >= pTSP->n - 1;
147          },
148          [](sref<ProblemContext> pTSP,
149             std::pair<int, int>& p) -> uptr<Move<ESolutionTSP>> {
150            // uptr<Move<XES>> (*fCurrent)(IMS&)       //
151            // iterator.current()
152            return uptr<Move<ESolutionTSP>>(
153                makeMoveSwap(pTSP, p.first, p.second));
154          }  // FNSSeq
155      }};
156  return nsseq2;
157}
158
159class OptFrameDemoTSP {
160 public:
161  sref<ProblemContext> pTSP;
162  sref<FConstructive<std::vector<int>, ProblemContext>> randomConstructive;
163  sref<FEvaluator<ESolutionTSP, MINIMIZE, ProblemContext>> eval;
164  sref<FNS<ESolutionTSP, ProblemContext>> nsSwap;
165  sref<FNSSeq<std::pair<int, int>, ESolutionTSP, ProblemContext>> nsseqSwap;
166  sptr<DecoderRandomKeys<ESolutionTSP, double>> decoder;
167
168  explicit OptFrameDemoTSP(sref<ProblemContext> p)
169      : pTSP{p},
170        randomConstructive{
171            new FConstructive<std::vector<int>, ProblemContext>{p, frandom}},
172        eval{new FEvaluator<ESolutionTSP, MINIMIZE, ProblemContext>{p,
173                                                                    fevaluate}},
174        nsSwap{new FNS<ESolutionTSP, ProblemContext>{p, fRandomSwap}},
175        nsseqSwap{make_nsseq(p)}
176
177  {}
178};
179
180//

Exploring with neighborhood exploration

OptFrame supports several strategies for neighborhood exploration, such as: First Improvement, Best Improvement, Random Selection and Multi Improvement. We can also combine several local search strategies in a multiple strategy called Variable Neighborhood Descent (VND).

 1int main() {
 2  srand(0);  // using system random (weak... just an example!)
 3
 4  // load data into problem context 'pTSP'
 5  Scanner scanner{"5\n1 10 10\n2 20 20\n3 30 30\n4 40 40\n5 50 50\n"};
 6  sref<ProblemContext> pTSP{new ProblemContext{}};
 7  pTSP->load(scanner);
 8  std::cout << pTSP->dist << std::endl;
 9
10  OptFrameDemoTSP demo{pTSP};
11
12  // evaluator
13  // TSPEval ev;
14  //
15  // create simple solution
16  // TSPRandom crand;
17  //
18  std::vector<int> sol = *demo.randomConstructive->generateSolution(0);
19  std::cout << sol << std::endl;
20
21  // evaluation value and store on ESolution pair
22  ESolutionTSP esol(sol, demo.eval->evaluate(sol));
23  esol.second.print();  // print evaluation
24
25  // swap 0 with 1
26  MoveSwap move{demo.pTSP, make_pair(0, 1), fApplySwap};
27  move.print();
28
29  // NSSwap nsswap;
30  // move for solution 'esol'
31  auto m1 = demo.nsSwap->randomMove(esol);
32  m1->print();
33
34  std::cout << std::endl;
35  std::cout << "begin listing NSSeqSwapFancy" << std::endl;
36  //
37  auto it1 = demo.nsseqSwap->getIterator(esol);
38  for (it1->first(); !it1->isDone(); it1->next()) it1->current()->print();
39  std::cout << "end listing NSSeqSwapFancy" << std::endl;
40
41  // Random number generator
42  RandGen rg;                      // stack version
43  sref<RandGen> rg2{new RandGen};  // heap version (safely shared)
44  // testing simulated annealing
45  BasicInitialSearch<ESolutionTSP> initRand(demo.randomConstructive, demo.eval);
46
47  vsref<LocalSearch<ESolutionTSP>> ns_list;
48  Evaluator<ESolutionTSP::first_type, ESolutionTSP::second_type>* ev2 =
49      new FEvaluator<ESolutionTSP, MinOrMax::MINIMIZE, ProblemContext>{
50          pTSP, fevaluate};
51  GeneralEvaluator<ESolutionTSP>* gev2 = (GeneralEvaluator<ESolutionTSP>*)ev2;
52  sref<GeneralEvaluator<ESolutionTSP>> eval2(gev2);
53  ns_list.push_back(new BestImprovement<ESolutionTSP>(eval2, demo.nsseqSwap));
54
55  VariableNeighborhoodDescent<ESolutionTSP> VND(demo.eval, ns_list);
56  // VND.setVerbose();

If one wants to build a complete metaheuristic, the Iterated Local Search (ILS) or a Variable Neighborhood Search (VNS). The ILS is based on general perturbation concept, so we will use the concept of Levels of Perturbation, that are increased when stuck in a poor quality local optimum. We adopt a perturbation strategy that tries to escape at level p by applying p+2 random moves, e.g., at level 0, 2 random moves are applied, and so on.

 1//
 2ILSLPerturbationLPlus2<ESolutionTSP> pert(demo.eval, demo.nsSwap, rg2);
 3
 4IteratedLocalSearchLevels<ESolutionTSP> ils(demo.eval, initRand, VND, pert, 10,
 5                                            5);
 6// ils.setVerbose();
 7
 8std::cout << "will start ILS for 3 seconds" << std::endl;
 9
10auto status = ils.search(
11    StopCriteria<ESolutionTSP::second_type>{3.0});  // 3.0 seconds max
12ESolutionTSP best = *status.best;
13// best solution value
14best.second.print();
15std::cout << "solution: " << best.first << std::endl;
16
17std::cout << "FINISHED" << std::endl;
18return 0;
19}

Complete Example for ILS

We provide the main file for TSP ILS mainTSP-fcore-ils.cpp.

mainTSP-fcore-ils.cpp

File 'mainTSP-fcore-ils.cpp' located in 'demo/03_QuickstartTSP_VNS_BRKGA/'

 1// SPDX-License-Identifier:  MIT OR LGPL-3.0-or-later
 2// Copyright (C) 2007-2022 - OptFrame developers
 3// https://github.com/optframe/optframe
 4
 5// OptFrame Demo TSP - Iterated Local Search
 6
 7// C++
 8#include <iostream>
 9//
10#include "TSP-fcore.hpp"
11// implementation of TSP
12
13#include <OptFrame/Core.hpp>
14#include <OptFrame/Heuristics/Heuristics.hpp>  // many metaheuristics here...
15#include <OptFrame/Heuristics/ILS/IteratedLocalSearchLevels.hpp>
16#include <OptFrame/Heuristics/LocalSearches/BestImprovement.hpp>
17#include <OptFrame/Heuristics/LocalSearches/VariableNeighborhoodDescent.hpp>
18#include <OptFrame/LocalSearch.hpp>
19
20// import everything on main()
21using namespace std;        // NOLINT
22using namespace optframe;   // NOLINT
23using namespace scannerpp;  // NOLINT
24// using namespace TSP_fcore;
25int main() {
26  srand(0);  // using system random (weak... just an example!)
27
28  // load data into problem context 'pTSP'
29  Scanner scanner{"5\n1 10 10\n2 20 20\n3 30 30\n4 40 40\n5 50 50\n"};
30  sref<ProblemContext> pTSP{new ProblemContext{}};
31  pTSP->load(scanner);
32  std::cout << pTSP->dist << std::endl;
33
34  OptFrameDemoTSP demo{pTSP};
35
36  // evaluator
37  // TSPEval ev;
38  //
39  // create simple solution
40  // TSPRandom crand;
41  //
42  std::vector<int> sol = *demo.randomConstructive->generateSolution(0);
43  std::cout << sol << std::endl;
44
45  // evaluation value and store on ESolution pair
46  ESolutionTSP esol(sol, demo.eval->evaluate(sol));
47  esol.second.print();  // print evaluation
48
49  // swap 0 with 1
50  MoveSwap move{demo.pTSP, make_pair(0, 1), fApplySwap};
51  move.print();
52
53  // NSSwap nsswap;
54  // move for solution 'esol'
55  auto m1 = demo.nsSwap->randomMove(esol);
56  m1->print();
57
58  std::cout << std::endl;
59  std::cout << "begin listing NSSeqSwapFancy" << std::endl;
60  //
61  auto it1 = demo.nsseqSwap->getIterator(esol);
62  for (it1->first(); !it1->isDone(); it1->next()) it1->current()->print();
63  std::cout << "end listing NSSeqSwapFancy" << std::endl;
64
65  // Random number generator
66  RandGen rg;                      // stack version
67  sref<RandGen> rg2{new RandGen};  // heap version (safely shared)
68  // testing simulated annealing
69  BasicInitialSearch<ESolutionTSP> initRand(demo.randomConstructive, demo.eval);
70
71  vsref<LocalSearch<ESolutionTSP>> ns_list;
72  Evaluator<ESolutionTSP::first_type, ESolutionTSP::second_type>* ev2 =
73      new FEvaluator<ESolutionTSP, MinOrMax::MINIMIZE, ProblemContext>{
74          pTSP, fevaluate};
75  GeneralEvaluator<ESolutionTSP>* gev2 = (GeneralEvaluator<ESolutionTSP>*)ev2;
76  sref<GeneralEvaluator<ESolutionTSP>> eval2(gev2);
77  ns_list.push_back(new BestImprovement<ESolutionTSP>(eval2, demo.nsseqSwap));
78
79  VariableNeighborhoodDescent<ESolutionTSP> VND(demo.eval, ns_list);
80  // VND.setVerbose();//
81ILSLPerturbationLPlus2<ESolutionTSP> pert(demo.eval, demo.nsSwap, rg2);
82
83IteratedLocalSearchLevels<ESolutionTSP> ils(demo.eval, initRand, VND, pert, 10,
84                                            5);
85// ils.setVerbose();
86
87std::cout << "will start ILS for 3 seconds" << std::endl;
88
89auto status = ils.search(
90    StopCriteria<ESolutionTSP::second_type>{3.0});  // 3.0 seconds max
91ESolutionTSP best = *status.best;
92// best solution value
93best.second.print();
94std::cout << "solution: " << best.first << std::endl;
95
96std::cout << "FINISHED" << std::endl;
97return 0;
98}

Search methods based on random keys

We finish with the Biased Random Key Genetic Algorithm (BRKGA), a simple metaheuristic inspired by classic Genetic Algorithm, using the solution representation of $n$ Random Keys, which are $[0,1]^n$ float values.

Random key generation

The BRKGA requires an initial solution generator, which is in this case, $n$ random [0,1] floats. This can be done automatically by the method (since its trivial do generate $n$ [0,1] random numbers), but we choose to demonstrate manually (by inheriting from OptFrame Core class Initial Population).

This is good to tune the degree of randomness (number of random digits) and also the random function used.

 1class MyRandomKeysInitEPop
 2    : public InitialEPopulation<
 3          std::pair<std::vector<double>, Evaluation<int>>> {
 4  using RSK = std::vector<double>;
 5
 6 private:
 7  int sz;
 8  sref<RandGen> rg;
 9
10 public:
11  explicit MyRandomKeysInitEPop(int size, sref<RandGen> _rg = new RandGen)
12      : sz{size}, rg{_rg} {}
13
14  // copy constructor
15  // MyRandomKeysInitEPop(const MyRandomKeysInitEPop& self)
16  //     : sz{self.sz}, rg{self.rg} {}
17
18  // this generator cannot evaluate solutions
19  bool canEvaluate() const override { return false; }
20
21  VEPopulation<std::pair<RSK, Evaluation<int>>> generateEPopulation(
22      unsigned populationSize, double timelimit) override {
23    VEPopulation<std::pair<RSK, Evaluation<int>>> pop;
24
25    for (unsigned i = 0; i < populationSize; i++) {
26      vector<double> vd(sz);
27      for (int j = 0; j < sz; j++) vd[j] = (rg->rand() % 100000) / 100000.0;
28      // assert(!this->canEvaluate());
29      std::pair<RSK, Evaluation<int>> ind{vd, Evaluation<int>{}};
30      pop.push_back(ind);
31    }
32
33    return pop;
34  }
35};

BRKGA decoding

BRKGA also requires a decoder function, that maps this array of random keys into a permutation.

This can be easily done with Functional Core class FDecodeRK, and an interesting approach based on sorting the keys, related to a predefined indexing of each key.

 1pair<Evaluation<int>, vector<int>> fDecodeEval(
 2    sref<Evaluator<typename ESolutionTSP::first_type,
 3                   typename ESolutionTSP::second_type, ESolutionTSP>>
 4        eval,
 5    const vector<double>& rk) {
 6  vector<pair<double, int>> v(rk.size());
 7  //
 8  for (unsigned i = 0; i < v.size(); i++) v[i] = pair<double, int>(rk[i], i);
 9
10  sort(v.begin(), v.end(),
11       [](const pair<double, int>& i, const pair<double, int>& j) -> bool {
12         return i.first < j.first;
13       });
14
15  // R = vector<int>
16  vector<int> p(v.size());
17  for (unsigned i = 0; i < v.size(); i++) p[i] = v[i].second;
18
19  /*
20  // ========== CHECKER ========
21  vector<bool> vb(v.size(), false);
22  for (unsigned i = 0; i < p.size(); i++)
23     vb[p[i]] = true;
24  for (unsigned i = 0; i < vb.size(); i++) {
25     if (!vb[i]) {
26        std::cout << "ERROR rk:" << rk << std::endl;
27        std::cout << "ERROR v:" << v << std::endl;
28        std::cout << "ERROR p:" << p << std::endl;
29        std::cout << "ERROR vb:" << vb << std::endl;
30     }
31     assert(vb[i]);
32  }
33  // ===== end CHECKER =====
34*/
35
36  Evaluation<int> e = eval->evaluate(p);
37  return make_pair(e, p);
38}
39
40// evaluator random keys (for TSP)
41// FDecoderEvalRK<std::pair<std::vector<int>, Evaluation<int>>, double>
42// decoder{fDecode};

BRKGA with TSP

We are ready to build a TSP instance with 3 cities with coordinates (10,10), (20,20) and (30,30), and invoke a BRKGA to solve it.

The parameters of BRKGA are: decoding function, initial solution generator, population size, number of iterations, also rates for mutation (randomness), elite (best solutions), preference for elite solutions, and finally, a random generation method.

 1int main() {
 2  sref<RandGen> rg = new RandGen;  // avoids weird windows OS interactions
 3
 4  // load data into problem context 'pTSP'
 5  Scanner scanner{"5\n1 10 10\n2 20 20\n3 30 30\n4 40 40\n5 50 50\n"};
 6  sref<ProblemContext> pTSP{new ProblemContext{}};
 7  pTSP->load(scanner);
 8  std::cout << pTSP->dist << std::endl;
 9
10  OptFrameDemoTSP demo{pTSP};
11  // setup decoder function
12  demo.decoder = sptr<DecoderRandomKeys<ESolutionTSP, double>>{
13      new FDecoderEvalRK<std::pair<std::vector<int>, Evaluation<int>>, double>{
14          demo.eval, fDecodeEval}};
15
16  // Parameters BRKGA
17  // (C1): Evaluator<S, XEv>& _evaluator, int key_size, unsigned numGen,
18  // unsigned _popSize, double fracTOP, double fracBOT, double _probElitism) :
19
20  sref<DecoderRandomKeys<ESolutionTSP, double>> _decoder = demo.decoder;
21  sref<InitialEPopulation<std::pair<vector<double>, ESolutionTSP::second_type>>>
22      _initPop = new MyRandomKeysInitEPop(pTSP->n);  // passing key_size
23
24  // eprk, pTSP.n, 1000, 30, 0.4, 0.3, 0.6
25  BRKGA<ESolutionTSP, double> brkga(
26      _decoder, MyRandomKeysInitEPop(pTSP->n, rg),  // key_size = pTSP.n
27      30, 1000, 0.4, 0.3, 0.6, rg);
28
29  auto searchOut = brkga.search(3.0);  // 3.0 seconds max
30  ESolutionTSP best = *searchOut.best;
31  // best solution value
32  best.second.print();
33  std::cout << "solution: " << best.first << std::endl;
34
35  std::cout << "FINISHED" << std::endl;
36  return 0;
37}

The result from searchOut can be split in two parts, an error code and the returned solution (the same as in Simulated Annealing or any other OptFrame search method).

Complete Example for BRKGA

We provide the main file for TSP BRKGA mainTSP-fcore-brkga.cpp.

mainTSP-fcore-brkga.cpp

File 'mainTSP-fcore-brkga.cpp' located in 'demo/03_QuickstartTSP_VNS_BRKGA/'

  1// SPDX-License-Identifier:  MIT OR LGPL-3.0-or-later
  2// Copyright (C) 2007-2022 - OptFrame developers
  3// https://github.com/optframe/optframe
  4
  5// OptFrame Demo TSP - BRKGA
  6
  7// C++
  8#include <iostream>
  9//
 10#include "TSP-fcore.hpp"
 11// implementation of TSP
 12//
 13#include <OptFrame/Core.hpp>
 14#include <OptFrame/Heuristics/EA/RK/BRKGA.hpp>
 15#include <OptFrame/Heuristics/Heuristics.hpp>  // many metaheuristics here...
 16#include <OptFrame/InitialPopulation.hpp>
 17#include <OptFrame/LocalSearch.hpp>
 18
 19// import everything on main()
 20using namespace std;        // NOLINT
 21using namespace optframe;   // NOLINT
 22using namespace scannerpp;  // NOLINT
 23// using namespace TSP_fcore;
 24
 25class MyRandomKeysInitEPop
 26    : public InitialEPopulation<
 27          std::pair<std::vector<double>, Evaluation<int>>> {
 28  using RSK = std::vector<double>;
 29
 30 private:
 31  int sz;
 32  sref<RandGen> rg;
 33
 34 public:
 35  explicit MyRandomKeysInitEPop(int size, sref<RandGen> _rg = new RandGen)
 36      : sz{size}, rg{_rg} {}
 37
 38  // copy constructor
 39  // MyRandomKeysInitEPop(const MyRandomKeysInitEPop& self)
 40  //     : sz{self.sz}, rg{self.rg} {}
 41
 42  // this generator cannot evaluate solutions
 43  bool canEvaluate() const override { return false; }
 44
 45  VEPopulation<std::pair<RSK, Evaluation<int>>> generateEPopulation(
 46      unsigned populationSize, double timelimit) override {
 47    VEPopulation<std::pair<RSK, Evaluation<int>>> pop;
 48
 49    for (unsigned i = 0; i < populationSize; i++) {
 50      vector<double> vd(sz);
 51      for (int j = 0; j < sz; j++) vd[j] = (rg->rand() % 100000) / 100000.0;
 52      // assert(!this->canEvaluate());
 53      std::pair<RSK, Evaluation<int>> ind{vd, Evaluation<int>{}};
 54      pop.push_back(ind);
 55    }
 56
 57    return pop;
 58  }
 59};
 60
 61
 62pair<Evaluation<int>, vector<int>> fDecodeEval(
 63    sref<Evaluator<typename ESolutionTSP::first_type,
 64                   typename ESolutionTSP::second_type, ESolutionTSP>>
 65        eval,
 66    const vector<double>& rk) {
 67  vector<pair<double, int>> v(rk.size());
 68  //
 69  for (unsigned i = 0; i < v.size(); i++) v[i] = pair<double, int>(rk[i], i);
 70
 71  sort(v.begin(), v.end(),
 72       [](const pair<double, int>& i, const pair<double, int>& j) -> bool {
 73         return i.first < j.first;
 74       });
 75
 76  // R = vector<int>
 77  vector<int> p(v.size());
 78  for (unsigned i = 0; i < v.size(); i++) p[i] = v[i].second;
 79
 80  /*
 81  // ========== CHECKER ========
 82  vector<bool> vb(v.size(), false);
 83  for (unsigned i = 0; i < p.size(); i++)
 84     vb[p[i]] = true;
 85  for (unsigned i = 0; i < vb.size(); i++) {
 86     if (!vb[i]) {
 87        std::cout << "ERROR rk:" << rk << std::endl;
 88        std::cout << "ERROR v:" << v << std::endl;
 89        std::cout << "ERROR p:" << p << std::endl;
 90        std::cout << "ERROR vb:" << vb << std::endl;
 91     }
 92     assert(vb[i]);
 93  }
 94  // ===== end CHECKER =====
 95*/
 96
 97  Evaluation<int> e = eval->evaluate(p);
 98  return make_pair(e, p);
 99}
100
101// evaluator random keys (for TSP)
102// FDecoderEvalRK<std::pair<std::vector<int>, Evaluation<int>>, double>
103// decoder{fDecode};
104int main() {
105  sref<RandGen> rg = new RandGen;  // avoids weird windows OS interactions
106
107  // load data into problem context 'pTSP'
108  Scanner scanner{"5\n1 10 10\n2 20 20\n3 30 30\n4 40 40\n5 50 50\n"};
109  sref<ProblemContext> pTSP{new ProblemContext{}};
110  pTSP->load(scanner);
111  std::cout << pTSP->dist << std::endl;
112
113  OptFrameDemoTSP demo{pTSP};
114  // setup decoder function
115  demo.decoder = sptr<DecoderRandomKeys<ESolutionTSP, double>>{
116      new FDecoderEvalRK<std::pair<std::vector<int>, Evaluation<int>>, double>{
117          demo.eval, fDecodeEval}};
118
119  // Parameters BRKGA
120  // (C1): Evaluator<S, XEv>& _evaluator, int key_size, unsigned numGen,
121  // unsigned _popSize, double fracTOP, double fracBOT, double _probElitism) :
122
123  sref<DecoderRandomKeys<ESolutionTSP, double>> _decoder = demo.decoder;
124  sref<InitialEPopulation<std::pair<vector<double>, ESolutionTSP::second_type>>>
125      _initPop = new MyRandomKeysInitEPop(pTSP->n);  // passing key_size
126
127  // eprk, pTSP.n, 1000, 30, 0.4, 0.3, 0.6
128  BRKGA<ESolutionTSP, double> brkga(
129      _decoder, MyRandomKeysInitEPop(pTSP->n, rg),  // key_size = pTSP.n
130      30, 1000, 0.4, 0.3, 0.6, rg);
131
132  auto searchOut = brkga.search(3.0);  // 3.0 seconds max
133  ESolutionTSP best = *searchOut.best;
134  // best solution value
135  best.second.print();
136  std::cout << "solution: " << best.first << std::endl;
137
138  std::cout << "FINISHED" << std::endl;
139  return 0;
140}

More Examples

For a other examples, see folder Examples/FCore-BRKGA and execute bazel build ...

Warning

Feel free to check folder OptFrame/Examples for other examples on FCore and OptFrame Classic.