前言
NEFU,计算机与控制工程学院,基于C/C++的算法设计与分析课程
实验四 回溯算法设计
环境
操作系统:Windows 10
IDE:Visual Studio Code、Dev C++ 5.11、Code::Blocks
说明
“实验四 回溯算法设计” 包含以下问题
- 0-1背包问题
- 旅行售货员问题
其他联系方式:
Gitee:@不太聪明的椰羊
B站:@不太聪明的椰羊
一、实验目的
掌握用回溯法解题的算法框架;根据回溯法解决实际问题。
二、实验原理
算法总体思想:回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
(1)问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。
(2)显约束:对分量xi的取值限定。
(3)隐约束:为满足问题的解而对不同分量之间施加的约束。
(4)解空间:对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
基本步骤:
(1)针对所给问题,定义问题的解空间,主要有子集树(如图1所示)和排列树(如图2所示)两种解空间形式。
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
图1 子集树
图2 排列树
三、实验内容
1、0-1背包问题
有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
要求每个物品要么放进背包,要么不放进背包。
1.1 分析
解空间:子集树
可行性约束函数:∑wixi≤C
限界函数:Bound()
backtrack函数是核心函数,通过递归地枚举所有可能的解,找到问题的最优解。
函数的每一步如下:
1. 检查是否搜索到了叶子节点
如果当前已经枚举完所有物品,即t > n,则说明已经搜索到了叶子节点,需要根据当前的解更新最优解,并返回上一级。
2. 搜索左子树(选择物品放入背包)
在当前节点选择将下一个物品放入背包,将物品的状态标记为已选,并更新节点的状态(即当前重量和当前价值)。然后递归调用backtrack函数,进入下层节点。
3. 搜索右子树(选择物品不放入背包)
在当前节点选择将物品不放入背包,将物品的状态标记为未选,并更新节点的状态(即不更新即当前重量和当前价值)。然后递归调用backtrack函数,进入下层节点。
4. 回溯
当左右子树都已经被搜索完毕后,需要将当前节点的状态恢复到进入该节点前的状态,然后返回上一级。
整个backtrack函数就是按照上述步骤不断递归地搜索所有可能的解,找到问题的最优解。递归调用backtrack函数之前,需要将当前物品按照单位重量价值从小到大排序,以便在搜索过程中更快地排除无用解。
1.2 代码
#include <iostream>
#include <algorithm>
using namespace std;
#define N 3 // 物品的数量
#define C 16 // 背包的容量
// 下面的数组从1开始使用,用-1给下标是0的位置占上,-1没有意义。
int w[N + 1] = {-1, 10, 8, 5}; // 每个物品的重量
int v[N + 1] = {-1, 5, 4, 1}; // 每个物品的价值
int x[N + 1] = {-1, 0, 0, 0}; // 解,但不一定是最优解,x[i]=1代表物品i放入背包,0代表不放入
int CurWeight = 0; // 当前放入背包的物品总重量
int CurValue = 0; // 当前放入背包的物品总价值
int BestValue = 0; // 最优值;当前的最大价值,初始化为0
int BestX[N + 1]; // 最优解;BestX[i]=1代表物品i放入背包,0代表不放入
int bound(int t) // 上界函数,求t物品对应节点的上界限
{
int cleft = C - CurWeight;
int b = CurValue; // b存当前节点的上界限
while (t <= N && w[t] <= cleft)
{
cleft = cleft - w[t]; // 物品t放入背包,背包剩余容量减少
b = b + v[t]; // 物品t放入背包,上界限增加
t++;
}
// 物品已经按照单位重量价值从大到小排序,当前物品不能完全放入背包时
if (t <= N)//将当前物品在背包中能够贡献的最大价值加入到当前节点的上界限b中
b = b + v[t] * cleft / w[t];
return b;//返回传入的t值对应节点的上界限
}
void backtrack(int t)
{
if (t > N)//检查是否搜索到了叶子节点
{
if (CurValue > BestValue)
{//根据当前的解更新最优解,并返回上一级
BestValue = CurValue;
for (int i = 1; i <= N; ++i)
BestX[i] = x[i];
}
}
else
{
// 左子树
if ((CurWeight + w[t]) <= C)
{
x[t] = 1;//标记当前物品放入
CurWeight += w[t];//当前物品放入背包,进入左子树
CurValue += v[t];
backtrack(t + 1);//进入下一层(下一个物品)
CurWeight -= w[t];//回溯回来后
CurValue -= v[t];//当前物品不放入背包,进入右子树
}
// 右子树
// bound(t + 1)求的是当前节点不放入时当前节点的上界限
// 剪枝函数,判断当前节点的下层节点是否需要进一步搜索
if (bound(t + 1) > BestValue) // 考虑上界函数时增加的代码
{
x[t] = 0;//标记当前物品不放入
backtrack(t + 1);//进入下一层(下一个物品)
}
}
}
void sort(int *w, int *v, int n)
{
int i, j, temp;
for (i = 1; i < n; i++)
for (j = 1; j < n - 1 - i; j++)
{
if (v[i] / w[i] < v[i + 1] / w[i + 1])
{
temp = w[i];
w[i] = w[i + 1];
w[i + 1] = temp;
temp = v[i];
v[i] = v[i + 1];
v[i + 1] = temp;
}
}
}
int main()
{
sort(w, v, N); // 依物品单位重量价值排序
backtrack(1);// 从第一层开始搜索
cout << "最优值(价值总和最大):" << BestValue << endl;
cout << "最优解(放入的物品):";
for (int i = 1; i <= N; i++)
{
if(BestX[i])
cout << "物品" << i << " ";
}
return 0;
}
1.3 测试
2、旅行售货员问题
设有一个售货员从城市1出发,到城市2,3,..,n去推销货物,最后回到城市1。假定任意两个城市i,j间的距离dij(dij=dji)是已知的,问他应沿着什么样的路线走,才能使走过的路线最短。
2.1 分析
算法思路:
1. 读入n和n*n的距离矩阵d[n][n];
2. 用minDis表示最短距离,visited数组(visited[0]表示的是城市1的访问情况)表示每个城市是否已经被访问过;
3. 从城市1开始,使用回溯法进行搜索,对于每个城市,如果该城市未被访问过且与当前城市有边相连,则继续递归搜索下一个城市;
4. 若所有城市都已经被访问到且返回起点是可行路径,则通过minDis = min(minDis, curDis + d[cur][0])更新最短路径长度;
在距离矩阵d中将不连通的城市间的距离设置为-1,在搜索过程中判断当前城市是否与下一个城市有连边。
2.2 代码
#include <cstring>
#include <iostream>
#include <limits.h>
using namespace std;
const int M = 10;
int d[M][M], n, minDis = INT_MAX; // d数组存储各个城市之间的距离,n表示城市个数,minDis表示最短距离(初始化为极大)
int visited[M]; // visited数组记录每个城市是否已经被访问过
void backtrack(int cur, int curDis, int count)
{ // cur表示当前城市编号,curDis表示当前距离,count表示已经访问的城市个数
if (count == n && d[cur][0] != -1)
{ // 所有城市都已经被访问到且返回起点是可行路径
minDis = min(minDis, curDis + d[cur][0]); // 更新最短路径长度
return;
}
if (curDis >= minDis)
{ // 如果当前路径长度已经大于等于当前最小路径长度,剪枝
return;
}
for (int i = 1; i < n; i++)
{ // 从第二个城市开始遍历,因为第一个城市即起点在递归中已经固定
if (visited[i] == 0 && d[cur][i] != -1)
{ // 如果该城市未被访问过且与当前城市有边相连
visited[i] = 1; // 标记该城市已经被访问过
backtrack(i, curDis + d[cur][i], count + 1); // 继续递归搜索
visited[i] = 0; // 回溯
}
}
}
int main()
{
cin >> n;
// 无向图输入距离矩阵
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
cin >> d[i][j];
}
}
memset(visited, 0, sizeof(visited)); // 初始化visited数组
visited[0] = 1; // 起点即城市1已经被访问过
backtrack(0, 0, 1); // 从城市1开始遍历
cout << "最短路径长度:" << minDis << endl;
return 0;
}
/*
输入:
4
0 30 6 4
30 0 5 10
6 5 0 20
4 10 20 0
输出:
25
对于这个测试用例,共有4个城市,距离矩阵如上所示。按照回溯法的思路,从城市1开始遍历,遍历过程如下:
(1) 1->2->3->4->1,路径长度为30+5+20+4=59;
(2) 1->2->4->3->1,路径长度为30+10+20+6=66;
(3) 1->3->2->4->1,路径长度为6+5+10+4=25(最短路径);
(4) 1->3->4->2->1,路径长度为6+20+10+30=66;
(5) 1->4->2->3->1,路径长度为4+10+5+6=25(最短路径);
(6) 1->4->3->2->1,路径长度为4+20+5+30=59;
因此,最短路径为1->3->2->4->1或1->4->2->3->1,路径长度均为25。
*/