算法设计与分析——回溯算法设计

 前言

NEFU,计算机与控制工程学院,基于C/C++的算法设计与分析课程

实验四 回溯算法设计

环境

操作系统:Windows 10
IDE:Visual Studio Code、Dev C++ 5.11、Code::Blocks

说明

 “实验四 回溯算法设计” 包含以下问题

  1. 0-1背包问题
  2. 旅行售货员问题

其他联系方式:

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。
*/

2.3 测试

  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值