算法分析与设计——实验4:回溯法

实验四  回溯法

一、实验目的

        1、理解回溯法的概念;

        2、掌握回溯法的基本要素;

        3、掌握回溯法的解题步骤与算法框架;

        4、通过应用范例学习回溯法的设计技巧与策略。

二、实验内容和要求

实验要求:通过上机实验进行算法实现,保存和打印出程序的运行结果,并结合程序进行分析,上交实验报告和程序文件。

实验内容

        1、实现旅行售货员问题的回溯算法:某售货员要到若干城市去推销商品,已知各城市之间的路线(或旅费)。要选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使总的路程(或总旅费)最小。

        2、使用回溯算法解决0-1背包问题。

        3、使用回溯算法解决迷宫问题:以一个M×N的长方阵表示迷宫,0和1分别表示迷宫中的通路和障碍。设计一个程序,对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。

        (1)根据二维数组,输出迷宫的图形。

        (2)探索迷宫的四个方向:RIGHT为向右,DOWN向下,LEFT向左,UP向上,输出从入口到出口的行走路径。

运行示例:

请输入迷宫的行数

9

请输入迷宫的列数

8

请输入9行8列的迷宫

0 0 1 0 0 0 1 0

0 0 1 0 0 0 1 0

0 0 1 0 1 1 0 1

0 1 1 1 0 0 1 0

0 0 0 1 0 0 0 0

0 1 0 0 0 1 0 1

0 1 1 1 1 0 0 1

1 1 0 0 0 1 0 1

1 1 0 0 0 0 0 0

有路径

路径以下:

三、算法思想分析

一)回溯法

1. 基本思想

回溯法是一种“能进则进,进不了则换,换不了就退”的基本搜索方法。

首先需要为问题定义一个解空间,这个解空间必须至少包含问题的一个解(可能是最优的)。在问题的解空间树中,按深度优先策略,从根节点出发搜索解空间树。算法搜索至解空间树的任一结点时,先判断该结点是否包含问题的解。如果不包含,则跳过该结点为根的字数,逐层对其他祖先结点回溯。否则,进入该子树,继续按照深度优先策略搜索。

2. 相关概念

解空间:对问题的一个实例,所有满足显性约束条件的多元解向量组成了该实例的一个解空间。一方面有助于快速找到问题解,另一方面可以防止遗漏部分可行解。回溯法的解空间可以组织成一棵树,通常有两类典型的解空间树:子集树和排列树。

子集树:当所给的问题是从n个物体r集合中找出满足某种性质的子集时,相应的解空间树成为子集树。每增加一个新元素,都使子集个数加倍,因此对于n个元素有2n个子集。

排序树:当所给的问题是从n个元素的集合中找出满足某种性质的排列时,相应的解空间树成为排序树。对于n个元素,得到不同排列的总数为n!,即排序树中至少有n!个叶节点,因此任何算法遍历排列树所需运行时间为O(n!)

3. 基本步骤

    运用回溯法解题通常包含以下三个步骤:

    1)针对所给问题,定义问题的解空间;

    2)确定易于搜索的解空间结构;

    3)以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索;

其中,通过深度优先搜索思想完成回溯的完整过程如下:

  • 设置初始化的方案;
  • 变换方式去试探,若全部试完则转到⑦;
  • 判断此法是否成功(通过约束函数),不成功则转②;
  • 试探成功则前进一步再试探;
  • 正确方案还未找到则转②;
  • 已找到一种方案则记录并打印;
  • 退回一步(回溯),若未退到头则转②;
  • 已退到头则结束或打印无解。

(二)实验内容一

旅行售货员问题是寻求单一旅行者由起点出发,通过所有给定的需求点之后,最后再回到原点的最短路径(或旅费)。

1. 解空间

各城市间的路线是一个带权图。其中,权为各路线的长度(或旅费),端点为城市。旅行售货员的一条周游路线,就是连接图中所有顶点的一条回路。周游路线的路程(或费用)是这条回路上所有边的权值之和。

旅行售货员问题的解空间是一棵排序树。从树的根结点到任一叶子结点的路径就是一条周游路线,我们只需找出在图G中路程(或费用)最小的周游路线即可。

假设起点为1。算法开始时 x = [1, 2, 3, …, n]。x[1 : n]有两重含义 x[1 : i]代表前 i 步按顺序走过的城市, x[i + 1 : n]代表还未经过的城市。利用Swap函数进行交换位置。

2. 约束条件
  1. 路径中相邻城市之间有路径相连。即当搜索的层次i < n 时,判断从x[i - 1]到x[i] 之间是否存在一条边,若存在则x [1 : i ] 构成了图G的一条路径。
  2. 最后一个城市与第一个城市有路径相连。即当前搜索的层次i = n(处在排列树的叶节点的父节点上)时,判断此时图G是否存在从顶点x[n-1] 到顶点x[n] 的一条边,和从顶点x[n] 到顶点x[1] 的一条边。若两条边都存在,则求得一个旅行售货员的回路,继续判断这条回路的路程(或费用)是否优于已经找到的当前最优回路。若是,则更新当前最优值minRoad和当前最优解bestX
3. 限界条件

若路径x[1: i] 走过的路径长度(或费用)小于当前最优解,则算法进入排列树下一层;否则没有必要继续搜索,剪掉相应的子树。

(三)实验内容二

0-1背包问题,给定一个容量为C的背包,n个物品,物品的体积为Ci,价值为Vi,1<=i<=n,要求使装入背包内的物品价值总量最大。

1. 解空间

解空间为用户输入的N个物品的子集,是一棵子集树。

2. 约束条件

规定放入背包的物品体积之和小于等于背包容量。

3. 限界条件

为了更好地计算上界,首先对剩余物品按单位价值进行从大到小的排序,先装价值大的,后装价值小的,直到不能装下时,再装入该物品的一部分,从而填满背包,获得上界。

在我的代码实现中,在完成输入背包的物品数量和容量以及的物品体积和价值后,通过sort()方法对物品进行价值排序,利用Backtrack()方法对子集树从根节点开始,采用深度优先算法遍历树,同时每一次遍历调用 Bound()限界函数判断是否剪枝

(四)实验内容三

用一个二维数组来定义迷宫的初始状态1表示为墙不能走,0表示路可以走。而后入口(左上角)开始顺着某一个方向前进,若能走通则继续往前进;否则沿着原路退回,换一个方向继续探索,直至出口位置,求得一条通路。假如所有可能的通路都探索到而未能到达出口,则所设定的迷宫没有通路。

四、程序代码

一)实验内容一

#include <bits/stdc++.h>
using namespace std;

const int max_ = 0x3f3f3f; //定义一个最大值
int cityNum;               //城市数
int edgeNum;               //边数
int nowRoad;               //记录当前的路程
int minRoad;               //记录最小的路程(最优)
int Graph[100][100];       //图的边距记录
int x[100];                //记录行走顺序
int bestX[100];            //记录最优行走顺序

void InPut() {
    cout << "请输入城市数量和道路数量:" << endl;
    cin >> cityNum >> edgeNum;

    memset(Graph, -1 , sizeof(Graph));   //初始化所有城市间没有道路

    cout << "请输入两座城市之间的距离(格式:城市 城市 距离):" << endl;
    int pos1, pos2, len;
    for(int i = 1; i <= edgeNum; ++i) {
        cin >> pos1 >> pos2 >> len;
        Graph[pos1][pos2] = Graph[pos2][pos1] = len;
    }
}

//初始化行走路线
void Init() {
    nowRoad = 0;
    minRoad = max_;
    for(int i = 1; i <= cityNum; ++i) {
        x[i] = i;
    }
}

void Swap(int &a, int &b) {
    int temp;
    temp = a;
    a = b;
    b = temp;
}

//计算第i步去的城市
void BackTrack(int i) {
    //判断到最后一个城市
    if(i == cityNum) {
        if(Graph[x[i - 1]][x[i]] != -1 && Graph[x[i]][x[1]] != -1
            && (nowRoad + Graph[x[i - 1]][x[i]] + Graph[x[i]][x[1]] < minRoad || minRoad == max_)) {
            //最小距离 = 当前的距离 + 当前城市到叶子城市的距离 + 叶子城市到初始城市的距离
            minRoad = nowRoad + Graph[x[i - 1]][x[i]] + Graph[x[i]][x[1]];
            for(int j = 1; j <= cityNum; ++j) bestX[j] = x[j];
        }
    }
    else {
        for(int j = i; j <= cityNum; ++j) {
            if(Graph[x[i - 1]][x[j]] != -1 && (nowRoad + Graph[x[i - 1]][x[j]] < minRoad || minRoad == max_)) {
                Swap(x[i], x[j]);
                nowRoad += Graph[x[i - 1]][x[i]];
                BackTrack(i + 1);   //递归判断下一步
                nowRoad -= Graph[x[i - 1]][x[i]];
                Swap(x[i], x[j]);
            }
        }
    }
}

void OutPut() {
    cout << "最短路程为:" << minRoad << endl;
    cout << "具体路线为:" ;
    for(int i = 1; i <= cityNum; ++i) cout << bestX[i] << "——> ";
    cout << "1" << endl;
}


int main() {
    InPut();
    Init();
    BackTrack(2);
    OutPut();
}

(二)实验内容二

#include <iostream>
using namespace std;

int n;              //物品数量
int bagC;           //背包容量
double c[1000];     //物品体积
double v[1000];     //物品价值
double vc[1000];    //物品单位重量价值
int order[1000];    //物品编号1~n
int bestX[1000];    //记录回溯过程的最优情况
int flag[1000];     //记录当前物品是否装入背包
int maxV = 0;       //最大价值
int nowC = 0;       //当前体积
int nowV = 0;       //当前价值

void InPut() {
    cout << "请输入物品数量:";
    cin >> n;
    cout << "请输入背包容量:";
    cin >> bagC;
    cout << "请输入所有物品重量:" << endl;
    for(int i=1; i<=n; ++i) cin >> c[i];
    cout << "请输入所有物品价值:" << endl;
    for(int i=1; i<=n; ++i) cin >> v[i];
    for(int i=1; i<=n; ++i) order[i] = i;
}

void sort() {
    for (int i = 1; i <= n; ++i) vc[i] = v[i] / c[i];    //单位重量价值
    //将vc[]从大到小排序,并对应改变order[]、v[]、c[]
    for (int i = 1; i <= n - 1; i++) {
        for (int j = i + 1; j <= n; j++) {
            if (vc[i] < vc[j]) {
                {
                    double temp;
                    temp = vc[i];
                    vc[i] = vc[i];
                    vc[j] = temp;

                    temp = order[i];
                    order[i] = order[j];
                    order[j] = temp;

                    temp = v[i];
                    v[i] = v[j];
                    v[j] = temp;

                    temp = c[i];
                    c[i] = c[j];
                    c[j] = temp;
                }
            }
        }
    }
}

//限界函数:该函数返回装入所有剩余物品后(不能超过c的前提下)的价值
int Bound(int i) {
    int cleft = bagC - nowC;   //剩余容量
    int value = nowV;
    while(i <= n && c[i] <= cleft) {
        cleft -= c[i];
        value += v[i];
        ++i;
    }
    if(i<=n) value += v[i] * cleft / c[i];
    return value;
}

void Backtrack(int i) {
    if(i > n) {
    //到达根节点且根节点处理完毕
    	for(int i = 1; i <= n; i++) {
            bestX[i] = flag[i];   //记录回溯的最优情况
        }
        maxV=nowV;
    } else {
        if(nowC + c[i] <= bagC) {
            //满足约束条件进入左子树,物品放入
            flag[i] = 1;
            nowC += c[i];
            nowV += v[i];
            Backtrack(i+1);
            nowC -= c[i];
            nowV -= v[i];
        }
        if(Bound(i+1) > maxV) {
            //满足限界函数进入右子树,物品不装入
            flag[i] = 0;
            Backtrack(i+1);
        }
    }
}

void OutPut() {
    cout<<"背包可装入的最大价值为:"<<maxV<<endl;
    cout<<"放入背包的物品编号为:" ;
    for(int i=1; i<=n; ++i)
        if(bestX[i] == 1) cout << order[i] << " ";
}

int main() {
    InPut();
    sort();
    Backtrack(1);   //从根节点开始回溯
    OutPut();
    return 0;
}

(三)实验内容三

package migong;
import java.util.*;

class Position{
    int row;    //行数
    int col;    //列数

    public Position() {}
    public Position(int row, int col){
        this.col = col;
        this.row = row;
    }
    public String toString(){
        return "(" + row + " ," + col + ")";
    }
}

class Maze{
    int maze[][];
    private int row;
    private int col;
    Stack<Position> stack;
    boolean p[][] = null;

    public Maze(){
        maze = new int[15][15];
        stack = new Stack<Position>();
        p = new boolean[15][15];
    }

    //构造迷宫
    public void init(){
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.println("请输入迷宫的行数:");
            row = scanner.nextInt();
            System.out.println("请输入迷宫的列数:");
            col = scanner.nextInt();
            System.out.println("请输入" + row + "行" + col + "列的迷宫:");
            for(int i = 0; i < row; ++i) {
                for(int j = 0; j < col; ++j) {
                    int temp = scanner.nextInt();
                    maze[i][j] = temp;
                    p[i][j] = false;
                }
            }
        }
    }

   //回溯迷宫,查看是否有出路
    public void findPath(){
        //给原始迷宫周围加一圈围墙
        int temp[][] = new int[row + 2][col + 2];
        for(int i = 0; i < row + 2; ++i) {
            for(int j = 0; j < col + 2; ++j) {
                temp[0][j] = 1;
                temp[row + 1][j] = 1;
                temp[i][0] = temp[i][col + 1] = 1;
            }
        }
        //将原始迷宫复制到新迷宫中
        for(int i = 0; i < row; ++i) {
            for(int j = 0; j < col; ++j) {
                temp[i + 1][j + 1] = maze[i][j];
            }
        }
        //从左上角开始按照顺时针开始查询
        int i = 1;
        int j = 1;
        p[i][j] = true;
        stack.push(new Position(i, j));
        while (!stack.empty() && (!(i == (row) && (j == col)))) {
            if ((temp[i][j + 1] == 0) && (p[i][j + 1] == false)) {
                p[i][j + 1] = true;
                stack.push(new Position(i, j + 1));
                j++;
            } else if ((temp[i + 1][j] == 0) && (p[i + 1][j] == false)) {
                p[i + 1][j] = true;
                stack.push(new Position(i + 1, j));
                i++;
            } else if ((temp[i][j - 1] == 0) && (p[i][j - 1] == false)) {
                p[i][j - 1] = true;
                stack.push(new Position(i, j - 1));
                j--;
            } else if ((temp[i - 1][j] == 0) && (p[i - 1][j] == false)) {
                p[i - 1][j] = true;
                stack.push(new Position(i - 1, j));
                i--;
            } else {
                stack.pop();
                if(stack.empty()) break;
                i = stack.peek().row;
                j = stack.peek().col;
            }
        }

        Stack<Position> newPos = new Stack<Position>();
        if (stack.empty()) {
            System.out.println("没有路径");
        } else {
            System.out.println("有路径");
            System.out.println("路径如下:");
            while (!stack.empty()) {
                Position pos = new Position();
                pos = stack.pop();
                newPos.push(pos);
            }
        }

        //图形化输出路径
        String resault[][]=new String[row+1][col+1];
        for(int k=0;k<row;++k){
            for(int t=0;t<col;++t){
                resault[k][t]=(maze[k][t])+"";
            }
        }
        while (!newPos.empty()) {
            Position p1=newPos.pop();
            resault[p1.row-1][p1.col-1]="#";
        }
        for(int k=0;k<row;++k){
            for(int t=0;t<col;++t){
                System.out.print(resault[k][t]+"\t");
            }
            System.out.println();
        }
    }
}

class miGong {
    public static void main(String[] args){
        Maze demo = new Maze();
        demo.init();
        demo.findPath();
    }
}

五、结果运行与分析

一)实验内容一

由于解是图顶点的一个排序,第1个顶点已经确定,因而一共有O((n-1)!)种可能,每次更新需要O(n)来更新bestX的值,因此总的时间复杂度为O(n!)

(二)实验内容二

回溯算法的运行时间取决于它在搜索过程中所生成的结点数,而限界函数可以大大减少生成结点的个数,避免无效搜索,加快搜索速度,由于解空间的子集树中叶子结点的数目为2^n,调用限界函数计算上界需要O(n)时间,在最坏情况下有O(2^n)个有儿子结点需要调用限界函数,因此总的时间复杂度为O(n2^n)。

三)实验内容三

 

六、心得与体会

本次是算法分析与设计的第四次实验,主要是应用回溯法分别解决旅行售货员问题、0-1背包问题和迷宫问题。

回溯算法有一种不撞南墙不回头的感觉。其基本思想就是按选优条件向前搜索,去尝试所有的可能性,以达到目标。但当探索到某一步发现原先选择并不优或达不到目标时,就退回一步重新选择。它强调了深度优先遍历思想的用途,用一个不断变化的变量,在尝试各种可能的过程中,搜索需要的结果,强调回退操作对于搜索的合理性。

我觉得该算法的难在在于空间解的求解,因此我们可以在做题前尝试画树去帮助理清思路。对于旅行售货员问题,它是典型的TSP问题,通过上文中的分析其空间解是一棵排列树。对于0-1背包问题,我们已经在前几次实验多次用其他算法进行求解,问题背景不再重述,其空间解显然是一个子集树。对于迷宫问题,按照上右下左顺序判断走还是不走,空间解为排列树。

对于排列树和子集树,我们遍历其每个结点的遍历函数写法也有所不同。

对于排列树:

void backtrack(int t){
    if(t>=n){ 
           output();   //此时已到达叶子结点,用于输出一个结果
           return;
    }
    //从第t个单元开始进行交换排列  
    for(int i=t;i<n;i++){
        swap(x[t],x[i]);  //交换两个排列顺序,得到了一个解
        if(ok(t))  //ok是判断目前的一个解有可行的机会 
            backtrack(t+1);  
        swap(x[t],x[i]);  //恢复原样,给后面的好继续交换判断
    }
}

对于子集树:

void backtrack(int t){
    if(t>=n){ 
           output();   //此时已到达叶子结点,用于输出一个结果
           return;
    }
    //对分叉点遍历0,1两种情况    
    for(int i=0;i<=1;i++){
        x[t]=i;  //给第t层代表的选择赋可能选择的值
        if(ok(t))  //ok是判断目前的一个解有可行的机会 
            backtrack(t+1);  
    }
}

总结比较,排列树的遍历公式中,for循环中的i是所有可能取值的存放位置;在子集树的遍历公式中,for循环中的i是可能的取值。为了提高算法的效率,我们可以在判断目前的一个解是否可行的ok()函数中加入对不可行解的剪枝操作。

总之,通过本次三个实验内容的分析与实现,加深了我对回溯法概念和基本要素的理解,实现了利用回溯法进行解题。

  • 12
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阮阮的阮阮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值