分支限界法求解旅行商问题(TSP)

1 实验内容

某售货员要到若干城市去推销商品,已知各城市之间的路程(以耗费矩阵的形式表示)。要求使用分支限界算法,求得一条回路,该回路经过每个城市一次,且总的耗费(总距离)最小。

2 算法思路

分支限界法是一种在问题的状态空间进行搜索的算法,对于每一个节点算法给出一个界,以x为根的子树生成的节点所表示的解不会超过这个界。在每次扩展时都选择界最优的节点进行扩展。
  对于旅行商问题,可以按下面的方式组织分支限界算法。
  在搜索树的组织方面,可以这样考虑。每次选择一个边,左子节点表示使用这条边时的求解状态,右子节点为不使用这条边的求解状态。这样整个搜索树被组织成了一颗二叉树,对于每个节点有使用、不使用两种状态,最多只需扩展有个节点(n为城市数量)。
  再考虑结点限界的计算,给每个部分解 ( x 1 , x 2 , ⋯   , x k ) (x_1,x_2,\cdots,x_k) (x1,x2,,xk)一个下界bound,表示包含这部分路径的完整回路的耗费至少是bound,具体计算可以用矩阵规约化的方法。注意到完整的巡回回路必定恰好包含每一行、每一列的一条边,这是因为路径只经过每个顶点一次,即所有的顶点只做一次起点也只做一次终点。若给耗费矩阵的一行或一列的每个项都减去常量r,则新矩阵下任何巡回履行的耗费比原矩阵耗费减少r。矩阵规约指使矩阵第i行减去 r i r_i ri,第j列减去%c_j%,使得每一行、每一列至少有一个0,这样 b o u n d = ∑ i = 1 n r i + ∑ j = 1 n c j bound = \sum_{i=1}^nr_i+\sum_{j=1}^nc_j bound=i=1nri+j=1ncj。每次从所有结点中选择限界最小的结点进行扩展直至获得解。
  最后讨论左右子树的生成规则,假设选取的边为 ( i , j ) (i,j) (i,j)。右子树不包括边 ( i , j ) (i,j) (i,j),将耗费矩阵中 ( i , j ) (i,j) (i,j)对应的元素修改为 ∞ \infin ,再对对应的行、列重新规约化求得新的限界值。左子树包括边 ( i , j ) (i,j) (i,j),由于不能再从任何其他的顶点到达j,也不能从顶点i到达任何其他的顶点,所以将对应的行、列删除,这样耗费矩阵的规模减少了1; ( i , j ) (i,j) (i,j)的使用说明边 ( j , i ) (j,i) (j,i)一定不会被使用,将其置为∞;最后考虑如何避免局部回路,若包含边为 ( u i , v 1 ) (u_i,v_1) (ui,v1),原先存在两条路径 ( u 1 , u 2 , ⋯   , u i ) (u_1,u_2,\cdots,u_i) (u1,u2,,ui) ( v 1 , v 2 , ⋯   , v j ) (v_1,v_2,\cdots,v_j) (v1,v2,,vj),则需要将 ( v j , u 1 ) (v_j,u_1) (vj,u1)对应的元素置为 ∞ \infin

3 数据结构

3.1 搜索结点表示

用结构体表示搜索结点,每个结点包含五个数据:当前耗费矩阵cost,剩余城市数量cityNum,限界bound,正向路径path,和反向路径inpath。其中耗费矩阵为二维的vecotr,路径为一维的vector,城市数量和限界为整型。
  path含义:path[i]表示完整回路上顶点i前一个顶点的编号;
  inpath含义:inpath[i]表示完整回路上顶点i下一个顶点的编号;
  在耗费矩阵中,选择一条边后会删除对应的行与列,这破坏了数组索引和顶点编号的对应关系,所以需要在耗费矩阵存储每一行、每一列对应的顶点。耗费矩阵第一行、第一列存储顶点编号。

3.2 搜索树的组织

因为每次都需要选取界限最小的结点进行扩展,可以选择用最小堆存储搜索树的结点。利用C++的模板类vector存储结点,在取结点时直接取堆顶元素即可,再用pop_head()和pop_back()从堆中删除堆顶元素。扩展后用push_back()将新结点添加到堆中,再用push_heap()调整堆的顺序。不断地选取结点、添加结点直至堆为空或者找到了最优解。
  注意,pop_head()和push_heap()不会创造新的容器而是一种组织容器的方式。在使用的过程中要对结点结构体重载比较函数。

4 具体实现:

4.1 矩阵规约化

  首先逐行扫描,先求得每一行中的最小值minum,再将改行所有元素减去这个最小值。由于需要用INT_MAX表示 ∞ \infin ,所以要略去数值为INT_MAX的元素,然后将节点的界限加上minum。
  再对列进行规约化处理。用一个bool类型的数组zeroLocation[n]表示该列是否有0,在对行处理时若cost[i][k]置为0,则将zeroLocation[k]置为true。这样对列处理时只需对没有0的列规约化即可。

4.2 边的选取

  边选取的规则会影响搜索树的组织形式,直接关系到能否快速找到最优解。根据左右节点的生成规则,可以发现,若最优周游路线存在于左子树中则只需要再选取n-1条边即可,且左子树的耗费矩阵规模更小。所以要尽量使得左子节点的限界值比右子节点小。
  若选的边耗费不为0(规约后),则右子结点限界不变,左子节点限界要么不变要么增大,不满足上述条件,所以必须选一条代价为0的边。这里的策略是选取一条令右子树在行规约化后具有最大限界的边。
  具体策略为:逐行扫描,记录改行除0外的最小值minum和该行中0的索引minIndex,若一行已扫描到两个0则可提前进入下一行;用maxminSide记录各行minum的最大值,用maxIndex[]记录待选边的索引,若minum>maxminSide,则更新maxminSide和mmaxIndex。至于为什么不考虑列,只考虑行可以逐行进行,时间复杂度为 O ( n 2 ) O(n^2) O(n2),若要考虑列,则要为每个0元素花费 O ( n ) O(n) O(n)的时间(求第i行、第j列除0外最小值),边选取时间复杂度变为 O ( n 3 ) O(n^3) O(n3)

4.3 左右孩子生成

  记当前的活动结点为eNode,左孩子为lNode,右孩子为rNode,在上一步中选定的边的索引为 ( i , j ) (i,j) (i,j)

  • 右孩子:
      右孩子的生成规则较为简单。首先右子树为不采取边 ( i , j ) (i,j) (i,j)的情况,故其路径不会发生变化,直接拷贝。而后将eNode的耗费矩阵复制给rNode,置rNode.cost[i][j]=INT_MAX,令rNode.cityNum=eNode.cityNum-1。并对rNoded的第i行和第j列进行规约化。
  • 左孩子:
      我们对eNode的耗费矩阵进行操作,最后再将缩减后的耗费矩阵赋给lNode。首先将 ( i , j ) (i,j) (i,j)对应的返回的边置为INT_MAX,这需要先读取出和耗费矩阵索引和顶点编号的对应关系,保存在row和column中,然后从中读出边的索引,不一定是 ( j , i ) (j,i) (j,i)。而后根据新添加的边 ( s t a r t , d e s t ) (start,dest) (start,dest)更新lpath和linpath两个数组。而后进行回路的避免,从lpath中读出以start为终点的路径的起始点u,从linpath中读出以dest为起点的路径的终点v,当u!=v时将回路的代价置为INT_MAX。最后对将该耗费矩阵取出第i行和第j列赋给右孩子,并调用convention函数对其规约化。
      在生成左右孩子后,用push_back()将其添加到搜索结点的vector中,然后用pop_heap()将其重新调整为堆。

5 时间复杂度分析:

  在最差情况下至多会扩展个 2 n 2^n 2n结点(考虑每个结点使用与不使用两种情况)。对每个结点扩展的时间复杂度有以下几部分:pop_heap()操作为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n),边选取操作为 O ( n 2 ) O(n^2) O(n2),右子结点生成为 O ( n ) O(n) O(n),左子节点为 O ( n 2 ) O(n^2) O(n2),即结点扩展时间复杂度为 O ( n 2 ) O(n^2) O(n2)。在最坏情况下算法的时间复杂度为 O ( 2 n n 2 ) O(2^nn^2) O(2nn2)

6 算法正确性证明

  考察深度为k的某结点 x k x_k xk的界限 b o u n d bound bound,可以发现他由两部分构成:

  • 已选边的实际耗费,因为所选取的每一条边的耗费都为0(规约化之后),故该部分一定包含在 b o u n d bound bound中,记为 h ( x k ) h(x_k) h(xk)
  • 到解结点耗费的估计,即剩余边最小耗费的估计,该部分也通过规约化求出,记为 g ∗ ( x k ) g^*(x_k) g(xk),可以发现 g ∗ ( x k ) ≤ g ( x k ) g^*(x_k)\leq g(x_k) g(xk)g(xk) g ( x k ) g(x_k) g(xk)表示当前结点到解的实际最小耗费。
       b o u n d ( x k ) = h ( x k ) + g ∗ ( x k ) bound(x_k)=h(x_k)+g^*(x_k) bound(xk)=h(xk)+g(xk)  显然,节点 x k x_k xk所能到达的任意一个解结点 x g x_g xg b o u n d ( x k ) ≤ b o u n d ( x g ) bound(x_k) \leq bound(x_g) bound(xk)bound(xg),且在解节点处实际耗费与限界相等,因为此时 g ∗ ( x k ) = g ( x k ) = 0 g^*(x_k)=g(x_k)=0 g(xk)=g(xk)=0。可以进一步证明,随着深度增加,限界只增大不减小,即 b o u n d ( x k ) ≤ b o u n d ( x k + 1 ) bound(x_k) \leq bound(x_{k+1}) bound(xk)bound(xk+1) ,因为对于节点 x k x_k xk所选边的耗费一定包含在了 g ∗ ( x k ) g^*(x_k) g(xk)的部分。
      若从队列中选出的结点 x g 0 x_{g0} xg0为解结点,则该解一定为最优解。选出的结点具有最小的限界,那么队列中其他所有的节点 x i x_i xi所能到达的解结点 x i g x_{ig} xig,有 b o u n d ( x i g ) ≥ b o u n d ( x i ) ≥ b o u n d ( x g 0 ) bound(x_{ig})\geq bound(x_i) \geq bound(x_{g0}) bound(xig)bound(xi)bound(xg0)。则 x g 0 x_{g0} xg0为全局最优解。

7 完整代码

#include<cstdio>
#include<cmath>
#include<iostream>
#include<algorithm>
#include <string>
#include<queue>
#include<limits.h>
#include<vector>
using namespace std;
//耗费矩阵
vector<vector<int>> initialCost =
{
    {0,1,2,3,4,5},
    {1,INT_MAX,17,7,35,18},
    {2,9,INT_MAX,5,14,19},
    {3,29,24,INT_MAX,30,12},
    {4,27,21,25,INT_MAX,48},
    {5,15,16,28,18,INT_MAX}
};

vector<vector<int>> initialCost2 =
{
    {0,1,2,3,4},
    {1,INT_MAX,5,2,10},
    {2,2,INT_MAX,5,12},
    {3,3,7,INT_MAX,5},
    {4,8,2,4,INT_MAX},
};

vector<vector<int>> initialCost3 =
{
    {0,1,2,3,4},
    {1,INT_MAX,5,INT_MAX,7},
    {2,INT_MAX,INT_MAX,4,2},
    {3,3,3,INT_MAX,2},
    {4,3,2,8,INT_MAX},
};

typedef struct Node
{
    vector<vector<int>> cost;
    int bound;
    int cityNum;
    vector<int> path;   //记录路径:path[i]为结点i的前一个点
    vector<int> inpath; //逆路径:path[i]为结点i的下一个点
    Node() {}
    Node(vector<vector<int>> c, int b, int ci) :cost(c), bound(b), cityNum(ci) 
    {
        path = vector<int>(ci + 1,-1);
        inpath = vector<int>(ci + 1,-1);
    }
    Node(vector<vector<int>> c, int b, int ci, vector<int>p,vector<int>ip) :cost(c), bound(b), cityNum(ci), path(p), inpath(ip){ }
}Node;

queue<Node> searchQueue;
vector<Node> searchHeap;
int i = INT_MAX;
//对耗费矩阵做规约化操作:使每一行每一列都至少有一个0
void convention(Node& eNode)
{
    //对行规约化:同时记录下0在第几列
    bool* zeroLocation = new bool[eNode.cityNum+1];
    for (int i = 0; i < eNode.cityNum + 1; i++)
        zeroLocation[i] = false;

    for (int i = 1; i <= eNode.cityNum; i++)
    {
        /*
        if (eNode.cost[i][0] == -1)
            continue;*/

        int minIndex = 1;
        int minum = eNode.cost[i][1];
        for (int j = 1; j <= eNode.cityNum; j++)
        {
            if (minum  > eNode.cost[i][j])
            {
                minIndex = j;
                minum = eNode.cost[i][j];
            }
        }
        eNode.bound += minum;

        for (int j = 1; j <= eNode.cityNum; j++)
        {
            if (eNode.cost[i][j] != INT_MAX)
            {
                eNode.cost[i][j] -= minum;
            }
        }

        zeroLocation[minIndex] = true;
    }
    //对列规约化:当该列不存在0是进行扫描
    for (int i = 1; i <= eNode.cityNum; i++)
    {
        if (!zeroLocation[i])
        {
            int minum = eNode.cost[0][i];
            for (int j = 1; j <= eNode.cityNum; j++)
            {
                minum = min(minum, eNode.cost[j][i]);
            }
            eNode.bound += minum;

            for (int j = 1; j <= eNode.cityNum; j++)
            {
                if (eNode.cost[j][i] != INT_MAX)
                {
                    eNode.cost[j][i] -= minum;
                }
            }
        }
    }
}
//堆的排序函数:make_heap()的排序函数可以这样理解:堆顶元素  比较   其他元素  全部为false
bool nodeGreater(Node x, Node y)
{
    return x.bound > y.bound;
}

//旅行商问题:遍历一圈回到出发点结点编号为0到n-1
void TSP(int num)
{
    Node initial = Node(initialCost, 0, num);
    convention(initial);
    searchHeap.push_back(initial);
    //堆不为空说明还有节点待扩展
    while (!searchHeap.empty())
    {
        Node eNode = searchHeap[0];
        pop_heap(begin(searchHeap), end(searchHeap), nodeGreater);     //删除堆根节点,实际上是将第一个元素移到末尾
        searchHeap.pop_back();                  //删除末尾元素
        if (eNode.cost.size() == 1)
        {
            printf("最小总代价为%d\r\n", eNode.bound);
            printf("最短周游路径为:1 ");
            int index = 1;
            for (int i = 0; i < num; i++)
            {
                index = eNode.inpath[index];
                printf("%d ", index);
                
            }
            break;
        }
        //对当前结点进行扩展
    
        //选择一个已规约为0的边进行选取、舍弃操作。  边为: i,j
        //(找一行中除0外的最小值)  有两个0则不考虑该行
        int maxminSide = 0;
        int maxIndex[] = { 1,1 };     //记录选取边的索引
       for (int i = 1; i < eNode.cityNum + 1; i++)
        {
            int minum = INT_MAX;
            int minIndex[] = { i,1 };   //当前行 0 的位置
            int zeroNum = 0;
            for (int j = 1; j < eNode.cityNum + 1; j++)
            {
                if (eNode.cost[i][j] == 0)
                {
                    if(++zeroNum == 2)
                        break;
                    minIndex[1] = j;
                }
                else if (eNode.cost[i][j] < minum)
                {
                    minum = eNode.cost[i][j];

                }
            }
            //若超过2个0,则忽略该行
            if (zeroNum >= 2)
                continue;

            if (minum > maxminSide)
            {
                maxminSide = minum;
                maxIndex[0] = minIndex[0];
                maxIndex[1] = minIndex[1];
            }
        }
        
        //创建右孩子:不使用边(i,j)

        vector< vector<int>> rCost = eNode.cost;
        vector<int> path = eNode.path;
        rCost[maxIndex[0]][maxIndex[1]] = INT_MAX;
        int rbound = eNode.bound;
        if (eNode.cityNum == 1)  //只剩一个边,不用必然为无穷
        {
            rbound = INT_MAX;
        }
        else
        {
            rbound = eNode.bound + maxminSide;
            if (rbound < 0)
                rbound = INT_MAX;
        }
        //对第i行约化
        for (int i = 1; i < eNode.cityNum+1;i++)
        {
            if (rCost[maxIndex[0]][i] != INT_MAX)
                rCost[maxIndex[0]][i] -= maxminSide;
        }
        Node rNode = Node(rCost, rbound, eNode.cityNum, eNode.path,eNode.inpath);


        //创建左孩子:选用边(i,j)
        int lcityNum = eNode.cityNum - 1;
        //根据索引求边的起始点和终止点,将边(j,i)置为无穷大
        int start = eNode.cost[maxIndex[0]][0];
        int dest = eNode.cost[0][maxIndex[1]];

        vector<int> row(num + 1, -1);
        vector<int> column(num + 1, -1);
        for (int i = 1; i < eNode.cityNum + 1; i++)
        {
            row[eNode.cost[i][0]] = i;
            column[eNode.cost[0][i]] = i;
        }

        int startIndex = row[start];
        int destIndex = column[dest];


        if (startIndex != -1 && destIndex != -1 )
            eNode.cost[startIndex][destIndex] = INT_MAX;

        //修改路径,同时防止除了到1以外的回路出现,再置无穷
        //且到1的回路只能最后包括,还剩一天边不能做以下处理
        vector<int> lpath = eNode.path;
        lpath[dest] = start;
        vector<int> linpath = eNode.inpath;
        linpath[start] = dest;
        if (lcityNum > 1) 
        {
            int u = start, v = dest;
            while (lpath[u] != -1)
            {
                u = lpath[u];
            }
            while (linpath[v] != -1)
            {
                v = linpath[v];
            }

            destIndex = row[v];
            startIndex = column[u];


            if (startIndex != -1 && destIndex != -1 && (destIndex != startIndex))
                eNode.cost[destIndex][startIndex] = INT_MAX;
        }
        //去除i行,j列的新耗费矩阵
        vector<vector<int>> lCost(eNode.cityNum);
        int row1 = 0;
        for (int i = 0; i < eNode.cityNum + 1; i++)
        {
            if (i != maxIndex[0])
            {
                for (int j = 0; j < eNode.cityNum + 1; j++)
                {
                    if (j != maxIndex[1])
                        lCost[row1].push_back(eNode.cost[i][j]);
                }
                row1++;
            }
        }

        Node lNode = Node(lCost, eNode.bound, lcityNum, lpath, linpath);
        //对新矩阵规约化
        convention(lNode);


        //将左右节点加入堆
        searchHeap.push_back(rNode);
        searchHeap.push_back(lNode);
        push_heap(begin(searchHeap), end(searchHeap), nodeGreater);
    }
}
int main()
{
    TSP(5);
}
  • 36
    点赞
  • 162
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
旅行商问题是一个NP-hard问题,因此需要采用一些高效的算法来解决。其中分支限界法是一种常用的解决TSP问题的算法分支限界法的基本思想是:将问题的解空间树划分为多个子树,每个子树代表一个可行解,通过限界函数对每个子树进行枝,从而减少搜索空间,提高搜索效率。具体来说,分支限界法通过不断地分支和限界,逐步缩小搜索空间,最终找到最优解。 下面是旅行商问题分支限界法的Python实现: ```python import numpy as np class Node: def __init__(self, path, bound, cost): self.path = path self.bound = bound self.cost = cost def tsp_branch_bound(graph): n = graph.shape[0] nodes = [] for i in range(n): path = [i] bound = bound_func(graph, path) cost = 0 nodes.append(Node(path, bound, cost)) nodes.sort(key=lambda x: x.bound) best_path = None best_cost = np.inf while nodes: node = nodes.pop(0) if node.bound >= best_cost: continue if len(node.path) == n: cost = node.cost + graph[node.path[-1], node.path[0]] if cost < best_cost: best_cost = cost best_path = node.path + [node.path[0]] else: for i in range(n): if i not in node.path: path = node.path + [i] bound = bound_func(graph, path) cost = node.cost + graph[path[-2], i] nodes.append(Node(path, bound, cost)) nodes.sort(key=lambda x: x.bound) return best_path, best_cost def bound_func(graph, path): n = graph.shape[0] bound = 0 for i in range(n): if i not in path: min_cost = np.min(graph[i, :]) bound += min_cost for i in range(len(path) - 1): bound += graph[path[i], path[i+1]] return bound # 示例 graph = np.array([[0, 10, 15, 20], [10, 0, 35, 25], [15, 35, 0, 30], [20, 25, 30, 0]]) best_path, best_cost = tsp_branch_bound(graph) print("最短路径为:", best_path) print("最短路径长度为:", best_cost) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值