问题描述
回溯法可以处理货郎担问题,分支定界法也可以处理货郎担问题,回溯法和分支定界法哪个算法处理货郎担问题效率更高呢?
实现回溯法、分支定界法,以及不同的界值函数(课上讲过的或者自己新设计的),通过随机产生10个不同规模的算例(城市数量分别为10,20,40,80,100,120,160,180,200,500,或者其它规模),比较回溯法和分支定界法在相同界值函数下的执行效率。另外,分别比较回溯法和分支定界法在不同界值函数下的执行效率。
问题分析
在本次实验中,首先需要理解回溯法和分支定界法的原理、基本实现和区别。
值得注意的是,回溯法的可以用于求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标通常是找出满足约束条件的一个解或最优解。而且,回溯法主要以深度优先的方式搜索解空间树,而分支限界法主要以广度优先或以函数优先的方式搜索解空间树。
下面,我将对两种算法进行简单分析。
回溯法分析
回溯法也称试探法,是一种通过试错来解决问题的算法策略,其基本思想是从初始状态出发,搜索其所能到达的所有“状态”。
对于本题的货郎担问题(旅行商问题),我们根据回溯法的求解步骤来求解:①针对问题定义解空间;②判断问题是否满足多米诺性质;③搜索解空间树,确定剪枝函数;④确定存储搜索路径的数据结构。
首先,货郎担从起始城市出发,经过每个城市一遍,最后回到起始城市。那么该问题的解空间为:①设第i个出现的城市为 ,解向量就为(
,
, … ,
)(注:比如
城市的编号为2,
为7,那么该解向量为(2,7,…));②显约束为n个城市的编号
= 1, 2, … , n(城市的编号互不重复);③隐约束为有从
到
的边,有从
到
的边(即题目要求的能回到出发城市),
≠
(即两个城市的编号不能重复)。综上三点,就是该问题的解空间。
多米诺性质(也称为最优子结构性质)是指一个问题的最优解包含其子问题的最优解。多米诺性质问题的解可以表示为一系列决策的结果,其中每个决策依赖于前一个决策。货郎担问题满足多米诺性质,因为每次选择下一个城市都依赖于当前所在的城市。
对于解空间树,一般可以分为子集树和排列树。货郎担相当于n个城市的一个排列,是一个经典的排列树问题。
对于本问题用于存储搜索路径的数据结构。我们可以使用数组 x 和 bestx,其中 x 存储当前路径,而 bestx 存储找到的最佳路径。
分支定界法分析
分支界限法与回溯法类似,也是在问题解空间中搜索问题解的一种算法。
对于本题的货郎担问题(旅行商问题),我们可以根据分支定界法的求解步骤来求解:①针对问题定义解空间;②判断问题是否满足多米诺性质;③确定剪枝函数与界函数。该部分与回溯法类似,界函数后续会详解。
此外,在分支限界法中,每个活结点只有一次机会成为扩展结点。一旦成为扩展结点,就一次性产生其所有儿子结点。在这些子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。有两种常见的搜索方法:①队列式搜索法,即像队列一样按照先进先出原则选取下一个节点为扩展节点;②优先队列式搜索法,即按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。
算法流程图及伪代码
流程图和伪代码供参考,有不足之处。
回溯法
通过回溯方法解决旅行商问题,首先初始化城市数量和邻接矩阵,然后使用递归`backtrack`方法探索所有可能的城市访问顺序。
backtrack函数是解决旅行商问题的核心部分,采用回溯算法的思想来探索所有可能的城市访问顺序。该函数的主要思路可以分为几个步骤:
①设置终止条件:函数首先检查当前深度i是否超过城市数量n。如果是,表示已经访问了所有城市,此时更新当前最佳代价bestc和最佳路径bestx。这是因为在遍历到这一点时,已经找到了一个完整的解决方案;
②交换城市:在未达到终止条件时,使用一个循环遍历当前深度i到城市数量n的所有城市。在每次循环中,通过交换位置i和j上的城市来生成新的路径组合。这一步是生成新解的关键,通过对城市的排列组合来探索不同的路径;
③路径合法性检查:在交换后,调用check函数检查当前路径是否合法。该检查确保当前访问的城市之间存在边相连(需要注意,最后一个城市要与起始城市有路径,这个不能遗忘),这对于确保路径的有效性至关重要。如果路径不合法,直接跳过后续处理,继续下一个循环;
④减枝:如果路径合法且当前城市不是最后一个城市,则进一步判断当前路径的代价cc加上新边的代价是否小于当前最佳代价bestc。如果满足条件,更新当前代价并递归调用backtrack进入下一深度。通过这种剪枝策略,可以减少一些不必要的计算,避免探索那些明显不会成为最优解的路径。
⑤考虑回路:因为旅行商要回到初始城市,所以当当前城市是最后一个城市时,需要考虑从最后一个城市返回起始城市的路径。并且在计算代价时,要添加从最后一个城市到起点的边的代价,并进行相应的更新和递归;
⑥递归结束后需要恢复现场:每次递归调用结束后,通过再次交换城市位置恢复到之前的状态,以便在下一次循环中继续探索其他组合。这种恢复机制是回溯算法的基本特征,确保了每次探索都在一个干净的状态下进行。

Procedure Backtrack (i)
If i > n Then
bestc = cc // 最短路径长度
bestx = x // 解向量
Else
For j = i To n Do
Swap (x [i], x [j]) // 交换元素 i 和 j 的位置
If Check (i) Then // 检查交换后的解是否可行
If i < n And cc + a [x [i - 1]] [x [i]] < bestc Then
cc = cc + a [x [i - 1]] [x [i]] // 更新当前解的值
Backtrack (i + 1) // 递归调用,处理下一个元素
cc = cc - a [x [i - 1]] [x [i]] // 回溯,恢复 cc 的值
End If
If i == n And cc + a [x [i - 1]] [x [i]] < bestc Then
cc = cc + a [x [i - 1]] [x [i]] + a [x [i]] [1] // 更新当前解的值
Backtrack (i + 1) // 递归调用,处理下一个元素
cc = cc - a [x [i - 1]] [x [i]] - a [x [i]] [1] // 回溯,恢复 cc 的值
End If
End If
Swap (x [i], x [j]) // 回溯,恢复 x 的值
End For
End If
End Procedure // 回溯过程结束
Procedure Swap (i, j)
temp = x [i]
x [i] = x [j]
x [j] = temp
End Procedure // 交换两个元素的位置
Function Check (pos)
If pos < 2 Or (pos < n And a [x [pos - 1]] [x [pos]] != NoEdge) Or (pos == n And a [x [pos - 1]] [x [pos]] != NoEdge And a [x [pos]] [1] != NoEdge) Then
Return True // 如果满足条件,则返回 True,表示解是可行的
Else
Return False // 否则,返回 False,表示解是不可行的
End If
End Function // 检查过程结束
分支定界法
要使用分支定界法来解决旅行商问题(TSP),首先要考虑我们上下界的计算方式,下面是本次我采用的计算方法:
①计算上界:使用贪心算法来计算一个路径的上界,从第一个城市出发,遍历所有城市,每次选择代价最低的未访问城市加入序列中,最后返回到起始城市,更新总代价。
②计算下界:根据当前城市的排列和代价矩阵,使用不同的策略来计算下界。在本问题中,我分成三大种情况。(1)首先,如果旅行商位于起始城市,那么,我们找出每个城市与其他城市相连最小的两条边(比如城市A与4个城市相连,路径分别为2、4、3、6,那么我们取2和3,这是与城市A相连路径最小的,相当于离开和回来该城市的最短路径),那么找出2条边后,我们将其相加除以2,再将各个城市的相加,就是下界。(2)如果旅行商不在初始城市且不是最后一个城市,那么,我们将旅行商之前途径的城市路径先相加。然后,我们和第一种情况相同方法,计算剩下的城市(找出最小的两条边相加再除2),即可获得下界;(3)如果旅行商位于最后一个城市,那么,我们将旅行商之前途径的城市路径相加。然后,判断最后一个城市有无回到初始城市的路径,如果没有,就加上一个Integer.MAX_VALUE,表示不可达;如果有,则加上该路径长度,得到下界。
③对于编写主要函数'bb4TSP',我通过构建一个优先队列来存储并扩展有潜力的节点,同时计算每个节点的代价下界来指导搜索方向,并利用上界进行剪枝以减少不必要的搜索,最终找到连接所有城市并返回起点的最短路径(具体代码见下)。
下面是对主要函数'bb4TSP'的流程图和伪代码。

function bb4TSP(cMatrix, n)
// 初始化城市排列 cityArrange,添加城市 0 到 n
initialize cityArrange with cities 0 to n
set level to 1
// 计算每个城市的最小入边和出边,存储在 minEdge 中
calculate minEdge as the minimum in and out edges for each city
// 计算上界 ub (使用贪心算法)
calculate ub as the upper bound
// 一次遍历不成功就值 Integer.MAX_VALUE
// 计算下界 lb (每个节点最小出边入边之和 * 1/2)
calculate lb as the lower bound
// 创建初始节点 curNode,包含城市排列、下界和层级
create initial node curNode with cityArrange, lb, and level
add curNode to priorHeap
// 如果还没到达最后一个城市
while level is not equal to n
// 从下一个城市开始遍历
for i from level + 1 to n
// 检查最后一个已排节点能否到达 i 节点
if there is an edge from the last arranged city to city i
// 交换存在已排好的最后一个节点有边的节点到下一个
swap the last arranged city with city i in curNode.cityArrange
// 计算下界
calculate temp as the lower bound for curNode.cityArrange, level + 1, and cMatrix
// 创建新节点
create new node with curNode.cityArrange, temp, and level + 1
// 如果大于上界,说明这个节点没有意义,剪枝
if newNode.lcost is greater than ub
continue to the next iteration
add newNode to priorHeap
// 交换回来,再找符合下一个节点进行扩展
swap city i with the last arranged city in curNode.cityArrange
if priorHeap is empty
break the loop
// 找出一层扩展结束后,最有潜力的节点进行扩展
curNode becomes the node with the highest potential from priorHeap
level becomes curNode.level
// 到达最后一个城市
if curNode.lcost is less than the minimum cost
// 更新最优解和最小代价
update bestH and minimum cost with curNode.cityArrange and curNode.lcost
end function
算法代码参考
回溯法
package Experiment2;
public class Back4TSP {
int NoEdge = -1;
int[][] a; // 邻接矩阵
int cc = 0; // 存储当前代价
int bestc = Integer.MAX_VALUE;// 当前最优代价
int[] x; // 当前解
int[] bestx;// 当前最优解
int n = 0; // 顶点个数
private void backtrack(int i) {//i为初始深度
if (i > n) {
//深度大于n的时候更新最低cost和最佳路径
bestc = cc;
bestx = x;
} else {
// 深度还没有超过最大深度
// 轮着交换
for (int j = i; j <= n; j++) {
// 交换位置i和位置j上的元素,更新路径
swap(x[i], x[j]);
if (check(i)) {
// i不是最后一个节点的时候,不用考虑从最后一个节点回到第一个节点的路
if (i < n && cc + a[x[i - 1]][x[i]] < bestc) {//当前解是否有可能成为最优解(进行剪枝)
// 更新最低cost
cc += a[x[i - 1]][x[i]];
// 进入下一深度
backtrack(i + 1);
// 恢复最低cost
cc -= a[x[i - 1]][x[i]];
}
// i是最后一个节点的时候,要考虑从最后一个节点回到第一个节点的路
if (i == n && cc + a[x[i - 1]][x[i]] + a[x[i]][1]< bestc) {
// 更新最低cost
cc += (a[x[i - 1]][x[i]] + a[x[i]][1]);
// 进入下一深度
backtrack(i + 1);
// 恢复最低cost
cc -= (a[x[i - 1]][x[i]] + a[x[i]][1]);
}
}
//恢复现场
swap(x[i], x[j]);
}
}
}
private void swap(int i, int j) {
int temp = x[i];
x[i] = x[j];
x[j] = temp;
}
public boolean check(int pos) {
//TODO
if(pos < 2) return true; //开始节点随意
if(pos<n && a[x[pos - 1]][x[pos]] != NoEdge) return true; //显示约束:未到达最后一层,解向量中第i个位置和第i+1个位置之间存在路径
if(pos==n && a[x[pos - 1]][x[pos]] != NoEdge && a[x[pos]][1] != NoEdge) return true; //到达最后一层,最后一个与前一个以及第一个之间都要存在路径
return false;
}
public void backtrack4TSP(int[][] b, int num) {
n = num;
x = new int[n + 1];
for (int i = 0; i <= n; i++)
x[i] = i;
bestx = new int[n + 1];
a = b;
backtrack(2);
}
}
分支定界法
package Experiment2;
import java.util.Collections;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Vector;
public class BB4TSP {
private int minCost = Integer.MAX_VALUE; //当前最小代价
private int[][] minEdge; //
public int getMinCost() {
return minCost;
}
public void setMinCost(int minCost) {
this.minCost = minCost;
}
Comparator<HeapNode> cmp = new Comparator<HeapNode>() { //e2的lcost值大于e1的lcost值,那么e2将被认为是大于e1的。
public int compare(HeapNode e1, HeapNode e2) {//从大到小排序
return e2.lcost - e1.lcost;
}//这个比较器实现了从大到小的排序。
};
private PriorityQueue<HeapNode> priorHeap = new PriorityQueue<HeapNode>(100, cmp);//存储活节点,大根堆
private Vector<Integer> bestH = new Vector<Integer>();
@SuppressWarnings("rawtypes")
public static class HeapNode implements Comparable{
Vector<Integer> cityArrange = new Vector<Integer>();//城市排列
int lcost; //代价的下界
int level;//0-level的城市是已经排好的
//构造方法
public HeapNode(Vector<Integer> cities,int lb, int lev){
cityArrange.addAll(0, cities);
lcost = lb;
level = lev;
}
@Override
public int compareTo(Object x) {//升序排列, 每一次pollFirst
int xu=((HeapNode)x).lcost;
if(lcost<xu) return -1;
if(lcost==xu) return 0;
return 1;
}
public boolean equals(Object x){
return lcost==((HeapNode)x).lcost;
}
}
/**
* 计算部分解的下界.
*
* @param cityArrange
* 城市的排列
*
* 当前确定的城市的个数.
* @param cMatrix
* 邻接矩阵,第0行,0列不算
*
* @exception IllegalArgumentException
*/
public int computeLB(Vector<Integer> cityArrange, int level, int[][] cMatrix) {
//TODO
int lowcost = 0;
int n = cMatrix.length - 1;
//已走部分解的路径,待×2
if(level==1) {//只有根节点时单独算下界
for(int i=1;i<=n;i++) {
lowcost+= (minEdge[i][0] + minEdge[i][1])/2;
}
return lowcost;
}
//计算已经确定的路径代价
for (int i = 1 ; i < level ; i++)
lowcost += cMatrix[cityArrange.get(i)][cityArrange.get(i + 1)];
//如果已经到达最后一个城市,就要检查这个城市与第一个城市之间有没有边可以走
//如果可以走通,那么在原本lowcost的基础上增加最后一个城市到第一个城市的cost
if (level == n && cMatrix[cityArrange.get(level)][1] != -1)
lowcost += cMatrix[cityArrange.get(level)][1];
//如果不能走通,那么就直接将lowcost置为MAXVALUE,表示此路不通
else if (level == n && cMatrix[cityArrange.get(level)][1] == -1)
lowcost = Integer.MAX_VALUE;
//如果还不是最后一个城市,从当前城市开始计算下界
else if (level != n){
for (int i = level+1 ; i <= n ; i++){
if(i != n){
//没有确定一条边的点
lowcost += (minEdge[cityArrange.get(i)][0] + minEdge[cityArrange.get(i + 1)][1]) / 2;
}
}
// lowcost += minEdge[1][1];
// lowcost += minEdge[level][0];
//确定一条边
lowcost += minEdge[1][1]==cMatrix[1][cityArrange.get(2)] ? minEdge[1][3] : minEdge[1][1];
lowcost += minEdge[cityArrange.get(level)][0] == cMatrix[cityArrange.get(level)-1][cityArrange.get(level)] ?
minEdge[cityArrange.get(level)][2] : minEdge[cityArrange.get(level)][0];
}
return lowcost;
}
/**
* 贪心法求TSP问题上界
* @param cMatrix
* @return int
*/
public int computeUB(int[][] cMatrix){
int numCities = cMatrix.length; // 获取城市的数量
boolean[] visited = new boolean[numCities]; // 创建一个布尔数组来跟踪哪些城市已经被访问过
int totalCost = 0; // 初始化总代价为0
int currentCity = 1; // 从第二个城市开始
int last = cMatrix.length-1;
for (int i = 1; i < numCities; i++) { // 遍历所有的城市
visited[currentCity] = true; // 标记当前城市为已访问
last--;
int nextCity = -1; // 初始化下一个城市为-1
int minCost = Integer.MAX_VALUE; // 初始化最小代价为最大整数值
for (int j = 1; j < numCities; j++) { // 遍历所有的城市
// 如果城市j未被访问,且从当前城市到城市j的代价小于最小代价,则更新最小代价和下一个城市
if (!visited[j] && cMatrix[currentCity][j] != -1 && cMatrix[currentCity][j] < minCost) {
minCost = cMatrix[currentCity][j];
nextCity = j;
}
}
if (nextCity != -1) { // 如果找到了下一个城市
totalCost += minCost; // 更新总代价
currentCity = nextCity; // 更新当前城市为下一个城市
}else {
if(last==0) //是最后一个城市
break;
else //不是最后一个城市,贪心失败
return Integer.MAX_VALUE;
}
}
// 返回到起始城市以完成旅行
if (cMatrix[currentCity][1] != -1) {
totalCost += cMatrix[currentCity][1];
} else {
totalCost = Integer.MAX_VALUE;
}
return totalCost; // 返回总代价
}
/**
* 计算每个城市最小的入边和出边
*
* @param cMatrix 邻接矩阵[n+1][n+1]:1->n
* @return minEdge
*/
public int[][] CalMinInOut(int[][] cMatrix){
// minEdge[i][0] 表示第i个城市的最小出边
// minEdge[i][1] 表示第i个城市的最小入边
// minEdge[i][2] 表示第i个城市的次小出边
// minEdge[i][3] 表示第i个城市的最小入边
int[][] minEdge = new int[cMatrix.length][4];
for (int i = 1 ; i < cMatrix.length ; i++){
// min1 保存城市i的最小出边
int min1 = Integer.MAX_VALUE;
// min2 保存城市i的最小入边
int min2 = Integer.MAX_VALUE;
// min3 保存城市i的次小出边
int min3 = Integer.MAX_VALUE;
// min4 保存城市i的次小入边
int min4 = Integer.MAX_VALUE;
for(int j = 0 ; j < cMatrix.length ; j++){
if(cMatrix[i][j] == min1) //第二次出现最小值
{
min3 = min1;
} else if(cMatrix[i][j] != -1 && cMatrix[i][j] < min1) { //更新最小值和次小值
min3 = min1;
min1 = cMatrix[i][j];
} else if (cMatrix[i][j] != -1 && cMatrix[i][j] < min3 && cMatrix[i][j]!= min1) { //更新次小值
min3 = cMatrix[i][j];
}
if(cMatrix[j][i] == min2){
min4 = min2;
}if (cMatrix[j][i] != -1 && cMatrix[j][i] < min2) {
min4 = min2;
min2 = cMatrix[j][i];
} else if (cMatrix[j][i] != -1 && cMatrix[j][i] < min4 && cMatrix[j][i] != min2) {
min4 = cMatrix[i][j];
}
}
// 更新minEdge
minEdge[i][0] = min1;
minEdge[i][1] = min2;
minEdge[i][2] = min3;
minEdge[i][3] = min4;
}
// 返回最小入边和出边
return minEdge;
}
/**
* 计算TSP问题的最小代价的路径.
*
* @param cMatrix 邻接矩阵,第0行,0列不算
* @param n 城市个数.
* @exception IllegalArgumentException exception
*/
public void bb4TSP(int[][] cMatrix, int n) {
//构造初始节点
Vector<Integer> cityArrange = new Vector<Integer>() ;//城市排列
cityArrange.add(0);//空出一个城市,与cMatrix一致
for(int i = 1; i<=n; i++) cityArrange.add(i);
int level = 1;//节点所属第几层
this.minEdge = CalMinInOut(cMatrix);// 算出每个城市的最小入边和出边
int ub = computeUB(cMatrix);// 上界
int lb = computeLB(cityArrange, level, cMatrix);// 下界
HeapNode curNode = new HeapNode(cityArrange, lb, level);// 第一个节点
this.priorHeap.add(curNode); // 添加第一个节点
while(level != n) // 如果还没到达最后一个城市
{
//从下一个城市开始遍历
for (int i = level + 1; i <= n ; i++){
//检查最后一个已排节点能否到达i节点
if (cMatrix[cityArrange.get(level)][cityArrange.get(i)] != -1){
//交换存在已排好的最后一个节点有边的节点到下一个
Collections.swap(curNode.cityArrange, level+1, i);
// 以下是计算该节点是否可以扩展
// 计算下界
int temp = computeLB(curNode.cityArrange, level + 1, cMatrix);
// 创建新节点
HeapNode node = new HeapNode(curNode.cityArrange, temp, level + 1);
// 如果大于上界 说明这个节点没有意义 这个节点的潜力太小 剪枝
if(node.lcost > ub)
continue;
this.priorHeap.add(node);
// 交换回来,再找符合下一个节点进行扩展
Collections.swap(curNode.cityArrange, level+1, i);
}
}
if(this.priorHeap.isEmpty()) break;
//找出一层扩展结束后,最有潜力的节点进行扩展
curNode = this.priorHeap.poll();
level = curNode.level;
}
// 到达最后一个城市
if (curNode.lcost < this.getMinCost()){
this.bestH = curNode.cityArrange;
this.setMinCost(curNode.lcost);
}
}
}
两种方法对比
题目中给的城市规模太大,我仅测试城市规模为5、10、15、20、25、30的几种情况。
经过实验,发现规模较小时(n=5、10),两者运行效率相近。但城市规模扩大时,分支定界法效率明显较高(n=30时回溯法耗时很长很长)。
此外,分支定界法的上下界算法也会影响算法效率。经过实践,更好更准确的下界会使分支定界法更加高效。

2196

被折叠的 条评论
为什么被折叠?



