旅行商问题

旅行推销员问题(英语:Travelling salesman problemTSP)是这样一个问题:给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路。它是组合优化中的一个NP难问题,在运筹学理论计算机科学中非常重要。

——百度百科

 禁忌搜索算法

禁忌搜索(Tabu Search,TS,又称禁忌搜寻法)是一种现代启发式算法,由美国科罗拉多大学教授Fred Glover在1986年左右提出的,是一个用来跳脱局部最优解的搜索方法。其先创立一个初始化的方案;基于此,算法“移动”到一相邻的方案。经过许多连续的移动过程,提高解的质量。

——百度百科

效果展示

禁忌搜索算法解决旅行商问题_禁忌搜索

 初始解

禁忌搜索算法解决旅行商问题_禁忌搜索_02

 最终解

代码实现

TSPoint

public class TSPoint
    {
        public double X { get; set; }
        public double Y { get; set; }

        public TSPoint()
        {

        }

        public TSPoint(double _x, double _y)
        {
            X=_x;
            Y=_y;
        }

        /// <summary>
        /// 计算欧式距离
        /// </summary>
        /// <param name="point"></param>
        /// <returns></returns>
        public double DistanceTo(TSPoint point)
        {
            return Math.Sqrt((point.X - X) * (point.X - X) + (point.Y - Y) * (point.Y - Y));
        }

        public override bool Equals(object? obj)
        {
            TSPoint point = obj as TSPoint;
            return (point.X==X)&&(point.Y==Y);
        }

        public override int GetHashCode()
        {
            TSPoint point = this as TSPoint;
            return point.GetHashCode();
        }

        public override string ToString()
        {
            return string.Format("({0}, {1})", X, Y);
        }
    }

TabuSearch

public class TabuSearch
    {
        /// <summary>
        /// 禁忌表
        /// </summary>
        private int[][] tabulist;

        /// <summary>
        /// 禁忌表长度
        /// </summary>
        private int neighbour_size;

        /// <summary>
        /// 禁忌代数
        /// </summary>
        public int tabu_size = 10;

        /// <summary>
        /// 迭代次数
        /// </summary>
        public int count = 100;

        /// <summary>
        /// 无穷大
        /// </summary>
        private readonly int INF = int.MaxValue;

        /// <summary>
        /// 城市距离表
        /// </summary>
        private double[][] distMap;

        /// <summary>
        /// 城市坐标
        /// </summary>
        public List<TSPoint> points;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="_points"></param>
        public TabuSearch(List<TSPoint> _points)
        {
            points  = _points;
        }

        /// <summary>
        /// 
        /// </summary>
        public void Run()
        {
            InitDist();
            int n = points.Count;
            // 禁忌表的长度
            neighbour_size = n*(n-1)/2;
            // 预生成所有可能的领域,0、1两列是要交换的点,第2列是这种交换下的路径长度,第三列是禁忌长度
            tabulist = TabuListInit(tabulist, points);

            List<int> sol_best = new List<int>();
            double best_val = INF;
            // 生成初始解,求出n个最终解,找到最优
            for (int i = 0; i < 20; i++)
            {
                List<int> sol_current = CreateRandSol(points);
                // 计算路径长度(目标函数)
                double sol_val = CalObjFunValue(sol_current);
                // 开始求解

                // 0:禁忌表中的下标;1:路径总距离(目标函数值)
                for (int j = 0; j < count; j++)
                {
                    int[] tabu_mindist = new int[2];// 保存特赦的解
                    int[] tabu_curBest = new int[2];// 保存禁忌表中最好的解
                    tabu_mindist[0] = tabu_mindist[1] = tabu_curBest[0] = tabu_curBest[1] = INF;
                    // 更新禁忌表
                    tabulist = UpdateTabulist(tabulist, sol_current, ref tabu_mindist);
                    // 遍历领域求得此代最佳
                    tabu_curBest = GetBestSol(tabulist, tabu_curBest);
                    // 特赦的(所有对象都被禁忌/该解优于前面任一个解)
                    if(tabu_curBest[0] == INF|| tabu_mindist[1]<sol_val)
                    {
                        tabu_curBest[0]=tabu_mindist[0];
                        tabu_curBest[1]=tabu_mindist[1];
                    }
                    // 更新此代最优
                    if (tabu_curBest[1]<sol_val)
                    {
                        int best_index = tabu_curBest[0];
                        sol_val = tabu_curBest[1];
                        tabulist[best_index][3] = tabu_size;
                        Swap(tabulist[best_index][0], tabulist[best_index][1], sol_current);
                    }
                    // 更新禁忌长度
                    tabulist = UpdateTabuSize(tabulist);
                }
                // 更新全局最优
                if (sol_val<best_val)
                {
                    best_val = sol_val;
                    sol_best = CopySol(sol_current);
                }
                Draw(sol_best, "best"+i+"-"+sol_val);
            }
            Draw(sol_best, "final" + best_val);
        }

        /// <summary>
        /// 更新禁忌长度
        /// </summary>
        /// <param name="tabulist"></param>
        /// <returns></returns>
        private int[][] UpdateTabuSize(int[][] tabulist)
        {
            for (int i = 0; i < neighbour_size; i++)
            {
                if (tabulist[i][3]>0)
                    tabulist[i][3]--;
            }
            return tabulist;
        }

        /// <summary>
        /// 交换城市位置
        /// </summary>
        /// <param name="ind1"></param>
        /// <param name="ind2"></param>
        /// <param name="obj"></param>
        private List<int> Swap(int ind1, int ind2, List<int> obj)
        {
            int  temp = obj[ind1];
            obj[ind1] = obj[ind2];
            obj[ind2] = temp;
            return obj;
        }

        /// <summary>
        /// 在禁忌表中找到最好的解
        /// </summary>
        /// <param name="tabulist"></param>
        /// <param name="tabu_curBest"></param>
        /// <returns></returns>
        private int[] GetBestSol(int[][] tabulist, int[] tabu_curBest)
        {
            for (int i = 0; i < neighbour_size; i++)
            {
                // 禁忌长度为0
                if(tabulist[i][3] == 0&& tabulist[i][2]<tabu_curBest[1])
                {
                    tabu_curBest[0] = i;
                    tabu_curBest[1] = tabulist[i][2];
                }
            }
            return tabu_curBest;
        }

        /// <summary>
        /// 更新禁忌表
        /// </summary>
        /// <param name="tabulist"></param>
        /// <param name="obj"></param>
        /// <param name="sol_mindist"></param>
        /// <returns></returns>
        private int[][] UpdateTabulist(int[][] tabulist, List<int> obj, ref int[] sol_mindist)
        {
            // 禁忌表每个交换选择都试一次,选出交换之后最好的那个解
            for (int i = 0; i < neighbour_size; i++)
            {
                // 交换邻域
                int ind1 = tabulist[i][0];
                int ind2 = tabulist[i][1];
                obj = Swap(ind1, ind2, obj);

                int dist = CalObjFunValue(obj);

                // 如果新的解在禁忌表中,就只存特赦相关信息
                if(tabulist[i][3] > 3)
                {
                    tabulist[i][2] = INF;
                    if (dist<sol_mindist[1])
                    {
                        sol_mindist[0] = i;
                        sol_mindist[1] = dist;
                    }
                }
                else
                {
                    tabulist[i][2]=dist;
                }
                // 再换回去保持原状
                obj = Swap(ind2, ind1, obj);
            }
            return tabulist;
        }

        /// <summary>
        /// 复制
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private List<int> CopySol(List<int> obj)
        {
            List<int> res = new List<int>();
            res.AddRange(obj);
            return res;
        }

        /// <summary>
        /// 计算目标函数值
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private int CalObjFunValue(List<int> obj)
        {
            double res = 0;
            for (int i = 0; i < obj.Count; i++)
            {
                res += distMap[obj[i]][obj[(i+1)%obj.Count]];
            }
            return (int)Math.Ceiling(res);
        }

        /// <summary>
        /// 产生随机解
        /// </summary>
        /// <param name="points"></param>
        /// <returns></returns>
        private List<int> CreateRandSol(List<TSPoint> points)
        {
            List<int> res = new List<int>();
            Random rand = new Random();
            int n = points.Count;
            int[] vis = new int[n];
            for (int i = 0; i < n; i++)
            {
                vis[i] = 1;
            }
            for (int i = 0; i < n; i++)
            {
                int index = rand.Next(n);
                while (vis[index]==0)
                {
                    index = (index+1)%n;
                }
                res.Add(index);
                vis[index] = 0;
            }
            return res;
        }

        /// <summary>
        /// 预生成所有可能的邻域
        /// 0、1两列是要交换的点
        /// 第2列是这种交换下的路径长度(初始化无穷大)
        /// 第3列是禁忌长度 (n-1)*n/2
        /// </summary>
        /// <param name="tabulist">禁忌表</param>
        /// <param name="points">城市坐标</param>
        /// <returns></returns>
        private int[][] TabuListInit(int[][] tabulist, List<TSPoint> points)
        {
            int n = points.Count;
            tabulist = new int[neighbour_size][];
            int i = 0;
            for (int j = 0; j < n-1; j++)
            {
                for (int k = j + 1; k < n; k++)
                {
                    tabulist[i] = new int[4];
                    tabulist[i][0] = j;
                    tabulist[i][1] = k;
                    tabulist[i][2] = INF;
                    i++;
                }
            }
            return tabulist;
        }

        /// <summary>
        /// 计算城市距离并保存
        /// </summary>
        private void InitDist()
        {
            distMap = new double[points.Count][];
            for (int i = 0; i<points.Count; i++)
            {
                distMap[i] = new double[points.Count];
            }
            for (int i = 0; i < points.Count - 1; i++)
            {
                for (int j = i+1; j < points.Count; j++)
                {
                    double dist = i == j ? 0 : points[i].DistanceTo(points[j]);
                    distMap[i][j] = dist;
                    distMap[j][i] = dist;
                }
            }
        }

        /// <summary>
        /// 画图
        /// </summary>
        /// <param name="sol"></param>
        /// <param name="name"></param>
        private void Draw(List<int> sol, string name = "sol")
        {
            MapData mapData = new MapData(1);
            List<Line> lines = PointsToLines(sol);
            mapData.CreateMap(mapData.GetMaxBound(lines));
            mapData.AddLines(lines, (int)AstarType.Red);
            mapData.MapToBitmap(mapData.mapData, "E://" + name + ".bmp");
        }

        /// <summary>
        /// 点转线
        /// </summary>
        /// <param name="sol"></param>
        /// <returns></returns>
        private List<Line> PointsToLines(List<int> sol)
        {
            int k = 10;
            List<Line> lines = new List<Line>();
            for (int i = 0; i < sol.Count; i++)
            {
                TSPoint p1 = points[sol[i]];
                TSPoint p2 = points[sol[(i+1)%sol.Count]];
                lines.Add(new Line(new XYZ(p1.X * k, p1.Y * k, 0), new XYZ(p2.X * k, p2.Y * k, 0)));
            }
            return lines;
        }
    }

测试方法:

/// <summary>
        /// 测试禁忌搜索
        /// </summary>
        public void Test()
        {
            // 城市坐标
            List<TSPoint> points = new List<TSPoint>()
            {
                new TSPoint(37,52),
                new TSPoint(49,49),
                new TSPoint(52,64),
                new TSPoint(20,26),
                new TSPoint(40,30),
                new TSPoint(21,47),
                new TSPoint(17,63),
                new TSPoint(31,62),
                new TSPoint(52,33),
                new TSPoint(51,21),
                new TSPoint(42,41),
                new TSPoint(31,32),
                new TSPoint(5,25),
                new TSPoint(12,42),
                new TSPoint(36,16),
                new TSPoint(52,41),
                new TSPoint(27,23),
                new TSPoint(17,33),
                new TSPoint(13,13),
                new TSPoint(57,58),
                new TSPoint(62,42),
                new TSPoint(42,57),
                new TSPoint(16,57),
                new TSPoint(8,52),
                new TSPoint(7,38),
                new TSPoint(27,68),
                new TSPoint(30,48),
                new TSPoint(43,67),
                new TSPoint(58,48),
                new TSPoint(58,27),
                new TSPoint(37,69),
                new TSPoint(38,46),
                new TSPoint(46,10),
                new TSPoint(61,33),
                new TSPoint(62,63),
                new TSPoint(63,69),
                new TSPoint(32,22),
                new TSPoint(45,35),
                new TSPoint(59,15),
                new TSPoint(5,7),
                new TSPoint(10,17),
                new TSPoint(21,10),
                new TSPoint(5,64),
                new TSPoint(30,15),
                new TSPoint(39,10),
                new TSPoint(32,39),
                new TSPoint(25,32),
                new TSPoint(25,55),
                new TSPoint(48,28),
                new TSPoint(56,37),
                new TSPoint(30,40),
            };
            TabuSearch ts = new TabuSearch(points);
            ts.Run();
        }