模拟退火算法的 C++ 实现

模拟退火算法的 C++ 实现模拟退火算法的C++实现最近的一个项目中需要实现个路径规划的算法,需要求得的路径的总长度尽可能的短。这就是典型的旅行商(TSP)问题了。解决这个问题的一个比较好用的方法就是模拟退火算法。网上关于用模拟退火算法解决TSP问题的文章挺多的,其中也有不少号称给出了C++代码。但是说句实话,这些代码中没有一个是按照面向对象的思想来实现了,并没有把模拟退火算法的框架封装好。因此,遇到一个新问题时,

大家好,欢迎来到IT知识分享网。

模拟退火算法的 C++ 实现

最近的一个项目中需要实现个路径规划的算法,需要求得的路径的总长度尽可能的短。这就是典型的旅行商(TSP)问题了。解决这个问题的一个比较好用的方法就是模拟退火算法。网上关于用模拟退火算法解决 TSP 问题的文章挺多的,其中也有不少号称给出了 C++ 代码。但是说句实话,这些代码中没有一个是按照面向对象的思想来实现了,并没有把模拟退火算法的框架封装好。因此,遇到一个新问题时,改写算法就很麻烦。

看了几个网上的实现后,发现 gsl 库给出的框架还是不错的。但是 gsl 是 C 语言的库,虽然是按照面向对象的思想来设计的,但是实现的接口真是挺繁琐的,用起来并不好用。因此,我就花了半天时间,把 gsl 里的相关代码扒了出来,重新封装到了一个类中。

模拟退火算法最早的思想是由 Metropolis 等人提出的。1983 年, Kirkpatrick 成功地将退火思想引入到组合优化领域。简单的说,模拟退火算法就是一种基于 Monte-Carlo 迭代求解策略的随机寻优算法。关于模拟退火算法的理论网上有很多文章介绍。这里就不多写了,今天主要说说如何C++ 实现。

模拟退火算法的核心功能封装到了 SimulatedAnnualingSolver 类中。

#ifndef SIMULATEDANNUALINGSOLVER_H
#define SIMULATEDANNUALINGSOLVER_H

#include<random>

class SimulatedAnnualingSolver
{
public:
    SimulatedAnnualingSolver(int rand_seed);
    void setParameters(double t_initial, double mu_t, double k = 1, double t_min = 1e-5, int iters_fixed_T = 100, double step_size = 1);
    void enablePrint(bool on) {m_print_position = on;}
    void solve();
protected:
    virtual double take_step(double step_size) = 0;
    virtual void undo_step() = 0;
    virtual void save_best() = 0;
    virtual double energy() = 0;
    virtual void print() = 0;
private:
    bool m_print_position;
    int m_iters_fixed_T;
    double m_step_size;
    double m_k;
    double m_t_initial;
    double m_mu_t;
    double m_t_min;
    std::mt19937 m_randGenerator;
    double boltzmann(double E, double new_E, double T, double k);
};

#endif // SIMULATEDANNUALINGSOLVER_H
#include "SimulatedAnnualingSolver.h"
#include <cmath>
#include <iostream>
#define GSL_LOG_DBL_MIN (-7.0839641853226408e+02)

SimulatedAnnualingSolver::SimulatedAnnualingSolver(int rand_seed)
    :m_t_initial(1),
      m_mu_t(1.01),
      m_k(1),
      m_t_min(0.01),
      m_iters_fixed_T(100),
      m_print_position(false),
      m_step_size(1),
      m_randGenerator(rand_seed)
{

}

void SimulatedAnnualingSolver::setParameters(double t_initial, double mu_t, double k, double t_min, int iters_fixed_T, double step_size)
{
    m_t_initial = t_initial;
    m_mu_t = mu_t;
    m_k = k;
    m_t_min = t_min;
    m_iters_fixed_T = iters_fixed_T;
    m_step_size = step_size;
}

void SimulatedAnnualingSolver::solve()
{
    int n_evals = 1, n_iter = 0;
    double E = energy();
    double best_E = E;
    save_best(); // 将当前状态存为最佳解

    double T = m_t_initial;
    double T_factor = 1.0 / m_mu_t;
    std::uniform_real_distribution<> dis(0, 1);
    while (1)
    {
        int n_accepts = 0;
        int n_rejects = 0;
        int n_eless = 0;

        for (int i = 0; i < m_iters_fixed_T; ++i)
        {
            double new_E = take_step (m_step_size);

            if(new_E <= best_E)
            {
                best_E = new_E;
                save_best();
            }

            ++n_evals;
            if (new_E < E)
            {
                if (new_E < best_E)
                {
                    best_E = new_E;
                    save_best();
                }
                /* yay! take a step */
                E = new_E;
                ++n_eless;
            }
            else if (dis(m_randGenerator) < boltzmann(E, new_E, T, m_k))
            {
                /* yay! take a step */
                E = new_E;
                ++n_accepts;
            }
            else
            {
                undo_step(); // 回退到上一个状态
                ++n_rejects;
            }
        }

        if (m_print_position)
        {
            printf ("%5d %7d %12g", n_iter, n_evals, T);
            print();
            printf (" %12g %12g\n", E, best_E);
        }

        /* apply the cooling schedule to the temperature */
        T *= T_factor;
        ++n_iter;
        if (T < m_t_min)
        {
            break;
        }
    }
}

inline double SimulatedAnnualingSolver::boltzmann(double E, double new_E, double T, double k)
{
    double x = -(new_E - E) / (k * T);
    /* avoid underflow errors for large uphill steps */
    return (x < GSL_LOG_DBL_MIN) ? 0.0 : exp(x);
}

这个类有几个纯虚函数。

  • take_step 用来在解空间中随机游走到一个新的状态。
  • undo_step 撤销最近的一次游走。
  • save_best 将当前的状态保存下来,最为当前最佳的解。
  • energy 能量函数,返回当前状态的目标函数值。(模拟退火算法就是求这个能量函数最小值所对应的状态)
  • print 输出当前状态。这个函数只是起辅助作用,如果不需要,可以实现为空函数。

对于我们一个具体的问题,我们只需要定义一个派生自这个类的新类,在新类中实现这些虚函数。

比如对于我的问题,一个简化了的旅行商问题,我建立了一个 TspSolver 的类。

#ifndef TSPSOLVER_H
#define TSPSOLVER_H

#include "SimulatedAnnualingSolver.h"
#include <QDebug>
#include <QVector>
#include <QPoint>
#include <random>

class tspSolver: public SimulatedAnnualingSolver { 
   
public:
    tspSolver(int rand_seed, QVector<QPoint> points);

    QVector<QPoint> result();
protected:
    double take_step(double step_size) override;
    void undo_step() override;
    void save_best() override;
    double energy() override;
    void print() override;
private:
    std::mt19937 m_randGen;

    QVector<QPoint> m_points;
    QVector<QPoint> m_best;

    int m_N;
    int m_n1;
    int m_n2;
};

#endif // TSPSOLVER_H
#include "tspsolver.h"

tspSolver::tspSolver(int rand_seed, QVector<QPoint> points)
    :SimulatedAnnualingSolver(rand_seed)
{
     std::random_device r;
     std::seed_seq seed2{r(), r(), r(), r(), r(), r(), r(), r()};
    m_randGen.seed(seed2);
    m_points = points;
    m_N = m_points.size();
}

void tspSolver::save_best()
{
    m_best = m_points;
}

QVector<QPoint> tspSolver::result()
{
    return m_best;
}

void tspSolver::print()
{
    qDebug() << m_points;
}

double tspSolver::take_step(double step_size)
{
    std::uniform_int_distribution<> dis(1, m_N - 1);
    m_n1 = dis(m_randGen);
    do
    {
        m_n2 = dis(m_randGen);
    }
    while(m_n2 == m_n1);
// qDebug() << "n1 = " << m_n1 << "n2 = " << m_n2;
    std::swap(m_points[m_n1], m_points[m_n2]);
    return energy();
}

void tspSolver::undo_step()
{
    std::swap(m_points[m_n1], m_points[m_n2]);
}

double tspSolver::energy()
{
    double e = 0;
    for(int i = 1; i < m_N; i ++)
    {
        QPoint a1 = m_points.at(i - 1);
        QPoint a2 = m_points.at(i);
        double x = abs(a1.x() - a2.x());
        double y = abs(a1.y() - a2.y());
        e += qMax(x, y);
    }
    return e;
}

大家可以看到,我这个 TSP 问题中的两点间的距离不是普通的直线距离,而是 X Y 方向投影距离的最大值。
take_step 函数实现的也很简答,就是随机选两个点,交换这两个点的坐标。undo_step 就是把这两个点的坐标再交换回去。

下面是一个算例,随便选了 9 个点。规划出的路径看起来还不错。
这里写图片描述

这个算例选了 32 个点。规划出的路径挺复杂,但是每一步的长度都是 1,所以是最优解。当然这个问题的最优解不止一个。这里得到的解是最优解中较复杂的一个情形。
这里写图片描述

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/10786.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信