学校刚刚小学期算法实训答辩完,我抽到的是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更好