IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> TSP问题的遗传算法实现(C++) -> 正文阅读

[数据结构与算法]TSP问题的遗传算法实现(C++)


一、实验内容

问题描述:旅行商问题,即TSP问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
内容提要:以N个节点的TSP(旅行商问题)问题为例,应用遗传算法并用选定的编程语言,设计简单的遗传优化系统对问题进行求解,求出问题的最优解,通过实验培养学生利用遗传算法进行问题求解的基本技能。

二、实验步骤

  1. 随机生成N个二维坐标节点。
  2. 应用遗传算法并用选定的编程语言(我选择的语言是C++),设计简单的遗传优化系统对问题进行求解,求出问题的最优解。
  3. 选择适当可视化方法显示结果。
  4. 分析适应度函数对启发式搜索算法的影响。
  5. *扩展选做题:考虑不同数值N对最终结果和求解性能的影响。对于比较大的N,是否设计更快速的近似方法代替原有算法。

三、分析说明(包括核心代码及解释)

1. 随机生成N个二维坐标节点。

2. 应用遗传算法并用选定的编程语言(C++),设计简单的遗传优化系统对问题进行求解,求出问题的最优解。

实际上遗传算法并不能百分百得到TSP问题的最优解,但能够较好地近似求解TSP问题,在问题规模比较大的时候,遗传算法的优点能够明显体现出来,但当规模非常大时,遗传算法的效率也会变低。

利用遗传算法解决TSP问题的流程可以归为如下图:
在这里插入图片描述
大致思路:首先随机生成N个城市的二维坐标,计算出两两城市之间的距离保存在一个二维数组中,接着随机初始化种群(为种群中的每一个个体随机生成一段城市访问序列),计算初始种群中每个个体的适应度(定义为城市序列中相邻两城的距离之和的倒数)和生存(被选择)概率,然后循环迭代,对种群进行选择、交叉和变异操作(这三步骤的实现后面详讲),当迭代次数达到一定值时结束循环,取循环过后的种群中适应度最高的作为TSP问题的最优解,打印出初始种群的最优路线顺序以及迭代过后得到的种群的最优路线顺序,并在Excel表中绘制两条路线进行对比。

选择算法(采用赌轮选择法):

求出当前种群每个个体的适应度后,计算出种群所有个体的适应度之和,个体适应度除以总适应度即为每个个体的的生存(被选择)概率,计算出每个个体的累计概率,随机生成一个0-1之间的随机浮点数,根据这个浮点数所在的累计概率区间选择出对应的个体,一轮过后未被选择到的个体就视为在自然选择中被淘汰的个体。(当然也可能发生小概率事件,当前最优的个体被淘汰了,这也是遗传算法不能得到最优解的原因之一)。

例:种群中有5条路线,每条路线的适应度分别为0.05、0.15、0.35、0.25和0.2,累计概率为0.05、0.2、0.55、0.8和1,那么随机数落在[0,0.05]区间则表示0号个体被选择,落在[0.05,0.2]区间则表示1号个体被选择,以此类推。可以用坐标轴形式展示,如下图所示:
在这里插入图片描述

交叉算法:

因为选择的个体是按照依次生成随机数所在区间所决定的,也就是说选择出来的个体的顺序是随机的,即此时种群数组中的个体顺序是随机的。因此可以假设第k(k=0、2、4、…、2n)个个体和k+1个个体有一定概率(我设置的Pc为0.9)交叉变换,这个概率就是交叉概率,随机生成一个0-1之间的随机浮点数,若这个数在交叉概率以内,就将当前个体(第k个)与k+1个个体进行交叉。

TSP问题中,交叉算法最主要的是解决交叉后产生的基因冲突问题,例如:
在这里插入图片描述

解决的方案是用映射关系,在两个个体交叉的那段中确定映射关系,将非交叉段中存在映射关系的数字替换为其映射的数,即可解决冲突问题:
在这里插入图片描述

变异算法:

设每个个体有一定概率(变异概率Pm,我设置的是0.1)发生基因多次对换,对种群中的每个个体,生成一个0-1之间的随机数,若该随机数在[0,Pm]内,则认为该个体发生变异,随机生成两个不相等的范围在[0,城市数 - 1]之间的随机整数,将这个个体在这两个随机数(下标)对应的城市编号对换,重复上述变换n次,n取[1,城市数]之间的随机整数。
在这里插入图片描述
当完成选择、交叉和变异后,用得到的种群覆盖原来的种群,判断迭代次数是否达到最大迭代次数,若达到则输出当前种群的最优路线,否则对新得到的一代种群重复进行上述选择、交叉和变异操作。

代码如下:

#include<iostream>
#include<vector>
#include<iomanip>
#include<unordered_map>
#include<algorithm>
#include<math.h>
#include<time.h>


#define GROUP_NUM 100    //种群规模
#define CITY_NUM 15     //城市数量
#define ITERATION_NUM 1000   //最大迭代次数
#define Pc 0.9      //交叉率
#define Pm 0.1     //变异率
using namespace std;

//路线类
class Route{
public:
    vector<int> seq;    //路线的城市顺序
    double fitness;   //适应度(定义为城市序列中相邻两城的距离之和的倒数)
    double Ps;  //生存概率(被选择概率)
    double dis; //路线距离

    //构造函数
    Route() {
        seq = vector<int>(CITY_NUM + 1);
        fitness = 0;
        Ps = 0;
    }
};

//城市坐标类
class City{
public:
    int x;  //横坐标
    int y;  //纵坐标
};

//为自定义类(Route)制定排序规则
//升序排列,即生存概率高的排在后面
bool my_cmp(Route r1, Route r2) {
    return r1.Ps < r2.Ps;
}

//城市之间的距离矩阵
vector<vector<double>> dis(CITY_NUM, vector<double>(CITY_NUM, 0.0));

//种群
vector<Route> group(GROUP_NUM);

//城市
vector<City> city(CITY_NUM);

//城市初始化函数,随机生成CITY_NUM个二维坐标节点,计算城市间的距离并存在距离矩阵中
void city_init() {
    //设城市全部坐落在100 * 100的二维平面内
    //种下随机种子,使每次运行生成的城市坐标不同
    srand((unsigned)time(NULL));
    cout << "生成的随机城市坐标:" << endl;
    for(int i = 0; i < CITY_NUM; i++) {
        //为每个城市随机生成坐标
        city[i].x = rand() % 100;
        city[i].y = rand() % 100;
        cout << i << "、" << '(' <<city[i].x << ", " << city[i].y << ')' << endl;
    }

    //计算城市距离,城市i到城市j的距离与城市j到i的距离相等
    for(int i = 0; i < CITY_NUM; i++) {
        for(int j = i; j < CITY_NUM; j++) {
            int temp1 = (city[i].x - city[j].x) * (city[i].x - city[j].x);
            int temp2 = (city[i].y - city[j].y) * (city[i].y - city[j].y);
            dis[i][j] = sqrt(temp1 + temp2);
            dis[j][i] = dis[i][j];
        }
    }
}

//种群初始化函数,生成GROUP_NUM个初始随机访问城市序列
void group_init() {
    srand((unsigned)time(NULL));
    for(int i = 0; i < GROUP_NUM; i++) {
        //用哈希表防止序列中生成重复的城市
        unordered_map<int, int> mp;
        for(int j = 0; j < CITY_NUM; j++) {
            int num = rand() % CITY_NUM;
            //如果随机生成的数重复了,则重新生成直到不重复为止
            while(mp[num] != 0) {
                num = rand() % CITY_NUM;
            }
            mp[num]++;
            group[i].seq[j] = num;
        }
        group[i].seq[CITY_NUM] = group[i].seq[0];
    }
    /*
    cout << "初始种群:" << endl;
    for(int i = 0; i < GROUP_NUM; i++) {
        for(int j = 0; j < CITY_NUM; j++) {
            cout << group[i].seq[j] << " ";
        }
        cout << endl;
    }*/

}

//计算初始种群中每个个体的适应度及生存概率
//适应度设置为序列中相邻两城之间的距离之和的倒数
void cal_group() {
    //种群总适应度
    double total_fit;

    //计算每个个体的适应度
    for(int i = 0; i < GROUP_NUM; i++) {
        double total_dis = 0;
        for(int j = 1; j <= CITY_NUM; j++) {
            total_dis += dis[group[i].seq[j]][group[i].seq[j - 1]];
        }
        group[i].dis = total_dis;
        //个体的适应度为总距离的倒数
        group[i].fitness = 1.0 / total_dis;
        //测试计算出来的路径和是否正确
        //cout << total_dis << " " << group[i].fitness << endl;

        total_fit += group[i].fitness;
    }

    //计算每个个体的生存概率(被选择概率),为个体适应度 / 总适应度
    for(int i = 0; i < GROUP_NUM; i++) {
        group[i].Ps = group[i].fitness / total_fit;
    }
}

//打印种群信息
void show() {
    for(int i = 0; i < GROUP_NUM; i++) {
        for(int j = 0; j <= CITY_NUM; j++) {
            cout << group[i].seq[j] << " ";
        }
        cout << setprecision(4) << "适应度为:" << group[i].fitness << "  生存概率为:"  << group[i].Ps << endl;
    }
}

//选择
void select() {

    //计算累计概率
    vector<double> acc_p(GROUP_NUM);
    acc_p[0] = group[0].Ps;
    for(int i = 1; i < GROUP_NUM; i++) {
        acc_p[i] = acc_p[i - 1] + group[i].Ps;
    }

    //记录被选择的个体,利用赌轮选择法,随机生成0~1之间一个数,根据计算出来的累计概率选择个体
    vector<Route> sel_individual(GROUP_NUM);
    srand((unsigned)time(NULL));
    for(int i = 0; i < GROUP_NUM; i++) {
        //生成0~1的随机数,4位小数
        float random = rand() % (10000) / (float)(10000);
        //cout << random << " ";

        for(int j = 0; j < acc_p.size(); j++) {
            if(random <= acc_p[j]) {
                //cout << random << " " << acc_p[j] << endl;
                sel_individual[i] = group[j];
                break;
            }
        }
    }

    //被选择的种群覆盖初始种群
    for(int i = 0; i < GROUP_NUM; i++) {
        group[i] = sel_individual[i];
    }

    /*cout << "打印经过自然选择后的种群序列:" << endl;
    for(int i = 0; i < GROUP_NUM; i++) {
        cout << i << "、" << " ";
        for(int j = 0; j < CITY_NUM; j++) {
            cout << group[i].seq[j] << " ";
        }
        cout << "适应度为:" << group[i].fitness << "  生存概率为:"  << group[i].Ps << endl;
    }*/
}

//交叉(交配)算法
//第k(k=0、2、4、...、2n)个个体和k+1个个体有一定的概率交叉变换
//设置一个0~1之间的随机数,若在Pc(交配率)范围内,则该该个体k与下一个个体k+1进行交配
void mating() {
    //随机生成子代交配时DNA交换的数量(1~CITY_NUM / 2)
    srand((unsigned)time(NULL));
    int change_num = (rand() % CITY_NUM / 2) + 1;
    //cout << "交换DNA数量:" << change_num << endl;

    //开始交配
    for(int i = 0; i < CITY_NUM; i += 2) {
        //生成0-1之间的随机数(3位小数)
        float random = rand() % (1000) / (float)(1000);
        //在交配率以内,则该个体i与下一个个体i+1进行交配
        if(random < Pc) {
            //随机生成交配点
            int point = rand() % (CITY_NUM - change_num);

            //cout << i << " 与 " << i + 1 << " 进行交配,断点:" << point << endl;

            //先将双亲的交配片段进行互换,并用哈希映射记录,然后解决基因冲突
            unordered_map<int, int> hash1;
            for(int j = point; j < change_num + point; j++) {
                int a = group[i].seq[j];
                int b = group[i + 1].seq[j];
                if(hash1.find(a) != hash1.end()) {
                    a = hash1[a];
                }
                if(hash1.find(b) != hash1.end()) {
                    b = hash1[b];
                }
                hash1[a] = b;
                hash1[b] = a;
                swap(group[i].seq[j], group[i + 1].seq[j]);
            }
            //处理双亲交配后可能产生的基因冲突问题(断点前)
            for(int j = 0; j < point; j++) {
                if(hash1.find(group[i].seq[j]) != hash1.end()) {
                    group[i].seq[j] = hash1[group[i].seq[j]];
                }
                if(hash1.find(group[i + 1].seq[j]) != hash1.end()) {
                    group[i + 1].seq[j] = hash1[group[i + 1].seq[j]];
                }
            }
            //断点后
            for(int j = point + change_num; j < CITY_NUM; j++) {
                if(hash1.find(group[i].seq[j]) != hash1.end()) {
                    group[i].seq[j] = hash1[group[i].seq[j]];
                }
                if(hash1.find(group[i + 1].seq[j]) != hash1.end()) {
                    group[i + 1].seq[j] = hash1[group[i + 1].seq[j]];
                }
            }
        }
        //最后一个城市的下一个城市是第一个城市
        group[i].seq[CITY_NUM] = group[i].seq[0];
    }


    /*
    //打印交配过后的种群
    for(int i = 0; i < GROUP_NUM; i++) {
        cout << i << "、" << " ";
        for(int j = 0; j < CITY_NUM; j++) {
            cout << group[i].seq[j] << " ";
        }
        //cout << "适应度为:" << group[i].fitness << "  生存概率为:"  << group[i].Ps << endl;
        cout << endl;
    }*/
}

//变异算法
//每个算子有一定概率(变异概率)基因多次对换。
//对每个个体,若满足变异概率,则随机生成两个不相等的范围在[0,城市数 - 1]之间的随机整数。将该个体在这两个随机整数对应的位置的城市编号对换
//进行上述n次对换,n是一个[1,城市数]之间的随机整数
void mutate() {
    srand((unsigned)time(NULL));
    for(int i = 0; i < GROUP_NUM; i++) {
        //生成0-1之间的随机数(4位小数)
        float random = rand() % (10000) / (float)(10000);
        //cout << random << " ";
        if(random < Pm) {
            //cout << i << " 号个体产生变异" << endl;
            //随机生成基因对换次数
            int exchange_times = rand() % CITY_NUM + 1;
            while(exchange_times > 0) {
                //随机生成两个不相等的范围在[0,城市数 - 1]之间的随机数
                int a = rand() % CITY_NUM;
                int b = rand() % CITY_NUM;
                swap(group[i].seq[a], group[i].seq[b]);
                exchange_times--;
            }
        }
        //最后一个城市的下一个城市是第一个城市
        group[i].seq[CITY_NUM] = group[i].seq[0];
    }
    /*cout << endl << "打印变异过后的种群" << endl;
    for(int i = 0; i < GROUP_NUM; i++) {
        cout << i << "、" << " ";
        for(int j = 0; j < CITY_NUM; j++) {
            cout << group[i].seq[j] << " ";
        }
        //cout << "适应度为:" << group[i].fitness << "  生存概率为:"  << group[i].Ps << endl;
        cout << endl;
    }*/
}



int main()
{
    int it = 0;   //迭代次数
    //随机生成初始城市坐标
    city_init();
    //随机生成初始种群
    group_init();

    cal_group();

    sort(group.begin(), group.end(),my_cmp);

    //show();
    cout << endl;

    cout << "初代“最优”路线为:";
    for(int i = 0; i < CITY_NUM + 1; i++) {
        cout << group[GROUP_NUM - 1].seq[i] << " ";
    } cout  << "适应度为:" << group[GROUP_NUM - 1].fitness << endl;

    cout << "该路线长度为:" << group[GROUP_NUM - 1].dis << endl;

    cout << "该路线对应的坐标点分别为:" << endl;
    for(int i = 0; i < CITY_NUM + 1; i++) {
        int t = group[GROUP_NUM - 1].seq[i];
        cout << '(' <<city[t].x << ", " << city[t].y << ')' << endl;
    }

    while(it <= ITERATION_NUM) {
        //计算适应度以及生存概率
        cal_group();

        //在种群中选择个体
        select();

        //种群进行交配
        mating();

        //种群中的个体产生变异
        mutate();

        it++;

    } cout << endl;


    sort(group.begin(), group.end(),my_cmp);

    //show();
    //cal_group();

    cout << "经过"  << ITERATION_NUM << "次迭代后:" << endl;
    cout << "“最优”路线为:";
    for(int i = 0; i < CITY_NUM + 1; i++) {
        cout << group[GROUP_NUM - 1].seq[i] << " ";
    } cout  << "适应度为:" << group[GROUP_NUM - 1].fitness << endl;


    cout << "该路线长度为:" << group[GROUP_NUM - 1].dis << endl;

    cout << "该路线对应的坐标点分别为:" << endl;
    for(int i = 0; i < CITY_NUM + 1; i++) {
        int t = group[GROUP_NUM - 1].seq[i];
        cout << '(' <<city[t].x << ", " << city[t].y << ')' << endl;
    }
    return 0;
}

3.选择适当可视化方法显示结果。

在程序中输出初代种群和迭代完成后种群的最优路线顺序以及城市坐标,将坐标填入Excel表中生成平面坐标折线图,对比初代种群和利用遗传算法得到的种群各自的最优路线。

下面选取几次对比较明显的测试:
测试①:
在这里插入图片描述

在Excel中画出两条路线作为对比:
在这里插入图片描述
在这里插入图片描述
可以看出,由于遗传算法本身存在一些缺点(后面讨论),不能得到最优解,但对比初代种群的最优路线来看,经过遗传算法得到的近似解明显比初代种群的要优很多。

测试②:
在这里插入图片描述

在Excel中画出两条路线作为对比:
在这里插入图片描述
在这里插入图片描述

测试③:
在这里插入图片描述
在这里插入图片描述

结论:
经过几次测试后发现,由于遗传算法是一种启发式搜索算法,只能尽可能得到最优解,但往往只能得到近似解,对比最终得到的解和初始路线,可以看出经过遗传算法后是可以得到一个比较优化的解的,而且在城市数量为15、种群规模为100,迭代1000次的情况下每次运行都只需要0.2s左右的时间,这体现了遗传算法的优势,在城市规模不是很大的时候算法效率是非常高的。

4. 分析适应度函数对启发式搜索算法的影响。

遗传算法评价一个解的好坏不是取决于它的解的结构,而是取决于该解的适应度值,这正体现了遗传算法“优胜劣汰”的特点。遗传算法不需要适应度函数满足连续可微等条件,唯一要求是针对输入可计算出能加以比较的非负结果。一般情况下适应度越大说明该个体适应性越强,越不容易在自然选择中被淘汰,因此本次TSP问题实验中将个体的适应度定义为城市序列中相邻两城的距离之和的倒数。

适应度函数也称评价函数,是根据目标函数确定的用于区分群体中个体好坏的标准,总是非负的,任何情况下都希望它的值越大越好。在选择操作中,会出现2个成为遗传算法欺骗的问题:

  1. 在遗传算法初期,通常会产生一些超常个体,按照比例选择法,这些超常个体会因竞争力突出,而控制选择过程,影响到算法的全局优化性能;
  2. 遗传算法后期,当算法趋于收敛时,由于种群中个体适应度差异较小,继续优化的潜能降低,可能获得某个局部最优解.因此,如果适应度函数选择不当就会产生以上的欺骗问题.因此适应度函数的选择对于遗传算法的意义重大。

5. *扩展选做题:考虑不同数值N对最终结果和求解性能的影响。对于比较大的N,是否设计更快速的近似方法代替原有算法。

查阅相关资料得知,求解TSP问题中有两个比较优的算法,遗传算法和蚁群算法,从查阅资料的数据来看,遗传算法在求解30城市一下的TSP问题时显示了很好的性能,其最优解和平均接都好于蚁群算法,但随着城市数量的增加,基本遗传算法由于搜索范围扩大,搜索好解的能力下降,在既有的迭代次数内得出的结果和最优结果相差就越来越大,虽然在没有发生早熟情况下,足够多的运算次数可以搜索到好解,但需要花费很长的时间,而蚁群算法却表现出良好的性能,在很少迭代次数内可以搜索到好解。

因此,对于比较大的N,用蚁群算法代替遗传算法能够提高效率。

四、总结心得

本次实验采用遗传算法求解TSP问题,在一开始设置的参数(种群规模、城市数、迭代次数、交叉率和变异率)中常常不能得到较好的解,通过测试发现在迭代几十次后,往后迭代的种群几乎没有改变,查阅资料得知这种现象称为遗传算法的“早熟现象”,即在遗传算法实现过程中,丧失了种群个体的多样性,使个体中的基因趋于一致,导致算法的搜索停止在局部,无法实现在全局的搜索,最终得到的是局部最优解。

出现早熟现象的原因主要有:

  1. 群体规模:当群体规模取值较小时,虽然算法的收敛速度会很快,但是生成的具有较高适应度的个体的数量会很少,容易造成早熟现象的发生;当群体规模取值较大时,保证了参加遗传算法的群体的多样性,能够产生具有较高适应度的个体,增大了得到全局最优解的可能性,但是增大了算法的计算量,降低了收敛速度。
  2. 选择操作:在自然界中,选择过程遵循着优胜劣汰的规律,在遗传算法中,这种规律是通过选择算子来实现的。选择操作主要是在本代种群中选择适应度较高的个体保留到下一代,保证遗传算法得到最优解。选择操作通常使用轮盘赌选择方法,增大了较高适应度值的个体被遗传的概率,但是这样有可能淘汰掉本代种群中的最佳个体,也有可能本代中的最佳个体通过交叉和变异操作之后,以前良好的基因组合被破坏,容易出现早熟现象。
  3. 交叉概率和遗传概率:在遗传算法中,通过交叉和变异的操作,保证了种群个体的多样性。传统的遗传算法中,交叉概率和变异概率采用了固定值。如果交叉概率取值较大,提高了交叉操作的速度,同时也破坏了原来个体,算法的寻优速度就会降低;如果交叉概率取值较小,会降低遗传个体的破坏性,但是算法可能会过早地得到局部最优解。如果变异概率较小,通过变异操作产生的新个体的数量也会变小,随着遗传操作的进行,适应度较高个体就会在群体中快速繁殖,从而出现局部最优解,出现早熟现象。

基于以上原因,重新调整了这几个参数的设置,测试的结果前面已经提到了,这里不再赘述。总之,在本次实验中,由于所有操作都是随机发生的,包括城市的坐标,在选择、交叉、变异过程中,有可能将原种群中优秀的个体淘汰了,得不到最优解,而且经过调整过后的参数也难免遇到早熟的现象,但在多数情况下还是能得到较好的解的。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-06-08 19:12:49  更:2022-06-08 19:13:18 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 1:23:18-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码