「C++」蚁群算法求解最佳路径问题(一日游规划)
「C++」蚁群算法求解最佳路径问题(一日游规划)
1 题目描述
为游客提供行程规划服务,在满足时间、资金约束下,使得行程的总体质量较高、路程较短。
- 某地有一组旅游景点,景点具有名称、营业时间、门票价格、评分、游玩时长、**地理位置(经纬度坐标)**等信息。
- 该程序要根据游客的资金预算,自动生成一条合理的旅游路线。
上次采用遗传算法实现了这个功能,这次采用蚁群算法。
2 题目要求
-
“一日游”的时间从早上八点到晚上七点;
-
生成的线路应尽量用完以上的游玩时间,但不能超时;
-
不需要考虑出发点和结束点,即可以从任意景点出发、在任意景点结束;
-
以线路中所有景点的评分的均值作为该线路的评价得分;
-
以所有景点的门票价格之和作为该线路的价格;
-
以各景点间的距离之和作为线路的路程长度;
-
线路的质量优劣,按以下公式计算:
Q = 0.4 ∗ n o r m a l ( 评价得分 ) + 0.4 ∗ n o r m a l ( 价格 ) + 0.2 ∗ n o r m a l ( 路程长度 ) Q=0.4 * normal(评价得分) + 0.4 * normal(价格)+0.2 * normal(路程长度) Q=0.4∗normal(评价得分)+0.4∗normal(价格)+0.2∗normal(路程长度)
其中, n o r m a l ( 评价得分 ) = 评价得分 景点数量 ; 0 ≤ 评价得分 ≤ 5 normal(评价得分) = \frac{评价得分}{景点数量}; 0\le评价得分\le5 normal(评价得分)=景点数量评价得分;0≤评价得分≤5
n o r m a l ( 价格 ) = 预算 − 价格 预算 ; 0 ≤ 价格 ≤ 预算 normal(价格)=\frac{预算-价格}{预算}; 0≤价格\le预算 normal(价格)=预算预算−价格;0≤价格≤预算
n o r m a l ( 路程长度 ) = 最大长度 − 路程长度 最大路程 ; 0 ≤ 路程长度 ≤ 最大长度 normal(路程长度)=\frac{最大长度-路程长度}{最大路程}; 0\le路程长度\le最大长度 normal(路程长度)=最大路程最大长度−路程长度;0≤路程长度≤最大长度 -
输入:
200,50//说明:200为游客的预算,则生成的线路的价格不能超过该预算;50为游客一天最远的行程,则线路的路程长度不能超过该最远行程。
输出:
//如果可行,则输出一条较优的线路: 景点1、景点3、景点8、景点4 (0.82、4.2、180、50)//说明:先按顺序输出景点名称,括号内分别为线路的质量得分、评价得分、价格、路程长度。 //如果不可行,如输入的预算为0且无免费景点,则输出: error
3 解决方案
3.1 蚁群算法
3.1.1 算法简介
蚁群算法(Ant Clony Optimization, ACO)是一种群智能算法,它是由一群无智能或有轻微智能的个体(Agent)通过相互协作而表现出智能行为,从而为求解复杂问题提供了一个新的可能性。蚁群算法最早是由意大利学者Colorni A., Dorigo M. 等于1991年提出。经过20多年的发展,蚁群算法在理论以及应用研究上已经得到巨大的进步。
蚁群算法是一种仿生学算法,是由自然界中蚂蚁觅食的行为而启发的。在自然界中,蚂蚁觅食过程中,蚁群总能够按照寻找到一条从蚁巢和食物源的最优路径。[1]
3.1.2 算法逻辑
- 放一只蚂蚁在一个起点处,由蚂蚁来决定下一步走哪一个节点;
- 蚂蚁经过一条路线,会留下信息素(Flomone),为后来的蚂蚁提供选择的依据;
- 信息素随时间衰减;
- 每一只蚂蚁经过这条路线都会带来信息素,以减缓信息素的衰减效果;
- 每一次迭代中的蚂蚁都会完成走完一条路线,一次迭代结束后根据蚂蚁走过路线的情况更新信息素,再进行下一次迭代;
3.1.3 蚂蚁的策略
- 在其他情况相同的情况下,蚂蚁倾向于选择路程更短的节点;
- 在路程相同的情况下,蚂蚁倾向于选择信息素浓度更高的路线;
- 蚂蚁通过轮盘赌的方式选择下一个节点,选择一个节点的概率为:
P i j = τ i j α η i j β Σ z τ i z α η i z β P_{ij}=\frac{\tau^\alpha_{ij}\eta^\beta_{ij}}{\Sigma_{z}\tau^\alpha_{iz}\eta^\beta_{iz}} Pij=Σzτizαηizβτijαηijβ
其中,z是所有剩余的可选择的节点, τ i j \tau_{ij} τij为第 i i i个节点到第 j j j个节点的路径上的信息素, η i j \eta_{ij} ηij为第 i i i个节点到第 j j j个节点的“能见度”(路程的倒数), α 和 β \alpha和\beta α和β分别为信息素和“能见度”的重要系数;
α \alpha α和 β \beta β影响蚁群的策略,需要一个适当的比例;
- alpha=0时,无视 flomone,蚂蚁完全根据能见度(上一次路线得分)做判断,容易陷入局部最优解;
- beta=0时,无视上次路线得分,蚂蚁完全根据flomone做判断,收敛速度快,但很难达到最优解;
公式解释: 从当前节点 i i i到节点 j j j的概率为这两个节点间的信息素与能见度的乘积,占当前节点到所有可选择节点的比例。
- 信息素的更新由下面的公式决定:
τ i j ′ = ρ τ i j + Δ τ \tau_{ij}'=\rho\tau_{ij}+\Delta\tau τij′=ρτij+Δτ
其中, ρ \rho ρ为信息素的衰减系数, Δ τ \Delta\tau Δτ为一轮迭代后蚂蚁带来的信息素变化。
Δ τ = Q L \Delta\tau=\frac{Q}{L} Δτ=LQ
Q Q Q为一个常量,可取1; L L L为这一只蚂蚁选择的路线的路程;每一只蚂蚁都会带来信息素的变化,则 Δ τ \Delta\tau Δτ应是所有变化的总和。
3.1.4 蚁群算法在本例中的应用
- 初始信息素浓度都为1;
- 由于本问题不是求解最短路径,而是有评分作为指标找出最优的路线,所以能见度定义为 η = 0.4 ∗ n o r m a l ( 评价得分 ) + 0.4 ∗ n o r m a l ( 价格 ) + 0.2 ∗ n o r m a l ( 路程长度 ) \eta=0.4 * normal(评价得分) + 0.4 * normal(价格)+0.2 * normal(路程长度) η=0.4∗normal(评价得分)+0.4∗normal(价格)+0.2∗normal(路程长度);
- 将信息素的变化量也相应改为 Δ τ = η Q \Delta\tau=\eta Q Δτ=ηQ;
3.2 构思
本质上需要用到的信息与《「C++」遗传算法求解最佳路径问题——“一日游”行程规划程序》相似,所以大部分内容不需要修改。主要的差异在于实现算法的逻辑。
3.2.1 核心定义的类
需要定义几个主要的类:
- scenicSpot类用于记录一个景点的信息(景点名、开放时间、关闭时间、价格、坐标等);
- Ant类用于记录条路线的信息(路线、路线评价、路线价格、路线路程等);
- antColony类用于记录蚁群和信息素等信息。
3.2.2 常用类和方法
除此之外,还有一些数据类型和功能被反复用到,处理成单独的类和函数比较方便管理和计算:
- coordinate类,记录坐标,并实现坐标的计算功能;
- mytime类,记录时间,只记录时、分、秒,方便时间的运算;
- selector_sort函数,选择排序;
- find_vector函数,在vector中查找元素并返回下标;
- vector容器存储路线节点,方便变化长度,作为辅助作用;
- 除此之外还可重载矩阵的四则运算符,方便做矩阵的计算;
4 代码概览
4.1 mytime类
class mytime
{
private:
int hour;
int min;
int sec;
public:
mytime() { this->hour = this->min = this->sec = 0; }
mytime(int h, int m = 0, int s = 0);
mytime(string t);
mytime(double t);
~mytime(){};
int H() const { return this->hour; }//返回小时
int M() const { return this->min; }//返回分钟
int S() const { return this->sec; }//返回秒
void H(int h) { this->hour = h; }//修改小时
void M(int m) { this->min = m; }//修改分钟
void S(int s) { this->sec = s; }//修改秒
int to_sec() const;//时间转为整数
mytime &operator=(const mytime &m);//重载赋值运算符
mytime &operator+=(const mytime &y);//重载自增运算符
friend mytime toMytime(int sec);//整型转为时间
friend ostream &operator<<(ostream &out, const mytime m);//重载输出运算符
//重载四则运算
friend mytime operator+(const mytime &m, const mytime &y);
friend mytime operator-(const mytime &m, const mytime &y);
friend double operator/(const mytime &m, const mytime &y);
friend double operator*(const int &m, const mytime &y);
//重载逻辑运算
friend bool operator>(const mytime &m, const mytime &y);
friend bool operator<(const mytime &m, const mytime &y);
friend bool operator>=(const mytime &m, const mytime &y);
friend bool operator<=(const mytime &m, const mytime &y);
friend bool operator==(const mytime &m, const mytime &y);
friend bool operator!=(const mytime &m, const mytime &y);
};
4.2 coordinate类
class coordinate
{
private:
float abscissa; // 横坐标
float ordinate; // 纵坐标
public:
coordinate() {}
coordinate(float x, float y);
coordinate(int x, int y);
coordinate(double x, double y);
float x() const { return abscissa; }//返回横坐标
float y() const { return ordinate; }//返回纵坐标
// 赋值
coordinate &operator=(const coordinate &coord);
// 计算距离
float distance(coordinate c);
friend ostream &operator<<(ostream &out, const coordinate c);
};
4.3 选择排序
template <typename T>
void selection_sort(std::vector<T> &arr)
{
for (int i = 0; i < arr.size() - 1; i++)
{
int min = i;
for (int j = i + 1; j < arr.size(); j++)
if (arr[j] < arr[min])
min = j;
std::swap(arr[i], arr[min]);
}
}
4.4 查找vector元素
// 查找
template <typename T>
int find_vector(vector<T> tlist, T t)
{
for (int i = 0; i < tlist.size(); i++)
{
if (tlist[i] == t)
{
return i;
}
}
return -1;
}
4.5 scenicSpot类
class scenicSpot
{
private:
string scenicname; // 景点名称
mytime busi_hour; // 营业时间
mytime clos_hour; // 歇业时间
float scenicprice; // 门票价格
float scenicscore; // 评分
mytime scenicduration; // 游玩时长
coordinate sceniccoord; // 景点坐标
public:
scenicSpot() {}
scenicSpot(string name, mytime busi = mytime(8), mytime clos = mytime(19), float price = 100, float score = 0, mytime duration = mytime(1), coordinate coord = coordinate(0, 0));
~scenicSpot(){};
// 返回各项成员
string name() const { return this->scenicname; }
mytime businesshour() const { return this->busi_hour; }
mytime closerhour() const { return this->clos_hour; }
float price() const { return this->scenicprice; }
float score() const { return this->scenicscore; }
mytime duration() const { return this->scenicduration; }
coordinate coord() const { return this->sceniccoord; }
// 重载运算符
scenicSpot &operator=(const scenicSpot s);
bool operator==(const scenicSpot &s);
friend ostream &operator<<(ostream &out, const scenicSpot &s);
};
4.6 class Ant核心函数
class Ant
{
private:
vector<scenicSpot> nodes; // 基因序列,路线
double Antstar; // 路线评价
double Antdistance; // 路线路程
double Antprice; // 路线票价
mytime Antendtime; // 路线游玩时间
double Antscore; // 路线得分
public:
//略去不重要的函数
// 计算评分、路程等参数
void calculate();
// 其他操作
vector<vector<double>> deltaTau(); // 返回这只蚂蚁带来的flomone变化
bool isExist(scenicSpot spot) const;// 判断路线是否已经存在该景点
bool append();// 增加路线长度
double calcscore(double budget, double maxtrip);// 计算线路得分
// 重载算符
friend ostream &operator<<(ostream &out, const Ant &c);
Ant &operator=(const Ant s);
};
4.7 class antColony核心函数
class antColony
{
private:
/* data */
vector<Ant> ants; // 蚂蚁
vector<Ant> best_ant; // 存储迭代中的最佳路径,防止好的结果被刷新掉(取20个)
vector<vector<double>> flomone; // 弗洛蒙浓度(信息素)
vector<scenicSpot> spots; // 景点信息
vector<vector<double>> spotsmap; // 景点地图(路程信息)
double bugdet; // 预算
double maxtrip; // 最大路程
int maxgen; // 最大迭代次数
ofstream fout; // 把过程写入文件方便查看
public:
//略去其他不重要的成员函数
int roulette(); // 轮盘赌
bool import(string path); // 从文件导入景点信息
bool calcDist();// 计算景点间距离
bool initFlomone(); // 初始化flomone
bool initAnt();// 初始化蚂蚁
bool updateFlomone(); // 更新Flomone
void Run();// 开始运行
};
4.8 一些参数
#define Q 0.1 // 系统常数(取值任意)
#define rho 0.3 // 挥发系数
#define alpha 0.5 // flomone 重要程度系数
#define beta 1.5 // 能见度重要程度系数(此处能见度定义为路线得分)
#define tau 1 // flomone浓度初始值
#define ants_num 10// 一个起点放置的蚂蚁数量
4.9 main
#include "antColony.hpp"
int main(int argc, char *argv[])
{
antColony antcolony;
antcolony.Run();
return 0;
}
5 参考
[1] 蚁群算法(Ant Colony Optimization)
[2] 【数之道 04】解决最优路径问题的妙招-蚁群ACO算法
[3] 「C++」遗传算法求解最佳路径问题——“一日游”行程规划程序