算法导论实践——旅行商问题

本文深入探讨了旅行商问题的算法理论与实践,利用C#语言实现了一个高效的解决方案。通过巧妙地利用代价矩阵的性质,实现了剪枝操作,大幅减少了搜索次数,提高了算法效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这个算法感觉也很巧妙。抓住代价矩阵的性质便能够进行剪枝操作,从而大大减少搜索次数。你就感觉很神奇

理论

这部分按照上课的PPT给出。

  1. 首先定义旅行商问题
    在这里插入图片描述
    简单的说,我们有一张地图,地图上有很多景点,景点与景点之间有不同路程的小路。我们的任务就是如何 能够不走回头路就能够把所有景点逛完且走的路最少。
  2. 代价矩阵的性质
    在这里插入图片描述
    简单的理解,我们给出一个3*3的地图。C[i,j]表示从点i到点j的代价
    在这里插入图片描述
    按行减,表明点i到其他所有点的距离同时减少,不会影响最优解的选择;
    按列减,表明点j到其他所有点的距离同时减少,不会影响最优解的选择;
  3. 分支界限
    由代价矩阵的变换给出代价矩阵下界的定义。当代价矩阵的每一行、每一列均含有至少一个0时,减的数的总和就是下界值。因为代价不能为负
  4. 爬山法
    一种贪心选择办法。以下图为例在这里插入图片描述
    爬山法选取局部最优解。运用爬山法,我们对上图进行遍历,得到:
    第一次:0 -> 2 -> 1 -> 3 -> 4(红色)
    第二次:1 -> 4 -> 3(蓝色)
    第三次:0 -> 71(绿色)
  5. 递归定义问题
  • 分解:用路径[i,j]划分左右子树的根节点。左子树为包含路径[i,j],右子树为不包含路径[i,j]。[i,j] 的选取很讲究。首先,我们按照代价矩阵的性质,将代价矩阵转变成每一列、每一行均含有至少一个0的矩阵。接着,由于我们希望用到爬山法的策略,因此我们寻找转变后的代价矩阵中的所有0点位置,并将依据这些点计算右子树的下界。
    假如C[i,j] = 0,则其右子树不包含路径[i,j],因此,我们将右子树的C[i,j]置为正无穷。再对其运用矩阵转换,重新求得矩阵下界。如此往复,我们需要找到所有0点中的最大下界。这样的目的是保证左右子树的代价差尽可能大,从而达到剪枝的作用,可以在代码中体会。

  • 解决:选择路径[i,j]后,对于左子树。划去其i行,j列,表示我们的要求的总路径中不包含从i出的边和从入j的边了。然后置[j,i]为无穷大,表明不走回头路;对于右子树,置[i,j]为无穷大即可,因为总的路径中并不包含[i,j]。

  • 合并:不需要合并。查找的路上需要维护一个路径栈,和一个记录分支下界的数组。

废话说了一大堆,直接实践。

C#实践

给出测试样例

在这里插入图片描述

应该的结果

在这里插入图片描述

TravelProblem.cs

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace SearchStrategies
{
    public static class TravelProblem
    {
        /*演算:旅行商问题
         * 矩阵C代表代价矩阵。Cij代表从点i到点j所要花费的代价。
         * 这里,我们需要在图中找一个Hamilton环,使之花费代价最小。
         *
         * 矩阵变换:
         *  我们有在代价矩阵每行每列减去同一个数,不影响优化解的求解。在旅行商问题中,
         *  相当于是说把一个结点到其他所有节点的路径都缩短相同的值,因此不影响最小代价求解。
         * 
         * 代价下界:
         *  实行矩阵变换后,减去的数之和
         * 
         * 划分:
         *  找到使有该条边和没该条边的代价下界(L.B)相差最大的边[i,j],并以有无为标准
         *  划分为左右子树。
         *  
         * 
         * 左子树:
         *  有该条边[i,j],则去掉第i行、第j列,因为不可能再有边是出点i,入点j了。同时,去掉
         *  [j,i],因为Hamilton环不走回头路,一笔画。实行矩阵变换,更新L.B,继续递归划分。
         * 
         * 右子树:
         *  (没有该条边[i,j],则对于Hamilton环必存在出点i的边和入点j的边。要使代价最小,我们
         *  需要找到min{[i,s1]},其中s1∈{点集}且s1≠j,还需要找到min{s2,j},其中s2∈{点集}
         *  且s2≠i,依据min{[i,s1]}和min{s2,j},可以计算代价,为了统一,我们采用接下来的方法)
         *  没有该条边[i,j],则去掉边[i,j],实行矩阵变换,更新L.B,继续递归划分
         * 
         * 爬山法:
         *  每次选择最小的L.B,不断更新分支下界,以达到剪枝的目的。
         * 
         */
        private static int dimension = 7;
        private static List<Node> pathRecord = new List<Node>();
        private static List<int> pathCost = new List<int>();
        private static bool isInit = true;
        private static int initLB = 0;
        
        /// <summary>
        /// 分支界限
        /// </summary>
        private static int cutBoundary = int.MaxValue / 2;
        
        public static void SolveTravelProblem(int[,] C)
        {
            if (CheckPath())
            {
                int cost = pathRecord[pathRecord.Count - 1].LB;
                pathCost.Add(cost);
                cutBoundary = pathCost.Min();
                GetResult();
                
            }
            else
            {
                Node node;
                if (isInit)
                {
                    initLB = MatrixTransform(C);
                    isInit = false;
                    node = Partion(C);
                    node.LB = initLB;
                    pathRecord.Add(node);
                    SolveTravelProblem(C);
                }

                else
                {
                    
                    node = Partion(C);
                    
                    if(node != null)
                    {
                        Console.WriteLine("孩子为:[" + node.i + "," + node.j + "]");                       
                        //左子树
                        int[,] includeIJ = CopyMatrix(C);
                        for (int j = 1; j <= dimension; j++)
                        {
                            includeIJ[node.i, j] = int.MaxValue / 2;
                        }
                        for (int i = 1; i <= dimension; i++)
                        {
                            includeIJ[i, node.j] = int.MaxValue / 2;
                        }

                        includeIJ[node.i, node.j] = 0;
                        includeIJ[node.j, node.i] = int.MaxValue / 2;

                        int LBIncreasement = MatrixTransform(includeIJ);

                        //增加这个点到路径中
                        node.exist = true;
                        node.LB = pathRecord.Last().LB + LBIncreasement;
                        pathRecord.Add(node);
                        if (cutBoundary > pathRecord.Last().LB)
                        {
                            Console.Write("[" +
                            node.i + "," + node.j + "," + node.exist + "]的左");
                            //Console.WriteLine("包含路径[" + node.i + "," + node.j + "]" + "的结点:" + node.LB);
                            SolveTravelProblem(includeIJ);          //递归求解
                        }
                        pathRecord.Remove(pathRecord.Last()); //从路径上移除最后一个结点

                        //右子树
                        int[,] notIncludeIJ = CopyMatrix(C);
                        notIncludeIJ[node.i, node.j] = int.MaxValue / 2;
                        LBIncreasement = MatrixTransform(notIncludeIJ);
                        node.exist = false;
                        node.LB = pathRecord.Last().LB + LBIncreasement;
                        pathRecord.Add(node);

                        if (cutBoundary > pathRecord.Last().LB)
                        {
                            Console.Write("[" +
                            node.i + "," + node.j +","+ node.exist +"]的右");
                            SolveTravelProblem(notIncludeIJ);          //递归求解
                        }
                        pathRecord.Remove(pathRecord.Last()); //从路径上移除最后一个结点
                        
                        //Console.WriteLine("当前查看不存在的结点[" + node.i + "," + node.j + "]");
                    }

                    else
                    {
                        Console.WriteLine("孩子:无");
                    }
                    
                }
            }
        }

        private static Node Partion(int[,] C)
        {
            //找最大差值,方便剪枝
            int max = int.MinValue / 2;
            Node node = null;
            for(int i = 1; i <= dimension; i++)
            {
                for(int j = 1; j <= dimension; j++)
                {
                    if(C[i,j] == 0)
                    {
                        //右子树
                        int[,] notIncludeIJ = CopyMatrix(C);
                        notIncludeIJ[i, j] = int.MaxValue / 2;
                        int increasement = MatrixTransform(notIncludeIJ);
                        if(increasement > max)
                        {
                            max = increasement;
                            node = new Node
                            {
                                i = i,
                                j = j
                            };
                        }
                    }
                }
            }
            return node;
        }

        /// <summary>
        /// 矩阵变换
        /// </summary>
        /// <param name="C">代价矩阵</param>
        /// <returns>需要减去的L.B的值</returns>
        private static int MatrixTransform(int[,] C)
        {
            //在矩阵变换时不考虑已经考虑过的点
            for (int i = 1; i < pathRecord.Count; i++)
            {
                C[pathRecord[i].i, pathRecord[i].j] = 0;
            }

            int LBIncreasement = 0;

            //检查行
            for(int i = 1; i <= dimension; i++)
            {
                bool zeroFlag = false;
                
                for(int j = 1; j <= dimension; j++)
                {
                    if(C[i,j] == 0)
                    {
                        zeroFlag = true;
                    }
                }
                if (!zeroFlag)
                {
                    int min = int.MaxValue / 2;
                    for(int j = 1; j <= dimension; j++)
                    {
                        if(C[i,j] < min)
                        {
                            min = C[i, j];
                        }
                    }

                    for(int j = 1; j <= dimension; j++)
                    {
                        C[i, j] -= min;
                    }

                    LBIncreasement += min;
                }
            }

            for (int j = 1; j <= dimension; j++)
            {
                bool zeroFlag = false;

                for (int i = 1; i <= dimension; i++)
                {
                    if (C[i, j] == 0)
                    {
                        zeroFlag = true;
                    }
                }

                if (!zeroFlag)
                {
                    int min = int.MaxValue / 2;
                    for (int i = 1; i <= dimension; i++)
                    {
                        if (C[i, j] < min)
                        {
                            min = C[i, j];
                        }
                    }

                    for (int i = 1; i <= dimension; i++)
                    {
                        C[i, j] -= min;
                    }

                    LBIncreasement += min;
                }
            }
            //矩阵变换完毕后,将路径上的点变成正无穷,表明下一次我们不会用到他们
            for (int i = 1; i < pathRecord.Count; i++)
            {
                C[pathRecord[i].i, pathRecord[i].j] = int.MaxValue / 2;
            }

            return LBIncreasement;
        }


        private static bool CheckPath()
        {
            List<int> iArray = new List<int>();
            List<int> jArray = new List<int>();
            List<bool> existArray = new List<bool>();
            for(int i = 0;i < pathRecord.Count; i++)
            {
                iArray.Add(pathRecord[i].i);
                jArray.Add(pathRecord[i].j);

            }

            for(int i = 1;i <= dimension; i++)
            {
                if (!iArray.Contains(i) || !jArray.Contains(i))
                    return false;
                if(pathRecord[i].exist == true)
                    existArray.Add(pathRecord[i].exist);
            }
            if (existArray.Count != dimension)
                return false;

            return true;
        }





        /// <summary>
        /// 生成随机矩阵
        /// </summary>
        /// <param name="n">矩阵维度</param>
        /// <returns></returns>
        public static int[,] RandomCostMatrix(int n)
        {
            int[,] C = new int[n + 1,n + 1];
            dimension = n;
            for (int i = 1; i<= n; i++)
            {
                for(int j = 1;j <= n; j++)
                {
                    C[i, j] = i == j ? int.MaxValue / 2 : new Random().Next(0, int.MaxValue / 4);
                    Console.Write(C[i, j]);
                    Console.Write(" ");
                }
                Console.WriteLine(" ");
            }
            return C;
        }

        private static int[,] CopyMatrix(int[,] from)
        {
            int[,] to = new int[dimension + 1, dimension + 1];
            for(int i = 1; i <= dimension; i++)
            {
                for(int j = 1; j <= dimension; j++)
                {
                    to[i, j] = from[i, j];
                }
            }
            return to;
        }

        private static void GetResult()
        {
            Console.WriteLine("当前路径为:");
            for(int i = 1; i < pathRecord.Count; i++)
            {
                Console.Write("[");
                Console.Write(pathRecord[i].i);
                Console.Write(",");
                Console.Write(pathRecord[i].j);
                Console.Write(",");
                Console.Write(pathRecord[i].exist);
                Console.Write("]");
            }
            Console.WriteLine(" ");
        }
    }

    public class Node
    {
        public int i;
        public int j;
        public bool exist;  //是否包含边[i,j]
        public int LB;
    }
}

Program.cs

using System;

namespace SearchStrategies
{
    class Program
    {
        static void Main(string[] args)
        {
           
            int[,] C =
            {
                {0,0,0,0,0,0,0,0},
                {0,int.MaxValue,3,93,13,33,9,57  },
                {0,4,int.MaxValue,77,42,21,16,34 },
                {0,45,17,int.MaxValue,36,16,28,25 },
                {0,39,90,80,int.MaxValue,56,7,91 },
                {0,28,46,88,33,int.MaxValue,25,57 },
                {0,3,88,18,46,92,int.MaxValue,7 },
                {0,44,26,33,27,84,39,int.MaxValue }
            };
            TravelProblem.SolveTravelProblem(C);
            
        }
    }
}

结尾

可能有BUG(手动滑稽,不过今天就暂时这样吧)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值