7-15 全排列 (10 分)

43 篇文章 37 订阅

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;
}
  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

关迪迪屁事.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值