回溯算法 --- 例题8.旅行售货员问题

一.问题描述

某售货员要到若干城市去推销商品, 已知各城市之间的路程(旅费), 他要选定一条从驻地出发, 经过每个城市一遍, 最后回到驻地的路线, 使总的路程(总旅费)最小。

二.解题思路

旅行售货员问题的解空间是一棵排列树。对于排列树的回溯法与生成1, 2, ……n的所有排列的递归算法Perm类似。开始时x=[1, 2, ……n], 则相应的排列树有x[1:n]的所有排列构成。

在递归算法Backtrack中, 当i=n时, 当前扩展节点是排列树的叶节点的父节点。此时算法检测图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边。如果这两条边都存在, 则找到一条旅行员售货回路。此时, 算法还需要判断这条回路的费用是否优于已找到的当前最优回流的费用bestc。如果是, 则必须更新当前最优值bestc和当前最优解bestx。

当i<n时, 当前扩展节点位于排列树的第i-1层。图G中存在从顶点x[i-1]到顶点x[i]的边时, x[1:i]构成图G的一条路径, 且当x[1:i]的费用小于当前最优值时算法进入树的第i层, 否则将剪去相应的子树。

代码如下:

// 旅行售货员问题
#include<bits/stdc++.h>
using namespace std;
class Traveling
{
    friend int TSP(int **, int *, int , int);
    private:
        void Backtrack(int i);
        int n,      //图G的顶点个数
            *x,     //当前解
            *bestx; //当前最优解
        int **a,   //图G的邻接矩阵
            cc,     //当前费用
            bestc,  //当前最优值
            NoEdge; //无边标记
};
void Traveling::Backtrack(int i)
{
    if(i == n)
    {
        cout<<"当前第"<<i<<"层为最后一层"<<",选择"<<x[i]<<endl;
        if(a[x[n-1]][x[n]] != NoEdge && a[x[n]][1] != NoEdge && (cc+a[x[n-1]][x[n]]+a[x[n]][1] < bestc || bestc==NoEdge))
        {
            cout<<"得到一个更优解:";
            for(int j=1; j<=n; j++)
            {
                bestx[j] = x[j];
                cout<<x[j]<<" ";
            }
            bestc = cc+a[x[n-1]][x[n]]+a[x[n]][1];
            cout<<"最优解更新为:"<<bestc<<endl;
        }
        else 
        {
            for(int j=1; j<=n; j++)
                cout<<x[j]<<" ";
            cout<<"此路径得不到更优解,回溯到第"<<i-1<<"层"<<endl;
        }
    }
    else
    {
        for(int j=i; j<=n; j++)
        {
            //是否可以进入x[j]子树
            if(a[x[i-1]][x[j]] != NoEdge && (cc+a[x[i-1]][x[j]] < bestc || bestc==NoEdge))  //剪枝函数
            {
                //搜索子树
                swap(x[i], x[j]);  //必须交换,这样才可以做到使得x[2:i]的表示已经选过的,x[i+1:n]表示还未选过!!!
                cc += a[x[i-1]][x[i]];
                cout<<"当前第"<<i<<"层"<<",选择"<<x[i]<<",递归深入一层,将到达第"<<i+1<<"层"<<endl;
                Backtrack(i+1);
                cout<<"当前第"<<i+1<<"层,递归回退一层,将到达第"<<i<<"层"<<endl;
                cc -= a[x[i-1]][x[i]];
                swap(x[i], x[j]);
            }
            else cout<<"不满足剪枝函数,对应子树被剪枝"<<endl;
        }
    }
}
int TSP(int **a, int *v, int n, int NoEdge)
{
    Traveling Y;
    // 初始化Y
    Y.x = new int[n+1];     
    for(int i=1; i<=n; i++) Y.x[i] = i;
    Y.a = a;
    Y.n = n;
    Y.bestc = INT_MAX;
    Y.bestx = v;
    Y.cc = 0;
    Y.NoEdge = NoEdge;
    Y.Backtrack(2);     //搜索x[2:n]的全排列
    delete[] Y.x;
    return Y.bestc;
}
int main()
{
    cout<<"请输入旅行地点个数:";
    int n;
    while(cin>>n && n)
    {
        cout<<"请输入邻接矩阵"<<endl;
        int **a = new int*[n+1];
        for(int i=0; i<=n; i++) a[i] = new int[n+1];
        for(int i=1; i<=n; i++)
            for(int j=1; j<=n; j++)
                cin>>a[i][j];
        int NoEdge = 0;
        int *v = new int[n+1];
        for(int i=0; i<=n; i++) v[i] = 0;
        int ans = TSP(a, v, n, NoEdge);
        cout<<"旅行路费最少为:"<<ans<<endl;
        for(int i=0; i<=n; i++) delete[] a[i];
        delete[] a;
        delete[] v;
        cout<<"请输入旅行地点个数";
    }
    system("pause");
    return 0;
}

运行结果:

结合排列树,大家自己动手画一画,应该会比较清晰.

这里有一个问题就是为啥需要 swap(x[i], x[j])呢?
在前面的N皇后问题以及图的m着色问题都不需要交换,为什么在这里我们需要交换呢?
我个人的看法是:因为在这里只有交换了x[i]和x[j],这样才能使得 x[2 : i]表示都是已经选择过的, x[i+1 : n]表示我们还没有选择的,这样递归深入后,就可以从j == i(相对于上一层而言是i+1)开始往后遍历.
这就和经典的全排列问题是一个框架,通过交换来保证之前的都是已经选择的,选择的情况由x数组给记录下来,之后的x[i+1:n]都是我们还未选择的,所以利用for循环(j==i开始)一个一个去尝试看看能不能找到最优解.如果大家还是不太清楚,建议再回头看看全排列的解题方法,代码我给大家贴在下面.

但是如果说实在不想要交换可不可以呢?
答案是可以的,但是我们必须要有一个数组isSelect记录了元素是否被选择过了,然后我们每递归深入一层也可以从j==1开始遍历,只要isSelect[j]为True的话,那么就直接continue;记得回溯的时候撤销操作.但是这样做损失了顺序信息,对于一些题目来说不可行,当然如果你比较执着的话,当然还可以再用一个数组来记录顺序信息,不过显然这并不值得做.
不用交换方法的就比如之前我们说过的0-1背包问题的回溯解法:用一个select数组记录物品是否被选择.最后可以通过select数组得到整体的选择情况.但是不具有先后顺序,我们仅仅是知道了它被选择了没有,并不知道它是第几个选择的,当然了,对于这一题来说这就已经够了.
而如果对于需要考虑先后顺序的题目,我们就需要使用的是交换的做法,到达叶节点后我们通过x数组得到的答案就已经包含了顺序的信息,这很重要!


全排列解题代码(针对数组中无重复元素)

template <typename T>
void Perm(T list[], int low, int high)
{
    if(low == high)
    {
        static int count = 1;
        cout<<"第"<<count++<<"个排列为:";
        for(int i=0; i<high; ++i)
        {
            cout<<list[i];
            cout.width(4);
        }
        cout<<'\n';
    }
    for(int i=low; i<high; ++i)
    {
        swap(list[i], list[low]);  //这里的swap保证了list[1:low]是已经选择的,list[low+1:high]是未选择的
        Perm(list, low+1, high);
        swap(list[i], list[low]); 
    }
参考毕方明老师《算法设计与分析》课件.

欢迎大家访问个人博客网站---乔治的编程小屋, 和我一起加油吧!

posted @ 2021-12-21 15:44  PGokc  阅读(918)  评论(0编辑  收藏  举报