C++模拟蚁群

本文介绍的程序使用SDL库,模拟蚁群找食物,背后的算法模型是简化的蚁群算法。我将采取自上而下的方式介绍。

SDL库介绍

安装

我在另一篇博客已经详细介绍了SDL库的安装与使用:C++实现图形化2048小游戏。这次要使用的是SDL2和SDL2_image两个子库。

设置编译参数

我使用的是VS Code,需要在task.json文件夹里设置args参数"-lSDL2","-lSDL2_image",代表编译时要使用到这2个库。

主函数介绍

在使用SDL库过程中,总体按照初始化SDL → \to 初始化窗口 → \to 构建窗口渲染器 → \to 绘制图形 → \to 销毁的步骤。
在使用SDL_image库时,总体按照初始化 → \to 加载图片成表面 → \to 生成纹理 → \to 绘制图片的步骤。

int main(int argc, char* argv[]) {
    int n;
    cout << "请输入蚂蚁最大种群数:" << flush;
    cin >> n;
    // 初始化SDL显示图像功能
    if (SDL_Init(SDL_INIT_VIDEO)) {
        cerr << "SDL could not initialize! SDL_Error:" << SDL_GetError() << endl;
        return 1;
    }
    // 初始化图片加载功能
    if (!IMG_Init(IMG_INIT_PNG)) { //初始化IMG组件失败
        cerr << "SDL_image could not initialize! SDL_image Error:" << IMG_GetError() << endl;
        IMG_Quit();
        return 1;
    }
    // 初始化窗口
    SDL_Window* window = SDL_CreateWindow("蚁群模拟", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, N*DIM, N*DIM, SDL_WINDOW_SHOWN);
    if (window == NULL) { //初始化窗口失败
        cerr << "Window could not be created! SDL_Error:" << SDL_GetError() << endl;
        IMG_Quit();
        SDL_Quit();
        return 1;
    }
    // 构建窗口渲染容器
    SDL_Renderer* rend = SDL_CreateRenderer(window, -1, 0);
    if (!rend) {
        cerr << "Unable to render! SDL_Render Error:" << SDL_GetError() << endl;
        SDL_DestroyWindow(window);
        IMG_Quit();
        SDL_Quit();
        return 1;
    }
    //初始化渲染器
    SDL_SetRenderDrawColor(rend, 255, 255, 255, 255); //白色
    SDL_RenderClear(rend);
    //加载第1种图片
    SDL_Surface *image0 = IMG_Load("ant0.png"); //打开图片
    if (image0 == NULL) { //加载图片失败
        cerr << "Unable to load image! SDL_image Error:" << IMG_GetError() << endl;
        SDL_DestroyRenderer(rend);
        SDL_DestroyWindow(window);
        IMG_Quit();
        SDL_Quit();
        return 1;
    }
    //加载第2种图片
    SDL_Surface *image1 = IMG_Load("ant1.png");
    if (image1 == NULL) { //加载图片失败
        cerr << "Unable to load image! SDL_image Error:" << IMG_GetError() << endl;
        SDL_DestroyRenderer(rend);
        SDL_DestroyWindow(window);
        IMG_Quit();
        SDL_Quit();
        return 1;
    }
    //生成纹理
    SDL_Texture *imageTexture0 = SDL_CreateTextureFromSurface(rend, image0);
    SDL_Texture *imageTexture1 = SDL_CreateTextureFromSurface(rend, image1);

    //初始化伪随机数生成器
    srand(time(NULL));

    //初始化蚂蚁之家
    Home::x = rand() % (N - 2) + 1; //不要在边沿放置
    Home::y = rand() % (N - 2) + 1;
    Home::render = rend;
    Home::texture0 = imageTexture0;
    Home::texture1 = imageTexture1;

    //初始化地面
    Ground::INFO_MAX = n / 2 * Ant::INFO;
    Ground::block_home[Home::x * N + Home::y] = Ground::INFO_MAX; //蚂蚁之家的信息素浓度
    Ground::render = rend;

    //生成食物
    int foodAmount = 5; //食物数量
    for (int i = 0; i < foodAmount; i++) {
        int p = rand() % (N * N);
        if (p == Home::x * N + Home::y) continue; //不能放置在蚂蚁窝
        cout << "生成食物:" << p / N << "," << p % N << endl;
        Foods::position[p] = 200; //随机位置,默认大小为200
    }
    Foods::render = rend;

    //一次性生成蚂蚁种群的一半
    for (int i = 0; i < n/2; i++) {
        Home::addAnt(); //手动添加蚂蚁
    }
    //增加用户交互性
    SDL_Event event;
    //刷新
    while (true) {
        SDL_PollEvent(&event); //等待事件
        if (event.type == SDL_QUIT) { // 退出
            break;
        } else if (event.type == SDL_KEYDOWN) { // 按键
            //防抖
            while(true) {
                SDL_PollEvent(&event); //等待事件
                if ((event.type == SDL_KEYUP)) break;
            }
            switch (event.key.keysym.sym) { //检测按了哪个方向键
                case SDLK_SPACE: //按下了空格键
                    if (Home::ants.size() < n)  {
                        Home::addAnt(); //手动添加蚂蚁
                        if (Home::ants.size() % 2 == 0) { //保持奇数个蚂蚁
                            Home::addAnt();
                        }
                        cout << "蚂蚁数量:" << Home::ants.size() << endl;
                    }
                break;
            }
        }
        display(rend); //显示
        Home::explore(); //让没食物的蚂蚁探索
    }

    //销毁
    SDL_FreeSurface(image0);
    SDL_FreeSurface(image1);
    SDL_DestroyTexture(imageTexture0);
    SDL_DestroyTexture(imageTexture1);
    SDL_DestroyRenderer(rend);
    SDL_DestroyWindow(window);
    IMG_Quit();
    SDL_Quit();
}

这里有个display函数,就是调用各个类的显示,先画地面,再画食物,最后画蚂蚁窝和蚂蚁:

//全局显示函数
void display(SDL_Renderer * rend) {
    Ground::show();
    Foods::show();
    Home::show(); //画蚂蚁窝和蚂蚁
    SDL_RenderPresent(rend); //刷新画布
    SDL_Delay(FREQUENCE); //延时,定义的宏,随便填,根据人眼分辨机能,可以设置为20ms
}

数据结构

蚂蚁之家类Home,采用静态类的模式,有3个静态函数,分别都会调用蚂蚁类Ant相应的函数:

//ant.h
//蚂蚁之家类=======================================
class Home {
    public:
    static SDL_Renderer *render; //屏幕渲染器指针
    static SDL_Texture *texture0, *texture1; //无食物和有食物状态的纹理
    static deque<Ant> ants; //所有蚂蚁
    static int x, y; //位置

    static void addAnt(); //添加蚂蚁
    static void show(); //图像显示蚂蚁之家和蚂蚁
    static void explore(); //模拟蚂蚁探索
};

蚂蚁类Ant,里面设置多个坐标,(x,y)代表蚂蚁在数组坐标,其实就是地面坐标,一会介绍Ground类包含了地板数组;(px,py)代表图像显示坐标,因为要实现蚂蚁移动的动画,它代表的是绘制蚂蚁图片的位置;(dx,dy)代表当前蚂蚁的目标坐标,也是数组坐标。其他的请看注释:

//ant.h
//蚂蚁类===============================
class Ant {
    private:
    SDL_Renderer *render; //屏幕渲染器指针
    SDL_Texture *texture0, *texture1; //无食物和有食物状态的纹理
    public:
    int x, y; //当前位置
    int px, py; //图像显示位置
    int dx, dy; //目标格子坐标
    int direction; //方向
    bool food; //有无食物
    int info; //信息素浓度
    static double INFO; //最大信息素浓度
    static int VOCITY; //速度
    static double IMPORTANT; //蚂蚁信息重要程度因子
    //基础函数部分
    Ant(int, int, SDL_Renderer *, SDL_Texture *, SDL_Texture *); //构造函数
    void emitInfo(); //在当前位置留下信息素
    void directionToPos(int, int *, int *); //将方向转化成下步坐标
    void sniffDirection(); //获取周围空间信息素并赋予方向,但不能走回头路,且有机率开创新路径
    void moveToNext(); //移动蚂蚁至下一个砖块
    void show(); //图像显示函数
    //高级函数部分
    void explore(); //探索
};

地面类,想在地板上根据蚂蚁信息素浓度标记地板颜色,所以使用了INFO_MAX用于表示信息素最大可以到的浓度,定义了2个数组block_home表示蚂蚁释放的回家信息素,它们外出找食物时会在地板上留下这种信息素,另1个数组block_food表示蚂蚁找到食物后释放的信息素,在回家的途中会进行释放:

//ant.h
//地面类===============================
class Ground {
    public:
    static int INFO_MAX; //最大信息浓度
    static SDL_Renderer *render; //屏幕渲染器指针
    static double block_home[N*N]; //共10X10块地砖,每块的数值是蚂蚁释放家信息素的浓度,而坐标就是地砖的坐标
    static double block_food[N*N]; //释放的食物信息素
    static void show(); //地板显示函数
    static void evade(); //气味消散函数
};

最后一个是食物类Foods,比较简单:

//ant.h
//食物类==================================
class Foods {
    public:
    static SDL_Renderer *render; //屏幕渲染器指针
    static int position[N*N]; //共16X16块地砖可能有食物,其数值代表食物大小
    static void show(); //食物显示函数
};

数学模型介绍

推荐看一篇文章数学建模学习笔记(一)蚁群算法(MATLAB),因为我设置的场景是正方形砖块平铺,蚂蚁也只有上、下、左、右4个方向,且距离都相等设为1,于是我对该模型进行了简化:
P k = τ k α ⋅ 1 ∑ k = 1 4 τ k α ⋅ 1 P_k=\frac{\tau_k^\alpha \cdot 1}{\sum_{k=1}^4 \tau_k^\alpha \cdot 1} Pk=k=14τkα1τkα1,其中 α \alpha α是信息素重要程度,我在程序里设置成2, τ \tau τ 是该方向上邻近的砖块所含的信息素浓度, P k P_k Pk就是蚂蚁选择方向k的概率。

类的实现

参数设置

所有参数统一在ant.h头文件中定义:

#define N 10 //房间每个边上地砖的块数
#define DIM 100 //地砖尺寸
#define FREQUENCE 20 //动画刷新频率
#define _INFO_ 1000 //单个蚂蚁最大信息素浓度
#define _INFO_DELT_ 50 //信息素递减浓度
#define _IMPORTANT_ 2 //信息素浓度重要程度
#define _EVADE_ 0.99 //挥发系数
  • _INFO_DELT_ 代表蚂蚁每移动一格信息素浓度递减的数值,也就是蚂蚁走的越远信息素浓度越小,直到没有,这样可以有效防止蚂蚁陷入局部最优解。
  • _INFO_ 蚂蚁分别在找到食物和把食物放回家这2种状态下可以补充信息素回到_INFO_的数值。
  • _IMPORTANT_就是数学模型里的 α \alpha α,于是概率方程就变成了 P k = τ k 2 ∑ k = 1 4 τ k 2 P_k=\frac{\tau_k^2}{\sum_{k=1}^4 \tau_k^2} Pk=k=14τk2τk2.
  • _EVADE_气味消散系数,定义蚂蚁每移动1格为1秒,地面上的所有信息素会随时间每秒变成原来的0.99倍。

蚂蚁之家类实现

比较简单,除了蚂蚁窝的位置、显示蚂蚁窝外,基本上就是调用各个蚂蚁相应的函数:

//home.cpp
//===============蚂蚁之家实现部分============
void Home::addAnt() {
    Home::ants.push_back(Ant(Home::x, Home::y, Home::render, Home::texture0, Home::texture1));
}

//绘制蚂蚁之家
void Home::show() {
    SDL_Rect rect = {Home::y * DIM + DIM/4, Home::x * DIM + DIM/4, DIM/2, DIM/2}; //用矩形代替
    SDL_SetRenderDrawColor(Home::render, 0, 0, 255, 255); //用蓝色
    SDL_RenderFillRect(Home::render, &rect);
    // int k = 0;
    for (deque<Ant>::iterator it = Home::ants.begin(); it != Home::ants.end(); it++) {//画蚂蚁
        // cout << "ant" << k++ << ":" << endl;
        it->show();
    }
}

//下达探索指令
void Home::explore() {
    for (deque<Ant>::iterator it = Home::ants.begin(); it != Home::ants.end(); it++) {
        it->explore();
    }
}

蚂蚁类实现

//ant.cpp

double Ant::INFO = _INFO_; //信息素浓度
int Ant::VOCITY = DIM / 10; //蚂蚁的速度
double Ant::IMPORTANT = _IMPORTANT_; //蚂蚁信息重要程度因子

//=========================蚂蚁实现部分============================
//构造函数
Ant::Ant(int x, int y, SDL_Renderer * rend, SDL_Texture *t0, SDL_Texture *t1):x(x), y(y), dx(x), dy(y),
render(rend), texture0(t0), texture1(t1) {
    cout << "蚂蚁出动:" << this->x << "," << this->y << endl;
    this->px = this->x * DIM + DIM/4;
    this->py = this->y * DIM + DIM/4;
    this->direction = rand() % 4; //随机方向
    this->food = false;
    this->info = Ant::INFO;
}
//省略部分代码
//...

//蚂蚁嗅探方向
void Ant::sniffDirection() {
    int randD[4] = {-1, -1, -1, -1}; //随机方向存储器
    double infoD[4] = {}; //各方向上信息素浓度
    double infoSum = 0; //各方向上总浓度
    int cur = 0; //随机方向存储器尺寸指针
    for (int d = 0; d < 4; d++) { //遍历4个方向
        if (d == (this->direction + 2) % 4) continue; //不能是反方向回头路
        int dx, dy;
        this->directionToPos(d, &dx, &dy);
        if (dx < 0 || dy < 0 || dx >= N || dy >= N) continue; //越界了
        randD[cur] = d; //标记可行方向
        if (this->food) { //根据是否有食物判断嗅探什么类型的信息素
            infoD[cur] = pow(Ground::block_home[dx * N + dy], Ant::IMPORTANT);
        } else {
            infoD[cur] = pow(Ground::block_food[dx * N + dy], Ant::IMPORTANT);
        }
        infoSum += infoD[cur];
        cur++;
    } //end of for
    assert(cur); //断言必有可行方向
    if (infoSum < 0.5) { //周围没有信息素
        this->direction = randD[rand() % cur]; //随机方向
    } else {
        for (int i = 0; i < cur; i = (i + 1) % cur) { //轮盘赌,直到选出方向
            if (rand() % 100 < infoD[i] / infoSum * 100) { //有概率选择了这一方向,使用了数学概率模型
                this->direction = randD[i];
                break;
            }
        }
    }
    this->directionToPos(this->direction, &(this->dx), &(this->dy)); //更新目标方格坐标
// cout << "改变方向:" << this->direction << " " << this->dx << "," << this->dy << endl;
}

//蚂蚁向下一方块移动
void Ant::moveToNext() {
    static int i = 0;
// cout << "边长" << sqrt((dx - x) * (dx - x) + (dy - y) * (dy - y) * 1.0) * DIM << endl;
    if (i++ < sqrt((dx - x) * (dx - x) + (dy - y) * (dy - y) * 1.0) * DIM  / Ant::VOCITY) { //中途用实际长度衡量
        if (dx != x) px += ((dx - x) / abs(dx - x)) * Ant::VOCITY;
        if (dy != y) py += ((dy - y) / abs(dy - y)) * Ant::VOCITY;
    }
    if (i >= sqrt((dx - x) * (dx - x) + (dy - y) * (dy - y) * 1.0) * DIM / Ant::VOCITY) { //到达终点
        this->x = this->dx; this->y = this->dy; //更新当前坐标
        this->px = this->x * DIM + DIM/4; this->py = this->y * DIM + DIM/4; //更新显示坐标
        Ground::evade(); //气味挥发
        this->emitInfo(); //蚂蚁在新的地砖释放信息素,根据是否有食物要释放不同种类的信息素
        i = 0; //初始化计数
    }
}

//蚂蚁探索函数,细分显示步骤
void Ant::explore() {
    if (this->x == this->dx && this->y == this->dy) { //到达指定方格,不在中间移动过程中
        if (Foods::position[this->x*N+this->y]) { //找到了食物
            if (this->food == false) {
                // cout << "找到食物:" << this->x << "," << this->y << endl;
                this->food = true;
                this->info = Ant::INFO; //重生信息素
                this->emitInfo(); //释放食物信息素
                this->direction = (this->direction + 2) % 4; //反方向
                Foods::position[this->x*N+this->y]--; //食物少1
            }
        } else if (Home::x == this->x && Home::y == this->y) { //找到家了
            if (this->food == true) { //有食物
                // cout << "放下食物" << endl;
                this->food = false;
                this->direction = (this->direction + 2) % 4; //反方向
                this->info = Ant::INFO; //重生信息素
            }
        }
        this->sniffDirection();
    } 
    this->moveToNext();
}

地面类及食物类的实现

为了对地板的颜色标记自然,不出现反差太大的情况,使用了log函数,即信息素浓度越大,颜色反差越小,使显示更渐近自然,这里只对回家信息素进行标记,使用max函数是为了防止数值越界成负值:

//gound.cpp
//===============地面实现部分===============
void Ground::show() {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            SDL_Rect rect = {j * DIM, i * DIM, DIM, DIM};
            SDL_SetRenderDrawColor(Ground::render, max(0,(int)(log(Ground::block_home[i*N+j])/log(Ground::INFO_MAX)*100))+125, max(0,(int)(log(Ground::block_home[i*N+j])/log(Ground::INFO_MAX)*100))+125,0,255); //越浓越鲜艳
            SDL_RenderFillRect(Ground::render, &rect);
            SDL_SetRenderDrawColor(Ground::render, 0, 0, 0, 255); //画边框
            SDL_RenderDrawRect(Ground::render, &rect);
        }
    }
}

//气味消散函数
void Ground::evade() {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            if (i == Home::x && j == Home::y) Ground::block_home[i*N+j] = Ground::INFO_MAX; //蚁穴回家气味不减
            Ground::block_food[i*N+j] *= _EVADE_;
            Ground::block_home[i*N+j] *= _EVADE_;
            if (Ground::block_food[i*N+j] < 0.5) {
                Ground::block_food[i*N+j] = 0;
            }
            if (Ground::block_home[i*N+j] < 0.5) {
                Ground::block_home[i*N+j] = 0;
            }
        }
    }
}

食物类的实现更简单了,使用红色方块代替:

//================食物实现部分==============
void Foods::show() {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            if (Foods::position[i*N+j]) { //有食物
                SDL_Rect rect = {j * DIM + DIM/4, i * DIM + DIM/4, DIM/2, DIM/2}; //用矩形代替
                SDL_SetRenderDrawColor(Foods::render, 255, 0, 0, 255); //用红色
                SDL_RenderFillRect(Foods::render, &rect);
            }
        }
    }
}

编译环境设置

我使用的是VS Code,Linux系统,要多文件编译,在task.json文件中编辑如下:

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: g++ 生成活动文件",
            "command": "/usr/bin/g++",
            "args": [
                "-fdiagnostics-color=always",
                "-g",
                "-lSDL2",
                "-lSDL2_image",
                "main.cpp",
                "ant.cpp",
                "home.cpp",
                "ground.cpp",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "调试器生成的任务。"
        }
    ],
    "version": "2.0.0"
}

效果展示

我使用了128只蚂蚁作为演示模拟蚁群

源码下载

C++模拟蚁群(需使用SDL、SDL-image库)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值