在这里插入图片描述

回溯算法可以理解为一种通过试错的方式来达到问题解决。在解决问题过程中,当它通过尝试发现现有解决方案不行时,它会取消一步甚至是上几步的计算,再通过其他的可能的分步解决方案继续尝试寻找问题的答案回溯算法非常适合用来解决多个步骤组成的问题,其中每个步骤都有多个选项。当我们在某一步选择了其中一个选项时,就进入到了下一步,然后面临新的选择我们就这样重复选择进入下一步的过程,如果发现某一步的选择无论如何都无法达到最终的要求,就退回到上一步,重新选择,这样反复进行,直到找到可能解决方案或所有的选项都试过,但没有办法得到满意的解决方案

回溯算法通常用递归的方式实现,递归是函数自己调用自己的一种方式使用递归可以使问题的解决方案更加简洁明了。在编程实现中,回溯算法可以被划分为三个主要的部分

  1. 路径:它记录了从起点到当前点的路径
  2. 选择列表:它记录了你当前可以做的选择
  3. 结束条件:它判断当前路径是否满足题目的要求,并结束递归。

接下来,我会通过三个经典回溯算法问题来详细说明这个算法的实现

经典问题一:N皇后问题

问题描述

一个N×N的棋盘放置N个皇后,需要满足皇后之间互不攻击条件(即任意两个皇后不能处在同一行、同一列以及同一斜线上)。求解所有可能配置方式。

解题思路

下面是C++实现代码示例代码中将包含详细的中文注释

#include <iostream>
#include <vector>
#include <string>

using namespace std;

// 检查当前放置的皇后和之前的皇后是否冲突
bool isSafe(int row, int col, vector<int&gt; &amp;position) {
    for (int i = 0; i < row; ++i) {
        // 检查冲突和对角线冲突
        if (position[i] == col || abs(row - i) == abs(col - position[i])) {
            return false;
        }
    }
    return true;
}

// 回溯函数,尝试棋盘的每一行放置皇后
void placeQueens(int row, int n, vector<int&gt; &amp;position, vector<vector<string&gt;&gt; &amp;solutions) {
    if (row == n) { // 所有的皇后都放置好了,转换输出格式
        vector<string> board(n, string(n, '.'));
        for (int i = 0; i < n; ++i) {
            board[i][position[i]] = 'Q';
        }
        solutions.push_back(board);
    } else {
        for (int col = 0; col < n; ++col) { // 遍历当前行的每一列
            if (isSafe(row, col, position)) { // 判断是否可以放置皇后
                position[row] = col; // 放置皇后
                placeQueens(row + 1, n, position, solutions); // 递归放置下一行的皇后
                // 回溯部分,撤销当前行的皇后放置
            }
        }
    }
}

vector<vector<string>> solveNQueens(int n) {
    vector<vector<string>> solutions; // 存储所有解决方案
    vector<int> position(n); // 每行皇后的放置位置
    placeQueens(0, n, position, solutions); // 从第一行开始放置皇后
    return solutions;
}

int main() {
    int n = 8; // 可以改变这个值来求解不同大小棋盘
    vector<vector<string>> solutions = solveNQueens(n);
    cout << "There are " << solutions.size() << " solutions to the " << n << "-Queens problem." << endl;
    for (auto &amp;solution : solutions) {
        for (auto &amp;row : solution) {
            cout << row << endl;
        }
        cout << endl;
    }
    return 0;
}

在上面的代码中,我们首先定义了一个辅助函数isSafe,它用于检查在当前行的某一列放置皇后是否会发生冲突然后placeQueens函数用于递归尝试在棋盘上放置皇后,它首先会检查整个棋盘是否已经放满皇后,如果已经放满了,就会生成一个解决方案添加solutions表中。在放置皇后时,如果在某一列上放置皇后不发生冲突,那么就在那一列放置皇后,并递归地放置下一行的皇后。一旦发现这一列不能放置皇后,或者放置了皇后之后,下一行没有合适的位置可以放置皇后,就会进行回溯,撤销当前的放置并尝试下一列。

经典问题二:全排列问题

问题描述:

给定一个不含重复数字数组返回其所有可能的全排列

解题思路:
#include <iostream>
#include <vector>
using namespace std;

void backtrack(vector<int> &amp;nums, vector<vector<int>> &amp;res, vector<int> &amp;track, vector<bool> &amp;used) {
    // 结束条件路径长度等于数组长度,说明已经完成一次排列
    if (track.size() == nums.size()) {
        res.push_back(track);
        return;
    }
    for (int i = 0; i < nums.size(); i++) {
        // 排除不正确的选择,如果数字已经在路径中,则不能再被选择
        if (used[i]) continue;
        // 做选择
        track.push_back(nums[i]);
        used[i] = true;
        // 进入下一层决策树
        backtrack(nums, res, track, used);
        // 取消选择
        track.pop_back();
        used[i] = false;
    }
}

vector<vector<int>> permute(vector<int>&amp; nums) {
    vector<vector<int>> res;
    vector<int> track; // 路径,记录在 track 中的排列顺序
    vector<bool> used(nums.size(), false); // 使用个数标记元素是否在路径中被使用
    backtrack(nums, res, track, used); // 调用回溯函数
    return res; // 返回结果
}

int main() {
    vector<int> nums = {1,2,3};
    vector<vector<int>> res = permute(nums);
    for (auto &amp;perm : res) {
        for (int num : perm) {
            cout << num << " ";
        }
        cout << endl;
    }
    return 0;
}

以上代码首先定义backtrack函数,用来进行回溯搜索排列。在搜索过程中,我们用一个track数组来记录当前的选择路径,用一个used数组标记数组中的元素是否被使用过。当track数组的大小等于输入数组nums大小时,说明已经得到了一个完整的排列,将其添加结果列表res中。在每一次尝试选择数组中的一个数放入路径时,需要检查这个数字之前是否已经被使用过了,如果已经使用过,则跳过,反之则加入到路径中,并标记为已使用,然后递归地进行下一次选择。当一次递归返回时,取消当前的选择,继续尝试其他的数字。

经典问题三:组合总和问题

问题描述:

给定一个无重复元素的数组和一个目标target找出数组中所有可以使数字和为 target组合。数组中的数字可以无限制重复选取

解题思路:
#include <iostream>
#include <vector>
using namespace std;

// 回溯
void backtrack(vector<int>&amp; candidates, int target, vector<int>&amp; track, int start, vector<vector<int>>&amp; res) {
    // 如果路径上数字和已经大于 target,结束递归
    if (target < 0) {
        return;
    }
    // 当组合中数字和等于 target,将它加入结果集中
    if (target == 0) {
        res.push_back(track);
        return;
    }
    // 从 start 开始防止产生重复的组合
    for (int i = start; i < candidates.size(); i++) {
        // 选择当前数字
        track.push_back(candidates[i]);
        // 由于数字可以重复选择,下一轮还从 i 开始
        backtrack(candidates, target - candidates[i], track, i, res);
        // 撤销选择
        track.pop_back();
    }
}

vector<vector<int>> combinationSum(vector<int>&amp; candidates, int target) {
    vector<vector<int>> res;
    vector<int> track;
    backtrack(candidates, target, track, 0, res);
    return res;
}

int main() {
    vector<int> candidates = {2,3,6,7};
    int target = 7;
    vector<vector<int>> res = combinationSum(candidates, target);
    for (const auto &comb : res) {
        for (int num : comb) {
            cout << num << " ";
        }
        cout << endl;
    }
    return 0;
}

上述代码中,backtrack函数采用额外变量start来防止产生重复的组合。我们将所有可能的数字组合搜索一遍,如果当前路径上数字的和大于目标target我们停止递归;如果和等于目标数,则将这个组合保存结果列表中。与全排列的问题不同,由于每个数字可以被无限次选择,我们在下一个递归调用仍然从当前位置开始。

以上介绍的三个问题都是回溯算法问题的典型代表通过这些例子,可以看出回溯算法是一种框架通用的算法,适用于多种问题的求解。它通过在问题的解空间树中,利用深度优先搜索策略尝试每一种可能解法。如果当前走的路径最终不可能到达正确答案,算法就会返回,尝试其他的路径,这就是所谓的“回溯”。尽管回溯算法在最坏情况下的时间复杂度比较高,但通过适当的剪枝优化,通常可以大幅度提高算法的效率,使其成为解决许多组合问题的有力工具

如果你想更深入地了解人工智能的其他方面,比如机器学习、深度学习自然语言处理等等,也可以点击这个链接,我按照如下图所示的学习路线大家整理了100多G的学习资源基本涵盖了人工智能学习的所有内容,包括了目前人工智能领域最新顶会论文合集和丰富详细的项目实战资料,可以帮助你入门进阶

链接人工智能交流群【最新顶会与项目实战】(点击跳转)

在这里插入图片描述

原文地址:https://blog.csdn.net/m0_73916791/article/details/134771820

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_34990.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注