当前位置: 首页 > news >正文

回溯算法(2):全排列问题

回溯算法

回溯算法的本质就是结合递归的深度遍历。基本所有的能用回溯算法解决的问题都分为两个大类:(1)子集问题,(2)全排列问题
上一篇文章我已经详细介绍了子集问题:子集问题
今天我们来学习全排列问题

全排列问题

想解决全排列问题有两个方法,时间复杂度与画出的树的结构都基本一致。我将他们命名为:
(1)元素交换法
(2)状态标注法
其中状态标注法与我们之前做过的子集去重问题有类似之处,且它的适用范围远比元素交换法要大,所以我们这里主要讲解状态标注法

全排列问题的经典描述:
给定一个数列nums=[1, 2, 3];,求它的全排列。
答案应该是有321=6种情况,分别为:

{1, 2, 3}	{2, 1, 3}	{3, 1, 2}
{1, 3, 2}	{2, 3, 1}	{3, 2, 1}

我们还是要画图:
全排列树
我们把上面这个树状图叫做全排列树。被红色框框选的节点就是我们结果的一部分,每个节点中上面表示状态标志state(和子集去重那个一致),下面表示我当前路径下的path内元素。这也是我之所以给大家讲状态标注法的原因,这里的state容器会起到一个前后衔接的作用。
可以看到对于全排列树的每一个节点的子节点从上到下依次减少。这取决于state的状态,我们每一次都是从索引为0开始遍历indx = [0,n-1],n为nums的大小。如果对应索引的state[i] = 0,则表示nums[i]还没有被使用过,则把它加入path。
用语言描述比较抽象,我们来直接看一个最基础的全排列题目:

全排列

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

完整代码:

class Solution {vector<vector<int>> res;	// 答案返回值vector<int> path;			// 节点值vector<bool> state;			// 状态标志void fun(vector<int>& nums) {if(path.size() == nums.size()) {	// 排列已满res.push_back(path);return;}for(int i = 0; i<nums.size(); i++) {if(state[i] == false) {		// 索引为i的元素不在path中path.push_back(nums[i]);state[i] = true;fun(nums);path.pop_back();state[i] = false;}}}
public:vector<vector<int>> permute(vector<int>& nums) {state.resize(nums.size(), false);fun(nums);return res;}
};

可以看到,在fun函数中每次都进行nums中元素的遍历,如果元素被使用过就直接跳过。此外,由于我们要输出全排列,所以nums的元素初始顺序对结果没有任何影响,对于两个不同的数组只要两者元素相同,即使顺序不一致,也不影响输出结果。

全排列去重

全排列2-力扣
题目描述
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
分析
这个其实就是和子集问题一样,我们需要过滤一下重复的结果。首先我们对nums进行排序,刚刚我说过:由于我们要输出全排列,所以nums的元素初始顺序对结果没有任何影响,对于两个不同的数组只要两者元素相同,即使顺序不一致,也不影响输出结果。所以在这里我们先要对nums进行排序,另值相同元素聚集。在重复区间内优选选前面的元素。
去重逻辑代码与子集问题完全一致:

if(i > 0 && nums[i-1] == nums[i] && state[i-1] == false) {continue;
}
else {path.push_back(nums[i]);state[i] = true;fun(nums);path.pop_back();state[i] = false;
}

完整代码

class Solution {vector<vector<int>> res;vector<int> path;vector<bool> state;void fun(vector<int>& nums) {if(path.size() == nums.size()) {res.push_back(path);return;}for(int i = 0; i < nums.size(); i++) {if(state[i] == false) {if(i > 0 && nums[i-1] == nums[i] && state[i-1] == false) {continue;}else {path.push_back(nums[i]);state[i] = true;fun(nums);path.pop_back();state[i] = false;}}}}
public:vector<vector<int>> permuteUnique(vector<int>& nums) {state.resize(nums.size(), false);sort(nums.begin(), nums.end());fun(nums);return res;}
};

练习题

由于有了子集问题的基础,因此全排列问题,大家应该很好理解了,我们直接来两个难题来练习一下。还是那一点,题目翻译能力。

例题1

n皇后-力扣
我们回溯算法的第一个例题就是大名鼎鼎的n皇后问题。
题目描述:
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
在这里插入图片描述
分析:
乍一看这道题输出的格式非常复杂,是vector<vector<string>>,而且需要考虑棋子的位置绘制棋盘。如果直接按照这个格式去做,就有点太麻烦了,我们要花大量时间去写代码,而不是研究这个算法。实际考试中也是不可取的。我们需要一个简化的表示方案,在最后的时候再讲我们的简化方案转化为题目要求的格式。
对于n皇后问题,我们只需要知道每个棋子在图像上的具体坐标即可。对于每个摆放方式,我们只需要表示n个坐标即可。因此我们最先想到的是键值对pair<int, int>。有n个棋子就要n个键值对,暨path = vector<pair<int,int>>。可能·有多种方案,因此我们自定义的返回值应该是一个res = vector<vector<pair<int, int>>>
看到这里大家可能会吐槽,这一点也不简单,我也这么觉得,实际上我们可以进一步简化我们的格式。
当前我们使用vector<pair<int,int>>来表示n个坐标,但是我们忽略了一点,就是由于N皇后问题的特殊性,实际上我们不可能出现两个棋子在同一轴的情况。因此对于n皇后,如果我尝试使用path = vector<int>来表示n个棋子的坐标,因为两个棋子不可能在同一轴,我们另可以用(indx, path[indx])来表示第indx个棋子的位置。如图:
在这里插入图片描述

综上所述,我们需要设置的变量格式为:

vector<vector<int>> res;
vector<int> path;
vector<bool> state;

还是我们的三个老朋友。
我们还需要几个接口来模块化的完成工作:

bool valid(vector<int> path);			// 判断当前棋盘的上棋子坐标(path)是否合格
void fun(vector<int>& nums, int n);		// 主要函数,用于生成全排列
void out_put(int n);					// 将我们的输出格式转换

valid:
vaild接口用于判断当前棋盘的上棋子坐标(path)是否合格,每当我新放入一个棋子暨path.push_back(i),就调用valid接口检测一下。加上我放入第k个棋子,我就检查第k个棋子与前k-1个棋子是否发生冲突。由于前k-1个棋子每次放入时都会检查一次,因此我们可以确定前k-1个棋子是合格的,只要检查新放入的第k个棋子即可。

bool valid(vector<int>& path) {		// path为当前棋子放置方案if(path.empty()) return true;int newch = path.size()-1;for(int i = 0; i < newch; i++) {if(newch == i || path[newch] == path[i]) {	// 防止出现在同一行,其实不可能出现这种情况,一会说return false;}else if(abs(i-newch) == abs(path[i]-path[newch])) {		// 防止在同一对角线return false;}}return true;
}

out_put:
out_put接口用于进行输出格式转换,,先生成一张空棋盘(全是 ‘.’),然后把res中的path取出来,每个path中都有n个棋子的坐标,把对应坐标上的元素变成‘Q’即可。

vector<vector<string>> out_put(int n) {		// 输入参数n为棋盘的维度,也对应棋子的个数vector<vector<string>> ret;for(auto vec: res) {vector<string> map(n, string(n,'.'));	// 生成一个空棋盘for(int i=0; i<n; i++) {map[i][vec[i]] = 'Q';				// 把棋盘上对应的位置放入棋子}ret.push_back(map);}return ret;
} 

fun:
生成棋子的全排列

void fun(vector<int>& nums, int n) {if(!valid(path)) return;if(path.size() == n) {res.push_back(path);return;}for(int i = 0; i<n; i++) {if(state[i] == false) {path.push_back(nums[i]);state[i] = true;fun(nums, n);path.pop_back();state[i] = false;}}
}

N皇后是如何与全排列联系到一起的呢?
看下面的代码,

vector<vector<string>> solveNQueens(int n) {vector<int> nums;state.resize(n, false);for(int i = 0; i<n; i++) {nums.push_back(i);	// 假设n = 4, nums = [0, 1, 2, 3]; 棋子在同一对角线}fun(nums, n);return out_put(n);
}

首先我们将n个棋子都放在对角线上:在这里插入图片描述
由于每个元素的值都不相同,因此不可能出现棋子在同一行或者同一列的情况。
当我们生成一个排列后,棋盘变成:
在这里插入图片描述
因为索引是固定的,所以每个棋子只能在对应的列上运动,且由于使用全排列的形式,因此不可能出现棋子出现在同一行的情况。因此哦们只要找到全排列,就可以找到所有满足条件的摆放方式。

完整代码:

class Solution {vector<vector<int>> res;vector<int> path;vector<bool> state;bool valid(vector<int>& path) {if(path.empty()) return true;int newch = path.size()-1;for(int i = 0; i < newch; i++) {if(newch == i || path[newch] == path[i]) {return false;}else if(abs(i-newch) == abs(path[i]-path[newch])) {return false;}}return true;}vector<vector<string>> out_put(int n) {vector<vector<string>> ret;for(auto vec: res) {vector<string> map(n, string(n,'.'));for(int i=0; i<n; i++) {map[i][vec[i]] = 'Q';}ret.push_back(map);}return ret;} void fun(vector<int>& nums, int n) {if(!valid(path)) return;if(path.size() == n) {res.push_back(path);return;}for(int i = 0; i<n; i++) {if(state[i] == false) {path.push_back(nums[i]);state[i] = true;fun(nums, n);path.pop_back();state[i] = false;}}}
public:vector<vector<string>> solveNQueens(int n) {vector<int> nums;state.resize(n, false);for(int i = 0; i<n; i++) {nums.push_back(i);}fun(nums, n);return out_put(n);}
};

这个就是N皇后问题的解决方法了,这个题其实还是很有难度的,力扣上显示是困难。但是实际上如果大家思路清晰,总体还是比较好解决的。

回溯算法总结

回溯算法用于解决子集问题与全排列问题,当题目中出现“全部”,“方案”,“组合”,“序列”等字眼且要求你给出具体每个方案的输出时,就可以考虑使用回溯算法。
回溯算法有些类似于二叉树的前中后序遍历,是一个深度优先搜索的过程,它会沿着子集树或者全排列树的一条路径走到末尾,并返回,大家要理解这一点。
回溯算法中(1)什么时候终止回溯 以及(2)什么时候把path放入res是最重要的,知晓这个才能更好的,更灵活的运用回溯算法解决问题。
对于实际考试中,大家需要具有题目翻译能力,从题目的故事中将需求抽象出来。
大家加油!

结束语

回溯算法还有一些内容,为了不影响大家的知识体系,我放在了番外篇中,大家酌情观看。
传送阵:回溯算法(3):番外篇

相关文章:

  • Serving入门
  • Java 动态代理实现
  • webgl入门实例-向量在图形学中的核心作用
  • 【每日八股】复习计算机网络 Day2:TCP 断开连接时四次挥手及其相关问题
  • [Java实战经验]异常处理最佳实践
  • opencv--图像处理
  • Vue3 + TypeScript中defineEmits 类型定义解析
  • LeetCode 5:最长回文子串
  • 【java实现+4种变体完整例子】排序算法中【冒泡排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
  • AI写代码工具分享:Cursor 高效使用攻略与实战秘籍
  • 【图片识别分类】如何快速识别照片中的水印文字,对图片进行关键字分类,快速整理水印相机拍摄图片,基于WPF和腾讯OCR的技术实现
  • QML中的3D功能--自定义着色器开发
  • 实现Azure Synapse Analytics安全地请求企业内部API返回数据
  • Flink框架十大应用场景
  • 嵌入式软件--stm32 DAY 2
  • 为什么浮点数会搞出Infinity和NAN两种类型?浮点数的底层原理?IEEE 754标准揭秘?
  • VSCode安装与环境配置(Mac环境)
  • 【计算机视觉】CV实战项目- Face-and-Emotion-Recognition 人脸情绪识别
  • sqlilabs-Less11 POST注入
  • 一个项目中多个Composer的使用方法
  • 马上评|机器人马拉松,也是具身智能产业的加速跑
  • 守护体面的保洁员,何时能获得体面?|离题
  • 江西一季度GDP为7927.1亿元,同比增长5.7%
  • 江西修水警方:一民房内发生刑案,犯罪嫌疑人已被抓获
  • 俄最高法宣布解除针对阿富汗塔利班的禁令
  • 天津一季度GDP为4188.09亿元,同比增长5.8%