7-15 全排列 (10 分)
对于1~n这n个不同的数,按照一定的顺序把这n个数排列起来(每个数出现一次,且不重复, n<10),将所有的排列列出,称为全排列。
输入格式:
一个数n。
输出格式:
1~n的全排列,每个排列一行(按字典序输出)。
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
思路:在之前我写过用动态规划的原理解决全排列问题 想看动态规划的可以点这动态规划解决全排列
下面我主要讲的是用递归回溯来解决全排列问题 以他为模板解决一系列问题
概念
先来讲解一下什么是回溯法 回溯(backtracking)法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
回溯法是一个既带有系统性又带有跳跃性的的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题.
回溯法中,首先需要明确下面三个概念:
-
约束函数:约束函数是根据题意定出的。通过描述合法解的一般特征用于去除不合法的解,从而避免继续搜索出这个不合法解的剩余部分。因此,约束函数是对于任何状态空间树上的节点都有效、等价的。
-
状态空间树:一个问题的解可以表示成解向量X = (x1, x2, …, xn),X中每个分量xi所有取值的组合构成问题的解向量空间,简称解空间或者解空间树,又称为状态空间树,是一个对所有解的图形描述。树上的每个子节点的解都只有一个部分与父节点不同。
-
扩展节点、活结点、死结点:所谓扩展节点,就是当前正在求出它的子节点的节点,在DFS中,只允许有一个扩展节点。活结点就是通过与约束函数的对照,节点本身和其父节点均满足约束函数要求的节点;死结点反之。由此很容易知道死结点是不必求出其子节点的(没有意义)。
-
由于采用回溯法求解时存在退回到祖先结点的过程,所以需要保存搜索过的结点。通常采用:
- 定义栈来保存
- 采用递归方法(主要讲这种)
-
用回溯法通常采用两种策略(均称为剪枝函数)避免无效搜索。
- 用约束函数在扩展结点处剪除不满足约束条件的路径
- 用限界函数剪去得不到问题的解或最优解的路径
下面主要讲解采用递归方法的回溯通用代码
void Backtrack(int t){
if(t>n){
Output(x);
}
else{
for(int i=f(n,t);i<=g(n,t);i++){
x[t]=h[i];
if(Constraint(t)&&Bound(t)){
Backtrack(t+1);
}
}
}
}
其中,形式参数t表示递归深度(可以理解为树的层数),即当前拓展节点在解空间树中的深度。n用来控制递归深度,当t>n时,表示算法已经搜索到叶节点(简单点说就是一个解已经被找出来了)。此时Output(x)记录或输出得到的可行解x。
for循环中的f(n,t)和g(n,t)分别表示在当前拓展节点处为搜索过的子树的起始编号和终止编号。
h(i)表示在当前拓展节点处x[t]的第i个可选值。
Constraint(t)为约束函数,返回值为true 或者false 用于判断取值是否合法,当不合法时,直接剪去其子树。
Bound(t)为界限函数,需要在一定区间内进行运算。
当确定当前取值合法时,我们还需要去他的下一层判断即调用Backtrack(t+1)。
回溯法是从根节点出发然后最后又回到了根节点,所以调用回溯法只需要调用一次backtrack(1)即可。
下面我将详细分析全排列要求下的相应函数
Output(x)函数
Output函数主要看题目的要求 这道题是要你输出全排列 那就一个一个输出就好了
int Output (int t){
for (int i=1;i<=n;i++){
cout<<x[i]<<" ";
}
cout<<endl;
return 0;
}
Constraint(t)函数或者我喜欢用judge(t)函数
这两个函数的作用是判断该节点是否合理 那么判断合理不合理的条件就是看当前节点之前有没有选过
或者你要是不喜欢写bool类型的函数 你可以自己定义一个vist数组用来标记当前节点之前是否被访问过 但是要记得在t+1被调用前把他给标记回未被访问过 不然会出现没有节点可访问的情况
bool judge (int t){
for (int i=1;i<t;i++){
if(x[i]==x[t]){//当前节点被保存在x[t]里 只要比较前t-1个x数组里面有没有跟x[t]值相等的就行了
return false;//出现过就表示当前节点不合法 那就得输出false
}
}//否则就表示当前节点合法
return true;
}
完整代码
#include <bits/stdc++.h>
using namespace std;
int x[100],n;
bool judge (int t){
for (int i=1;i<t;i++){
if(x[i]==x[t]){
return false;
}
}
return true;
}
int Output (int t){
for (int i=1;i<=n;i++){
cout<<x[i]<<" ";
}
cout<<endl;
return 0;
}
void Backtrack(int t){
int i;
if(t>n){
Output(t);
}
else {
for (i=1;i<=n;i++){
x[t]=i;
if(judge(t)){
Backtrack(t+1);
}
}
}
}
int main(){
cin>>n;
Backtrack(1);
return 0;
}
下面是不喜欢写函数的代码
#include <bits/stdc++.h>
using namespace std;
int x[100],vist[100],n;
void Backtrack(int t){
int i;
if(t>n){
for (i=1;i<=n;i++){
cout<<x[i]<<" ";
}
cout<<endl;
return ;
}
else {
for (i=1;i<=n;i++){
x[t]=i;
if(vist[i]==0){//0表示未被访问过 1表示访问过
vist[i]=1;
Backtrack(t+1);
vist[i]=0;//黄线重点部分 下一个调用的时候要把值改回来
}
}
}
}
int main(){
cin>>n;
Backtrack(1);
return 0;
}
有些题目可能还涉及到要你输出到底结果有几个 那么你就需要先想 我肯定是需要一个全局变量cnt来记录我一共找到了多少个解 但是 我要在哪一部分给他cnt++呢 毫无疑问 当然是在你找到一个解的时候给他cnt++啦 根据递归回溯的思想 每找到一个解 我就需要调用Output函数来给结果进行输出 所以 我们只要在调用Output函数之前或之后 给cnt++就行了 部分代码如下
int Output (int t){//这三处任选一处都可以 只要不要再for循环里 并且在return之前就行
//cnt++ 第一处
for (int i=1;i<=n;i++){
cout<<x[i]<<" ";
}
//cnt++ 第二处
cout<<endl;
//cnt++ 第三处
return 0;
}
完整代码如下
#include <bits/stdc++.h>
using namespace std;
int x[100],n,cnt;
bool judge (int t){
for (int i=1;i<t;i++){
if(x[i]==x[t]){
return false;
}
}
return true;
}
int Output (int t){
for (int i=1;i<=n;i++){
cout<<x[i]<<" ";
}
cnt++;
cout<<endl;
return 0;
}
void Backtrack(int t){
int i;
if(t>n){
Output(t);
}
else {
for (i=1;i<=n;i++){
x[t]=i;
if(judge(t)){
Backtrack(t+1);
}
}
}
}
int main(){
cin>>n;
Backtrack(1);
cout<<cnt<<endl;
return 0;
}