算法笔记之回溯

总述

回溯是一种经典的搜索算法,通常用于解决组合、排列、子集等问题。

回溯法既不属于动态规划也不属于贪心算法。回溯法是一种搜索算法,它通过不断尝试各种可能的选择,然后回溯(撤销选择)来找到问题的解。回溯算法通常用于求解组合、排列、子集等问题。回溯法是一种通用的搜索算法,适用于一些组合、排列、子集等问题,而不限于最优化问题。

虽然回溯法与动态规划和贪心算法都属于求解最优化问题的算法范畴,但它们有很大的区别:

动态规划:动态规划通常通过保存子问题的解来避免重复计算,具有最优子结构。典型的动态规划问题有斐波那契数列、背包问题等。

贪心算法:贪心算法则通过每一步的局部最优选择来期望达到全局最优解,不进行回溯。典型的贪心算法问题有霍夫曼编码、最小生成树算法等。

回溯法更注重搜索整个解空间,通过深度优先搜索的方式,逐步尝试各种选择,遇到无效选择时回溯撤销选择,直到找到问题的解或遍历完整个解空间。

回溯算法基本思想:

  • 递归: 使用递归实现对解空间的深度优先搜索。
  • 选择: 在每一步根据问题的要求做出选择,尝试不同的可能性。
  • 撤销选择: 在递归完成后,撤销当前选择,进行回溯,继续尝试其他可能性。

关键点和优化:

  1. 剪枝: 在递归的过程中,通过一些条件判断提前终止不符合条件的搜索路径,减少搜索空间,提高效率。
  2. 状态重置: 在递归完成后,需要将当前选择撤销,进行回溯,保持状态的一致性。
  3. 选择列表: 在每一步的递归中,需要考虑当前可以做的选择,通常使用循环遍历选择列表。
  4. 记录路径: 如果需要记录路径或结果,需要使用合适的数据结构进行记录。

典型问题类型:

组合问题: 如组合总和、子集、电话号码的字母组合等。
排列问题: 如全排列、字符串的全排列等。
N 皇后问题: 在 n×n 棋盘上放置 n 个皇后,使其不能相互攻击。

总体思路:

  1. 确定问题的解空间和选择列表。
  2. 编写回溯函数,实现对解空间的深度优先搜索。
  3. 在递归中做出选择、递归到下一层、撤销选择,实现回溯。

LeetCode实战

全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

#include <vector>
class Solution {
public:
	std::vector<std::vector<int>> permute(std::vector<int>& nums) {
		std::vector<std::vector<int>> result;
		if (nums.empty()) {
			return result;
		}

		std::vector<int> current;  // 用于存储当前排列
		std::vector<bool> used(nums.size(), false);  // 记录数字是否被使用过
		backtrack(nums, current, used, result);

		return result;
	}

private:
	void backtrack(const std::vector<int>& nums, std::vector<int>& current,
					std::vector<bool>& used, std::vector<std::vector<int>>& result) {
		// 终止条件:当前排列长度达到数组长度
		if (current.size() == nums.size()) {
			result.push_back(current);  // 将当前排列加入结果集
			return;
		}

		for (int i = 0; i < nums.size(); ++i) {
			if (!used[i]) {
				// 选择当前数字,递归到下一层
				current.push_back(nums[i]);
				used[i] = true;
				backtrack(nums, current, used, result);
				// 撤销选择,进行回溯
				current.pop_back();
				used[i] = false;
			}
		}
	}
};

在这个实现中,used 数组用于记录数字是否被使用过,防止重复选择。回溯函数 backtrack 中,在每一层递归中选择当前数字,递归到下一层,然后撤销选择进行回溯。

电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

#include <vector>
#include <string>

class Solution {
public:
	std::vector<std::string> letterCombinations(std::string digits) {
		std::vector<std::string> result;
		if (digits.empty()) {
			return result;
		}

		std::vector<std::string> mapping = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
		std::string current;
		backtrack(digits, 0, current, mapping, result);

		return result;
	}

private:
	void backtrack(const std::string& digits, int index, std::string& current,
					const std::vector<std::string>& mapping, std::vector<std::string>& result) {
		if (index == digits.size()) {
			// 当递归到字符串末尾时,将当前组合加入结果集
			result.push_back(current);
			return;
		}

		int digit = digits[index] - '0';
		const std::string& letters = mapping[digit];

		for (char letter : letters) {
			// 选择当前字母,递归到下一层
			current.push_back(letter);
			backtrack(digits, index + 1, current, mapping, result);
			// 撤销选择,进行回溯
			current.pop_back();
		}
	}
};

实现中,mapping 数组存储了数字和字母的映射关系。回溯函数 backtrack 通过递归地选择当前字母,构建可能的组合,并在递归完成后撤销选择进行回溯。

组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。

class Solution {
public:
	std::vector<std::vector<int>> combinationSum(std::vector<int>& candidates, int target) {
		std::vector<std::vector<int>> result;
		std::vector<int> current;
		backtrack(candidates, target, 0, current, result);
		return result;
	}

private:
	void backtrack(const std::vector<int>& candidates, int target, int start,
					std::vector<int>& current, std::vector<std::vector<int>>& result) {
		if (target == 0) {
			// 当目标值为0时,将当前组合加入结果集
			result.push_back(current);
			return;
		}

		for (int i = start; i < candidates.size(); ++i) {
			if (target - candidates[i] >= 0) {
				// 选择当前数字,并递归下一层
				current.push_back(candidates[i]);
				backtrack(candidates, target - candidates[i], i, current, result);
				// 撤销选择,进行回溯
				current.pop_back();
			}
		}
	}
};

N 皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

#include <vector>
#include <string>

class Solution {
public:
	std::vector<std::vector<std::string>> solveNQueens(int n) {
		std::vector<std::vector<std::string>> result;
		if (n <= 0) {
			return result;
		}

		std::vector<std::string> board(n, std::string(n, '.'));  // 初始化棋盘
		backtrack(n, 0, board, result);

		return result;
	}

private:
	void backtrack(int n, int row, std::vector<std::string>& board,
					std::vector<std::vector<std::string>>& result) {
		// 终止条件:已经放置完所有皇后
		if (row == n) {
			result.push_back(board);  // 将当前棋盘加入结果集
			return;
		}

		for (int col = 0; col < n; ++col) {
			if (isValid(board, row, col, n)) {
				// 在当前位置放置皇后,递归到下一层
				board[row][col] = 'Q';
				backtrack(n, row + 1, board, result);
				// 撤销选择,进行回溯
				board[row][col] = '.';
			}
		}
	}

	bool isValid(const std::vector<std::string>& board, int row, int col, int n) {
		// 检查同一列是否有皇后
		for (int i = 0; i < row; ++i) {
			if (board[i][col] == 'Q') {
				return false;
			}
		}

		// 检查左上到右下斜线是否有皇后
		for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
			if (board[i][j] == 'Q') {
				return false;
			}
		}

		// 检查左下到右上斜线是否有皇后
		for (int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j) {
			if (board[i][j] == 'Q') {
				return false;
			}
		}

		return true;
	}
};

实现中,使用回溯算法递归地尝试在每一行放置皇后,检查是否满足国际象棋的规则。通过不断递归和回溯,生成所有不同的 N 皇后问题的解决方案。

模板

基本模板:

class Solution {
public:
	// 主函数,入口点
	void solve(/*其他参数*/) {
		// 初始化结果集等必要的数据结构
		// ...

		// 调用回溯函数
		backtrack(/*参数列表*/);

		// 打印结果或其他操作
		printResult();
	}

private:
	// 回溯函数
	void backtrack(/*参数列表*/) {
		// 终止条件
		if (/*满足条件*/) {
			// 处理当前解
			processSolution();
			return;
		}

		// 递归处理每一步的选择
		for (/*每个选择*/) {
			// 做出选择
			makeChoice();

			// 递归到下一层
			backtrack(/*参数列表*/);

			// 撤销选择,进行回溯
			undoChoice();
		}
	}
}
posted @ 2024-01-17 20:59  StuBoo  阅读(124)  评论(0)    收藏  举报