旅行商(TSP)问题--DFS和状压DP两种解法

学校刚刚小学期算法实训答辩完,我抽到的是TSP问题,借此机会更个博文~

TSP问题阐述: 

旅行商需要选定一个城市出发,去若干城市推销产品,每个城市只能经过一次,最终返回初始城市,各城市之间旅费已知,问题目的是求解最小旅费(路径)

问题分析:该问题本质是遍历一张无向图,求解最短路;可选定某一节点作为初始节点,不断扩展节点,使得路径始终保持最短,最终回到初始节点得到全局最短路

问题关键:存储最优路径、存储子问题最优解、设置约束条件保证回路并且每个节点只访问一次  

一.DFS法:

1.问题分析:

(1)先构建一棵递归搜索树,描述所有可能的旅行路径,搜索树层数等于节点数;层数cur>n搜索到底,开始回溯,穷举所有可能路径,从而不断计算最优解

(2)变量定义: (1)matrix[][]邻接矩阵,存储节点间边的权值 (2)list[]和bestList[]路径数组,用于存储路径选择顺序(list[]赋值为所有节点,bestList赋值为0) (3)curLen和bestLen,不断更新当前和最佳路径长度

(3)限制条件:什么时候才能扩展该路径? 节点t与t+1之间有路径时并且curlen+dist(t,t+1)<bestLen

(4)这里规定起始点就是1号城市,最短路若能找到,那么从谁开始都是一样的,路径顺序有轮换对称性 

2.参考代码: 

import java.util.Scanner;

class Main {
    static int N=1000;
    static int INF=Integer.MAX_VALUE; //定义一个无穷大量
    static int n,m; //城市个数与边数
    static int [][]matrix =new int[N][N]; //定义邻接矩阵,存储结点边的权值
    static int []list =new int[N]; //记录路径选择顺序
    static int []bestList=new int[N]; //记录最优路径选择
    static int curLen=0; //记录当前已扩展的路径长度
    static int bestLen=INF; //记录可能的最佳路径长度


    public static void main(String[] args)
    {
        int a,b,s;
        Scanner sc=new Scanner(System.in);
        System.out.println("输入城市个数:");
        n=sc.nextInt();
        System.out.println("输入边数:");
        m=sc.nextInt();

        for(int i=1;i<=n;i++)  //赋初值
            for(int j=1;j<=n;j++)
                matrix[i][j]=INF;
        for(int i=0;i<=n;i++) {
            list[i]=i;
            bestList[i]=0;
        }

        System.out.println("输入城市编号以及路费:");
        for(int i=1;i<=m;i++) {
            a=sc.nextInt();
            b=sc.nextInt();
            s=sc.nextInt();
            matrix[a][b]=matrix[b][a]=s;
        }
        dfs(2);  //从搜索树第二层开始
        System.out.println("最少旅行费用为:"+bestLen);
        System.out.println("最少花费采取的路径为:");
        for(int i=1;i<=n;i++) {
            System.out.print(bestList[i]+"->");
        }
        System.out.print("1");
    }

    public static void dfs(int cur)  //递归函数,cur表示当前层数
    {
        if(cur>n)  //递归终止条件
        {
            if(matrix[list[n]][1]!=INF&&curLen+matrix[list[n]][1]<bestLen) //返程路径如果可选
            {
                for(int i=1;i<=n;i++) bestList[i]= list[i];
                bestLen=curLen+matrix[list[n]][1];
            }
        }
        else{ //扩展可能节点
            for(int k=cur;k<=n;k++) {
                if(matrix[list[cur-1]][list[k]]!=INF&&curLen+matrix[list[cur-1]][list[k]]<bestLen) {//下一个节点(t)可选的话
                    swap(list,cur,k);  //在当前选择中将该城市选中
                    curLen+=matrix[list[cur-1]][list[cur]];  //扩展新路径
                    dfs(cur+1); //继续向下搜索
                    curLen-=matrix[list[cur-1]][list[cur]];  //回溯,恢复现场
                    swap(list,cur,k);
                }
            }
        }
    }
    public static void swap(int []a, int i, int j) //扩展节点函数
    {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

代码说明:

(1)递归终止条件:cur>n,返程节点存在最短路径,那么说明回路成立,最优路径可以更新并输出(2)已扩展的路径末节点list[cur-1]和马上要扩展的节点list[k]之间存在最优路径,就可以继续扩展;利用swap函数交换k与cur的元素位置,表示更新不同可能的路径选择,并更新当前花费;主函数调用dfs(2) (第一层是规定为城市1,第二层开始才面临选择)

3.时空复杂度:

*时间复杂度:对于一个无向图,回路路径是首尾相同,中间(n-1)节点排列组合,总路径为n(n-1)!即n!种可能,通过递归搜索树也可以看出,因此一般情况下回溯法时间复杂度会达到O(n!)

*空间复杂度:空间复杂度与城市n数量紧密相关。由于开辟了一个二维邻接矩阵,空间复杂度达到O(n²);又开辟了一维路径数组,空间复杂度达到O(n),其他一些变量空间复杂度为O(1),因而总体空间复杂度为O(n²)

二.状压DP

1.问题分析:

考虑定义dp[1]{2,3,4}表示从1出发经过2,3,4回到1的最短路径,那么这样的情形可以表示为min(dp[2]{3,4}+dist(2,1),dp[3]{2,4}+dist(3,1),dp[4]{2,3}+dist(4,1)),还可以不断细化下去,直到求解最小子结构的最优解。于是可以考虑创建一张状态转移表dp[i][state],表示从i经过某状态回到初始城市的最优路径;state是二进制掩码,1表示访问过(走),0表示未访问过(不走) dp表可创建为dp[n][1<<n]描述不同节点组合对应的不同状态下的最优解

如何填充dp表? 根据定义,不难得到状态转移方程dp[j][state^(1<<i)]+arr[j][i],含义是经过某个不包含该城市的状态的最优解,加上该城市与起点城市的距离,所以可有: 起点为dp[0][1](城市0只经过0回到0),显然为0;dp[0][2](城市0从1经过回到0),显然为dist(0,1); dp[0][3] = min(dp[0][3], dp[1][2] + matrix[1][0]),以此类推……

理解:所谓状态压缩可以理解为n个节点针对于不同状态应该是n维数组,但这样处理空间复杂度会很高,而且很麻烦,所以可以压缩成二维数组的形式,通过二进制状态表示存储城市状态,这样很简单巧妙;然后利用一张二维表存储状态的最优解,再去搜索全局最优解。

2.参考代码:

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    static int N=1000;
    static int [][]matrix=new int[N][N]; //邻接矩阵
    static Scanner sc=new Scanner(System.in);
    static int n=sc.nextInt();//城市数目
    static int m;//无向图总边数
    static int[][] dp = new int[n][1 << n]; //表示从起点出发经过state集合中的所有城市,最后回到该城市的最短路径长度
    static int INF=Integer.MAX_VALUE;

    public static void main(String[] args)
    {
        int a,b,s;
        System.out.println("输入总边数:");
        m=sc.nextInt();
        System.out.println("输入节点与权值:");
        for(int i=0;i<m;i++)
        {
            a=sc.nextInt();
            b=sc.nextInt();
            s=sc.nextInt();
            matrix[a-1][b-1]=matrix[b-1][a-1]=s;
        }
        int minCost = tsp(matrix);
        System.out.println("最小旅行费用为:" + minCost);
    }

    public static int tsp(int [][]arr)
    {
        for (int[] d : dp) {// 初始化dp数组,起点为0号城市
            Arrays.fill(d, INF/2); //防止整数溢出
        }
        dp[0][1] = 0; // 起点为城市0,初始状态只包含城市0,距离为0

        for (int state = 1; state < (1 << n); state++) //遍历所有状态
        {
            for (int i = 0; i < n; i++) //遍历状态下所有终点城市
            {
                if ((state & (1 << i)) == 0) continue; // 状态如果不包含城市i,就处理下个城市
                for (int j = 0; j < n; j++)  //遍历所有起点城市
                {
                    if (i!=j && (state & (1 << j)) != 0) //从城市j转移到城市i
                    {
                        dp[i][state] = Math.min(dp[i][state], dp[j][state ^ (1 << i)] + arr[j][i]); //状态转移方程
                    }
                }
            }
        }
        int minCost = INF;
        for (int i = 1; i < n; i++) { //求全局最短路
            minCost = Math.min(minCost, dp[i][(1 << n) - 1] + arr[i][0]);
        }
        return minCost;
    }
}

代码说明:

(1)外层循环遍历所有状态state

(2)内层循环1:遍历所有终点城市i。如果城市不在该状态里,就处理下一个城市(利用&运算结果是否为0判断城市i在不在state里)

(3)内层循环2:遍历所有起点城市j。j在状态里并且j还没到终点,考虑j可以向i转移,则利用状态转移方程dp[j][state^(1<<i)]+arr[j][i]填充dp表的值(利用异或运算将state第i位抛掉,即i不在状态里时的最优解加上dist(i,j),从而将其扩展进去)

3.时空复杂度:

(1)时间复杂度:该方法处理的核心就是状态转移。对于n个城市,根据dp表定义可知存在2^n个子集,因为每个状态都需要计算每个城市作为终点的最优路径长度,所以总的状态数目为n2^n;每次在状态转移时都要考虑不超过n-1个城市作为下一城市扩展进来,所以需要计算(n-1) n2^n种转移情形。所以总的时间复杂度将达到O(n^2*2^n) 

(2)空间复杂度:由于开辟状态表dp[n][1<<n],宽度为n,长度为2^n,因此空间复杂度达到O(n2^n);又开辟了二维邻接矩阵,空间复杂度达到O(n^2),一些变量等占据空间复杂度为O(1),因此算法整体空间复杂度近似为O(n2^n)

三.两种算法效率对比:
1.空间效率:状压DP空间复杂度是指数级别的,问题规模较大时,会比DFS高很多

2.时间效率:状态压缩DP在问题规模特别小时,如n<5时,时间效率可能不如回溯法,但差别不算很大;但问题规模稍大一点时,该方法效率远高于回溯法。如n=20时,状压时间复杂度数量级为10^8,但回溯法数量级已经达到10^18

3.总之:在时间效率上,问题规模较小时二者均可,差别并不大;问题规模较大时,选择DP更好

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值