1. 思想
1.1 群体智能
蚁群算法也是一种群体智能算法,群体智能指的是,多个个体按某种规则协作,对某一个问题进行优化,类似地,粒子群算法,遗传算法也都属于群体智能算法
1.2 蚁群思想

生物学家观察蚂蚁的行为,会发现蚂蚁虽然作为独立的个体智商很低,但是协作时可以展现出复杂的行为,比如野外觅食时,蚂蚁通过简单的交流,就能找到最优路线,并让整个族群都知道这条路,于是最早研究启发式算法的学者尝试把蚁群寻路的过程。蚁群寻路就是一个图论问题,于是最早被在图论启发式算法中被引入。
想要实现蚁群算法,首先需要了解蚂蚁到底是如何传递信息的,生物学研究发现,蚂蚁会在走过的地方留下信息素,后来的蚂蚁会根据信息素的浓度,决定跟随哪条路,在没有信息素时,蚂蚁会随机探索。
信息素留在路径上,相当于让后来的蚂蚁寻路时可以借鉴先来的蚂蚁的经验,不必从零开始。
但也不能让历史经验压制了新路径的探索,因此每次新的寻路时,历史经验和当前探索,按一个权重融合;并且历史经验随着时间会逐渐减弱,这对应到现实就是信息素会逐渐挥发。
1.3 蚁群算法的优势
虽然无法保证得到精确解,但是实际应用中往往不需要要精确解,只需要近似解。正解算法用时往往是指数增长的,比如TSPTSPTSP问题下的状态压缩DPDPDP,复杂度O(n2n)O(n2^n)O(n2n),数据稍微增大就做不了了,而蚁群算法等启发式算法,在数据增大后仍然可以在多项式时间内解决,代价只是牺牲了一点精度。
2. 实现
2.1 一般实现思路
大体思路是:
- 每一轮生成一群蚂蚁,每个蚂蚁都根据当前的信息素浓度,随机找到一个路径
- 寻路的过程是,对于当前点的所有出边,每个出边都有实际长度和信息素两种信息,加权融合一下。然后并不是直接取最优,容易陷入历史经验,这里引入一点随机性,根据权重进行一个轮盘赌,也就是随机抽奖,权重越高的被抽中的概率越大。走过的点标记,不重复访问,重复这个过程,直到找到一条合法路径。
- 全部路径上的信息素同步蒸发
- 这一轮的蚂蚁,在他们找到的路径上留下信息素,越好的路径留下的信息素浓度越高
2.2 例题
例题选择https://www.luogu.com.cn/problem/P1433,经典TSP问题
对于TSPTSPTSP问题,寻找路径必须是一个经过所有点,回到起点的简单路径。我们把前述的寻路机制封装成一个函数choose_next,调用nnn次,由于有visvisvis数组记录访问过的点,不允许重复访问,这样就得到了一个TSPTSPTSP解。需要把这个路径保存下来,后面拿来更新路径信息素。
复杂度有必要分析一下,nnn轮搜索,每轮生成o(n)o(n)o(n)只蚂蚁,每只蚂蚁构造一条路径,需要调用nnn次choose_next,每次调用,由于这是完全图,是在nnn条出边里选一个,复杂度O(n)O(n)O(n),故整体复杂度O(n4)O(n^4)O(n4)左右,空间更是只用了O(n2)O(n^2)O(n2)存邻接矩阵和信息素矩阵,相比DPDPDP一般有巨大的优势
接下来的问题就是精度怎么样,调参是否方便,在1e−31e-31e−3的精度要求下,简单的调参就能通过全部测试点,跑的也很快,基本全面碾压正解状压DPDPDP了。
2.3 例题运行结果
这是蚁群的用时

这是DPDPDP的用时,DPDPDP需要在状态里保存经过的点集,以及当前的位置,状态数O(n2n)O(n2^n)O(n2n),转移时还需要枚举下一个点,整体复杂度O(n22n)O(n^22^n)O(n22n),空间用的也多,O(n2n)O(n2^n)O(n2n),内存局部性差。慢是正常的。

2.4 实现细节(加权轮盘赌)
这里实现上有点小巧思,第一点是,如何实现加权的抽奖?
实现思路是,先计算所有点的权重之和sumsumsum
c
std::vector<double> weight(n_ + 1, 0.0);
double sum = 0.0;
for (int city = 1; city <= n_; ++city) {
if (visited[city]) {
continue;
}
// tau 是信息素强度,eta 是启发函数(这里取距离的倒数)。
// 距离越近、信息素越大,被选中的概率通常越高。
const double tau = std::pow(pheromone_[current][city], alpha_);
const double eta = std::pow(1.0 / (dist_[current][city] + 1e-9), beta_);
weight[city] = tau * eta;
sum += weight[city];
}
再生成一个[0,sum][0,sum][0,sum]内的随机数,把每个点看成[0,sum][0,sum][0,sum]内的一个子区间,长度就是权重,看随机数落在哪个点对应的区间内,就认为抽中了哪个点。具体实现上就是枚举全部点,把随机数减去权重,哪一次减去后,随机数变成负数了,说明随机数落在这个点的区间内
c
// 轮盘赌选择:按权重比例随机决定下一座城市。
std::uniform_real_distribution<double> pick(0.0, sum);
double target = pick(rng_);
for (int city = 1; city <= n_; ++city) {
if (visited[city]) {
continue;
}
target -= weight[city];
if (target <= 0.0) {
return city;
}
}
2.5 实现细节(two_opt后处理)
路径规划的经典后处理,找到一个解后,可以随机交换其中一些点的顺序,看是否更优,这样不需要太大的开销,也能把答案进一步优化。
这里two_opt指的就是,假设我们找到的路径中包含(a,b)...(c,d)(a,b)...(c,d)(a,b)...(c,d)两条边,可以改成(a,c)...(b,d)(a,c)...(b,d)(a,c)...(b,d),中间的路径原本是b→cb\rightarrow cb→c,改后变成c→bc\rightarrow bc→b了,但由于这是无向图吗,没有影响。
我们在这个后处理阶段,已经有了一个路径,那么枚举其中两个点i,ji,ji,j,令i,i+1,j,j+1i,i+1,j,j+1i,i+1,j,j+1分别为a,b,c,da,b,c,da,b,c,d,进行上面的优化即可,复杂度O(n2)O(n^2)O(n2),没有超过前面的构造路径复杂度,不会拖慢整体复杂度,只是增加了一点常数。
而且实际应用中,不一定要在每一轮,对每个点都进行这个后处理。原本更优的方案,后处理后大概率仍然更优,所以为降低常数,可以只对每轮最优的路径后处理。并且不一定要每一轮都后处理,后处理应该用在基础策略的性能已经被榨干后再用,性价比最高,所以检测每一轮优化效果,如果多轮没有进展,再考虑用后处理优化一下。
two_opt后处理实现如下
c
void two_opt(std::vector<int>& route, double& length) const {
// 2-opt 是常见的 TSP 局部搜索:尝试翻转一段区间,若能缩短路径则接受。
bool improved = true;
int rounds = 0;
while (improved && rounds < 12) {
improved = false;
++rounds;
for (int i = 0; i < n_ - 1; ++i) {
const int a = (i == 0 ? 0 : route[i - 1]);
const int b = route[i];
for (int j = i + 1; j < n_; ++j) {
const int c = route[j];
const int d = (j + 1 == n_ ? -1 : route[j + 1]);
const double old_edges =
dist_[a][b] + (d == -1 ? 0.0 : dist_[c][d]);
const double new_edges =
dist_[a][c] + (d == -1 ? 0.0 : dist_[b][d]);
if (new_edges + 1e-12 < old_edges) {
// 翻转 [i, j] 这段路径,相当于做一次 2-opt 交换。
std::reverse(route.begin() + i, route.begin() + j + 1);
length += new_edges - old_edges;
improved = true;
}
}
}
}
length = route_length(route);
}
2.6 例题完整代码
c
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>
#include <numeric>
#include <random>
#include <vector>
struct Point {
double x;
double y;
};
struct AntResult {
std::vector<int> route;
double length = std::numeric_limits<double>::infinity();
};
class AntColonyTSP {
public:
explicit AntColonyTSP(std::vector<Point> points)
: points_(std::move(points)),
n_(static_cast<int>(points_.size()) - 1),
rng_(static_cast<unsigned>(
std::chrono::steady_clock::now().time_since_epoch().count())) {
build_distance();
// 初始时所有边的信息素一致,后续再由好路径逐步强化。
pheromone_.assign(n_ + 1, std::vector<double>(n_ + 1, initial_pheromone_));
}
double solve() {
AntResult global_best;
const int restart_count = 1;
for (int restart = 0; restart < restart_count; ++restart) {
reset_pheromone();
AntResult restart_best = run_colony();
if (restart_best.length < global_best.length) {
global_best = restart_best;
}
}
return global_best.length;
}
private:
void build_distance() {
// 预处理两两距离,避免构造路径时重复算欧氏距离。
dist_.assign(n_ + 1, std::vector<double>(n_ + 1, 0.0));
for (int i = 0; i <= n_; ++i) {
for (int j = i + 1; j <= n_; ++j) {
const double dx = points_[i].x - points_[j].x;
const double dy = points_[i].y - points_[j].y;
const double d = std::hypot(dx, dy);
dist_[i][j] = d;
dist_[j][i] = d;
}
}
}
void reset_pheromone() {
// 重置到统一初值,相当于重新开始一次独立搜索。
for (int i = 0; i <= n_; ++i) {
std::fill(pheromone_[i].begin(), pheromone_[i].end(), initial_pheromone_);
}
}
AntResult run_colony() {
AntResult best;
// 这里的参数偏保守,优先控制时限。
const int ant_count = std::max(20, n_ * 3);
const int iterations = std::max(60, n_ * 8);
int stagnant_rounds = 0;
for (int iter = 0; iter < iterations; ++iter) {
std::vector<AntResult> ants;
ants.reserve(ant_count);
bool improved_this_round = false;
for (int ant = 0; ant < ant_count; ++ant) {
// 每只蚂蚁独立构造一条完整路径。
AntResult current = construct_route();
ants.push_back(current);
}
std::sort(ants.begin(), ants.end(),
[](const AntResult& lhs, const AntResult& rhs) {
return lhs.length < rhs.length;
});
// 只对本轮最优的少数路径做局部优化,避免每只蚂蚁都精修导致超时。
const int local_search_count = std::min(2, ant_count);
for (int i = 0; i < local_search_count; ++i) {
two_opt(ants[i].route, ants[i].length);
}
std::sort(ants.begin(), ants.begin() + local_search_count,
[](const AntResult& lhs, const AntResult& rhs) {
return lhs.length < rhs.length;
});
if (ants[0].length < best.length) {
best = ants[0];
improved_this_round = true;
}
// 长时间没进展时,偶尔再对当前全局最优做一次精修。
if (!improved_this_round && iter % 12 == 11) {
AntResult refined_best = best;
two_opt(refined_best.route, refined_best.length);
if (refined_best.length < best.length) {
best = refined_best;
improved_this_round = true;
}
}
evaporate();
deposit(ants[0], 1.0);
deposit(best, 2.8);
const int elite_count = std::min(4, ant_count);
for (int i = 1; i < elite_count; ++i) {
deposit(ants[i], 0.45 / i);
}
if (improved_this_round) {
stagnant_rounds = 0;
} else {
++stagnant_rounds;
if (stagnant_rounds >= 20) {
break;
}
}
}
return best;
}
AntResult construct_route() {
AntResult result;
result.route.reserve(n_);
std::vector<char> visited(n_ + 1, 0);
// 题目默认从原点 0 出发,依次接上还未访问的点。
int current = 0;
double total = 0.0;
for (int step = 0; step < n_; ++step) {
int next = choose_next(current, visited);
visited[next] = 1;
result.route.push_back(next);
total += dist_[current][next];
current = next;
}
result.length = total;
return result;
}
int choose_next(int current, const std::vector<char>& visited) {
std::vector<double> weight(n_ + 1, 0.0);
double sum = 0.0;
for (int city = 1; city <= n_; ++city) {
if (visited[city]) {
continue;
}
// tau 是信息素强度,eta 是启发函数(这里取距离的倒数)。
// 距离越近、信息素越大,被选中的概率通常越高。
const double tau = std::pow(pheromone_[current][city], alpha_);
const double eta = std::pow(1.0 / (dist_[current][city] + 1e-9), beta_);
weight[city] = tau * eta;
sum += weight[city];
}
// 理论上 sum 应该为正,这里只是数值异常时的兜底保护。
if (sum <= 0.0) {
for (int city = 1; city <= n_; ++city) {
if (!visited[city]) {
return city;
}
}
}
// 轮盘赌选择:按权重比例随机决定下一座城市。
std::uniform_real_distribution<double> pick(0.0, sum);
double target = pick(rng_);
for (int city = 1; city <= n_; ++city) {
if (visited[city]) {
continue;
}
target -= weight[city];
if (target <= 0.0) {
return city;
}
}
for (int city = n_; city >= 1; --city) {
if (!visited[city]) {
return city;
}
}
return 1;
}
double route_length(const std::vector<int>& route) const {
double total = 0.0;
int prev = 0;
for (int city : route) {
total += dist_[prev][city];
prev = city;
}
return total;
}
void two_opt(std::vector<int>& route, double& length) const {
// 2-opt 是常见的 TSP 局部搜索:尝试翻转一段区间,若能缩短路径则接受。
bool improved = true;
int rounds = 0;
while (improved && rounds < 12) {
improved = false;
++rounds;
for (int i = 0; i < n_ - 1; ++i) {
const int a = (i == 0 ? 0 : route[i - 1]);
const int b = route[i];
for (int j = i + 1; j < n_; ++j) {
const int c = route[j];
const int d = (j + 1 == n_ ? -1 : route[j + 1]);
const double old_edges =
dist_[a][b] + (d == -1 ? 0.0 : dist_[c][d]);
const double new_edges =
dist_[a][c] + (d == -1 ? 0.0 : dist_[b][d]);
if (new_edges + 1e-12 < old_edges) {
// 翻转 [i, j] 这段路径,相当于做一次 2-opt 交换。
std::reverse(route.begin() + i, route.begin() + j + 1);
length += new_edges - old_edges;
improved = true;
}
}
}
}
length = route_length(route);
}
void evaporate() {
// 信息素挥发:旧经验逐步衰减,避免算法过早锁死在某条路上。
for (int i = 0; i <= n_; ++i) {
for (int j = 0; j <= n_; ++j) {
pheromone_[i][j] *= (1.0 - evaporation_);
if (pheromone_[i][j] < min_pheromone_) {
pheromone_[i][j] = min_pheromone_;
}
}
}
}
void deposit(const AntResult& ant, double factor) {
if (!std::isfinite(ant.length) || ant.route.empty()) {
return;
}
// 路径越短,增量越大;优秀路径会让对应边在后续更容易被选中。
const double delta = factor / ant.length;
int prev = 0;
for (int city : ant.route) {
pheromone_[prev][city] += delta;
pheromone_[city][prev] += delta;
prev = city;
}
}
std::vector<Point> points_;
int n_;
std::vector<std::vector<double>> dist_;
std::vector<std::vector<double>> pheromone_;
std::mt19937 rng_;
const double alpha_ = 1.1;
const double beta_ = 4.2;
const double evaporation_ = 0.24;
const double initial_pheromone_ = 1.0;
const double min_pheromone_ = 1e-6;
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<Point> points(n + 1);
points[0] = {0.0, 0.0};
for (int i = 1; i <= n; ++i) {
std::cin >> points[i].x >> points[i].y;
}
AntColonyTSP solver(points);
const double answer = solver.solve();
std::cout << std::fixed << std::setprecision(2) << answer << '\n';
return 0;
}