Муравьиный алгоритм | Задача коммивояжёра

Для того, чтобы реализовать алгоритм нам нужны виртуальные муравьи и виртуальная колония муравьев. Тажке нужна будет структура данных, для удобного хранения пути и длины пути, пройденного муравьем.

struct AntPath {
    std::vector<std::size_t> vertices;
    double distance = 0;
};

struct Ant {
     explicit Ant(std::size_t start_vertex = 0);

     AntPath path;

     std::vector<std::size_t> visited;
     std::size_t start_location = 0, current_location = 0;
     bool can_continue = true;

     void MakeChoice(const Graph<double> &g, const Matrix<double> &p, double a, double b);
     double getRandomChoice();
     std::vector<std::size_t> getNeighborVertexes(const Graph<double> &g)
};

class AntColonyOptimization {
public:
    explicit AntColonyOptimization(const Graph<double> &graph);

    AntPath SolveSalesmansProblem();

private:
    const double kAlpha_ = 1.0;
    const double kBeta_ = 2.0;
    const double kPheromone0_ = 1;
    const double kQ_ = 5.0;
    const double kEvaporation_ = 0.2;

    Graph<double> graph_;
    Matrix<double> pheromone_;

    std::vector<Ant> ants_;

    void CreateAnts();
    void UpdateGlobalPheromone(const Matrix<double> &local_pheromone_update);
};

AntColonyOptimization class


  • Основной метод называется SolveSalesmansProblem, будет возвращать самый выгодный путь, найденный муравьями.
  • Коэффициенты kAlpha kBeta kPheromone0, kQ, kEvaporation определяются и подбираются опытным путем под каждую задачу
  • Класс AntColonyOptimization будет конструироваться от графа, в котором нужно решить задачу коммивояжёра.
  • Метод CreateAnts будет создавать муравьев и задавать начальную инициализацию каждому муравью
  • Метод UpdateGlobalPheromone делает глобальное обновление феромона, приминает на вход локальную соостовляющую обновления феромона.

Ant struct


  • Конструируется от стартовой вершины.
  • Хранит в себе структуру AntPath для удобного хранения пройденного пути и длины пройденного пути
  • Поле visited будет хранить посещенные вершины муравьев, для того чтобы повторно не посещать уже пройденные вершины.
  • start_location — начальная вершина (вершина старта) муравья, current_location — текущее положение муравья

Конструктор AntColonyOptimization


  • Конструктор сохраняет граф к себе в поле и инициализирует матрицу феромонов начальным значением.
  • Также конструктор определяет константу kQ, от которой в дальнейшем будет зависеть обновление локальной составляющей феромона

AntColonyOptimization::AntColonyOptimization(const Graph<double> &graph)
        : graph_(graph), kQ_(0.015 * graph.getGraphWeight()) {
   const std::size_t kVertexesCount = graph_.getVertexesCount();
   Matrix<double> matrix(kVertexesCount);
   for (std::size_t row = 0; row != kVertexesCount; ++row)
       for (std::size_t col = 0; col != kVertexesCount; ++col)
           if (row != col) matrix(row, col) = kPheromone0_;

   pheromone_ = std::move(matrix);
}

Реализация основных методов


Метод создания муравьев и инициализации муравьев.
Количество муравьев будет равно количеству вершин в графе.
Каждый муравей будет стартовать со своей вершины.

void AntColonyOptimization::CreateAnts() {
    const auto kAntsCount = graph_.getVertexesCount();
    ants_.resize(kAntsCount);

    for (std::size_t i = 0, size = ants_.size(); i != size; ++i)
        ants_[i] = Ant(i);
}

Метод глобального обновления феромона

  • Добавил ограничение на испарение феромона, пусть минимально возможным количеством феромона не ребре будет 0.01

void AntColonyOptimization::UpdateGlobalPheromone(const Matrix<double> &lpu) {
    // lpu - local pheromone update
    for (std::size_t from = 0, size = lpu.getRows(); from != size; ++from) {
        for (std::size_t to = 0; to != size; ++to) {
            pheromone_(from, to) = (1 - kEvaporation_) * pheromone_(from, to) + lpu(from, to);
            if (pheromone_(from, to) < 0.01 and from != to)
                pheromone_(from, to) = 0.01;
        }
    }
}

Метод генерация случайного числа для вероятности
Так как вероятность — это число в пределах [0, 1], генерировать будем в этих же пределах

double Ant::getRandomChoice() {
    std::uniform_real_distribution<> dist(0.0, 1.0);
    std::default_random_engine re(system_clock::now().time_since_epoch().count());
    return dist(re);
}

Метод получения возможных для посещения вершин от текущего положения

std::vector<std::size_t> Ant::getNeighborVertexes(const Graph<double> &g) {
    std::vector<std::size_t> vertexes;
    for (std::size_t to = 0; to != graph.getVertexesCount(); ++to) {
         bool edge_is_exist = g(current_location, to) != 0;
         bool vertex_is_unvisited = std::find(visited.begin(), visited.end(), to) == visited.end()
         if (edge_is_exist and vertex_is_unvisited)
              vertexes.push_back(to);
    }
    return vertexes;   
}

Метод выбора следующей вершины

void Ant::MakeChoice(const Graph<T> &g, const Matrix<double> &p, double a, double b) {
    if (path.vertices.empty()) {
        path.vertices.push_back(current_location);
        visited.push_back(current_location);
    }

   std::vector<std::size_t> neighbor_vertexes = getNeighborVertexes(g);

   if (neighbor_vertexes.empty()) {
       can_continue = false;
       if (graph(current_location, start_location) != 0) {
           path.vertices.push_back(start_location);
           path.distance += graph(current_location, start_location);
       }
       return;
  }

   std::vector<double> choosing_probability(neighbor_vertexes.size());

   {
        // Подсчет вероятности перехода муравья из одной вершины в другую
        std::vector<double> wish;
        std::vector<double> probability; 
        double summary_wish = 0.0f;
        for (auto v : neighbor_vertexes) {
                double t = p(current_location, v);
                double w = g(current_location, v);
                double n = 1 / w;
                wish.push_back(std::pow(t, alpha) * std::pow(n, beta));
                summary_wish += wish.back();
       }

       for (std::size_t neighbor = 0; neighbor != neighbor_vertexes.size(); ++neighbor) {
           probability.push_back(wish[neighbor] / summary_wish);
           if (neighbor == 0)
               choosing_probability[neighbor] = probability.back();
           else
               choosing_probability[neighbor] = choosing_probability[neighbor - 1] + probability.back();
        }
   }

   // Определение следующей вершины, которую посетит муравей
   std::size_t next_vertex = 0;
   double choose = getRandomChoice();

   for (std::size_t n = 0; n != neighbor_vertexes.size(); ++n ) {
       if (choose <= choosing_probability[n ]) {
           next_vertex = neighbor_vertexes[n ];
           break;
       }
   }

   path.vertices.push_back(next_vertex);
   path.distance += graph(current_location, next_vertex);
   visited.push_back(next_vertex);
   current_location = next_vertex;
}

  • choosing_probability — это массив, который представляет из себя следующее:
    Допустим есть три возможные вершины для посещения и вероятности их посетить равны соответственно {0.2 0.5 0.3}
    Таким образом в массиве choosing_probability будет лежать {0.2 0.7 1.0} — это нужно для того, можно было выбрать следующую вершину, сгенерировав случайное число от 0 до 1.
  • wish это массив желаний муравья, отвечающий за числитель в формуле "Вероятность перехода муравья из вершины i в j"
  • summary_wish отвечает за знаменатель в этой же формуле.

Главный метод, объединяющий функционал и решающий задачу коммивояжёра

AntPath AntColonyOptimization::SolveSalesmansProblem() {
    if (graph_.IsEmpty())
         return {};

    constexpr std::size_t kMaxIterationsWithoutImprovement = 100;
    const std::size_t kVertexesCount = graph_.getVertexesCount();
    std::size_t counter = 0;

    AntPath path;
    path.distance = kInf<double>;

    while (counter++ != kMaxIterationsWithoutImprovement) {
        Matrix<double> local_pheromone_update(kVertexesCount, 0.0);
        CreateAnts();

        for (auto &ant : ants_) {
            while (ant.can_continue)
                ant.MakeChoice(graph_, pheromone_, kAlpha_, kBeta_);

            auto ant_path = ant.path;
            if (ant_path.vertices.size() == kVertexesCount + 1) {
                 if (path.distance > ant.path.distance) {
                     path = std::move(ant.path);
                     counter = 0;
                }

                for (std::size_t v = 0; v != ant_path.vertices.size() - 1; ++v)
                    local_pheromone_update(ant_path.vertices[v], ant_path.vertices[v + 1]) += kQ_ / ant_path.distance;
            }
        }
        UpdateGlobalPheromone(local_pheromone_update);
    }
    return path;
}

  • Последовательно для каждого муравья, пока он может продолжать, выбираем ему следующую вершину.
  • Если он посетил все вершины, то в случае того, что текущий найденный путь длиннее пути, найденного муравьем, мы обновляем текущий путь и обнуляем счетчик.
  • Счетчик нужен для того, чтобы при нахождении нового (более короткого пути) алгоритм еще немного поработал, чтобы была возможность найти еще более короткий путь.
  • Вспомним условие, что феромоны откладывают только те муравьи, которые выполнили поставленную задачу, поэтому на локальное обновление феромона влияют только такие муравьи.