蚁群算法(例题TSP问题)

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;
}
相关推荐
青山师1 小时前
数组与链表深度解析:从内存布局到工业级实践
数据结构·算法·链表·数组·算法与数据结构
alxraves1 小时前
超声图像斑点噪声处理算法
算法·健康医疗
呃呃本1 小时前
算法题(二分查找)
算法
吃好睡好便好1 小时前
在Matlab中绘制马鞍函数曲面图
开发语言·人工智能·学习·算法·matlab·信息可视化
wa的一声哭了1 小时前
Mit6.s081 Interrupts and device driver(中断和设备驱动)
linux·服务器·arm开发·数据库·python·gpt·算法
luyun0202021 小时前
实用小工具,吾爱出品
开发语言·c++·算法
NNYSJYKJ1 小时前
K12 学习常见问题破解:脑能思维链的算法与教育应用
学习·算法
2301_789015622 小时前
Linux:基础指令(二)
linux·运维·服务器·c语言·开发语言·c++·算法
闻缺陷则喜何志丹2 小时前
【区间合并】P7912 [CSP-J 2021] 小熊的果篮|普及+
c++·算法·洛谷·区间合并