BUAA(2021春)全排列数的生成——DFS(深度优先遍历)+回溯 秒杀本题

48 篇文章 154 订阅

看前须知

要点介绍和简要声明.

第一次上机题汇总

扩展字符A——strchr的灵活使用.

表达式求值.

小数形式与科学计数法转换(简)——分类讨论一定要有逻辑.

超长正整数的减法(高精度减法)+其他三种高精度运算.

全排列数的生成——DFS(深度优先遍历)+回溯 秒杀本题.

题目内容

问题描述

输入整数N( 1 <= N <= 10 ),生成从1~N所有整数的全排列。

输入形式

输入整数N。

输出形式

输出有N!行,每行都是从1~N所有整数的一个全排列,各整数之间以空格分隔。各行上的全排列不重复。输出各行遵循“小数优先”原则, 在各全排列中,较小的数尽量靠前输出。如果将每行上的输出看成一个数字,则所有输出构成升序数列。具体格式见输出样例。

样例

【样例输入1】1
【样例输出1】1
【样例说明1】输入整数N=1,其全排列只有一种。
【样例输入2】3 
【样例输出2】
	1 2 3
	1 3 2
	2 1 3
	2 3 1
	3 1 2
	3 2 1

【样例说明2】输入整数N=3,要求整数1、2、3的所有全排列, 共有N!=6行。且先输出1开头的所有排列数,再输出2开头的所有排列数,最后输出3开头的所有排列数。在以1开头的所有全排列中同样遵循此原则。
【样例输入3】10
【样例输出3】
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 10 9
1 2 3 4 5 6 7 9 8 10
1 2 3 4 5 6 7 9 10 8
1 2 3 4 5 6 7 10 8 9
1 2 3 4 5 6 7 10 9 8
1 2 3 4 5 6 8 7 9 10
1 2 3 4 5 6 8 7 10 9
1 2 3 4 5 6 8 9 7 10
1 2 3 4 5 6 8 9 10 7

题解

思路分析

这道题一眼就可以看出来是在考察递归,但是如何实现递归往往令人头秃,要么是根本想不到,要么就是想到了但是无法用C语言实现代码编写。

本题在知道递归之后,我们更需要知道DFS(深度优先遍历)和回溯这两个算法,如果我们知道了这两个算法,这道题简直可以秒杀。

我们先以3为例,看看整个过程是如何模拟的

在这里插入图片描述
从上面的图,不难看出是一个树状结构,那么现在难点就在于怎么去完成树状结构的输出。对于这个问题就需要交给DFS(深度优先遍历)和回溯这两个算法来解决啦。

介绍一下DFS(深度优先遍历)和回溯的定义:

  1. 回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
    找到一个可能存在的正确的答案;
    在尝试了所有可能的分步方法后宣告该问题没有答案。
  2. 深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止

从全排列问题开始理解回溯算法

我们尝试在纸上写 3个数字、4个数字、5 个数字的全排列,相信不难找到这样的方法。以数组 [1, 2, 3] 的全排列为例。

先写以 1开头的全排列,它们是:[1, 2, 3], [1, 3, 2],即 1 + [2, 3] 的全排列(注意:递归结构体现在这里);
再写以 2开头的全排列,它们是:[2, 1, 3], [2, 3, 1],即 2 + [1, 3] 的全排列;
最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1],即 3 + [1, 2] 的全排列。
总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示(上面展示的图片)。

解析图片:

  1. 每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为「状态」;
  2. 使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」回溯);
  3. 深度优先遍历,利用递归一层一层往下搜索;
  4. 深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果。
    使用编程的方法得到全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列

如何设计状态变量

首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即:在已经选择了一些数的前提下,在剩下的还没有选择的数中,依次选择一个数,这显然是一个 递归 结构;

递归的终止条件是: 一个排列中的数字已经选够了 ,因此我们需要一个变量来表示当前程序递归到第几层,我们把这个变量叫做 depth,表示当前要确定的是某个全排列中下标为 depth 的那个数是多少;

布尔数组 b,初始化的时候都为 0 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 1 ,这样在考虑下一个位置的时候,就能够以 O(1)的时间复杂度判断这个数是否被选择过,这是一种**「以空间换时间」**的思想。
这些变量称为「状态变量」,它们表示了在求解一个问题的时候所处的阶段。需要根据问题的场景设计合适的状态变量。

对笔者启发很大的文章

对笔者启发很大的文章同时也是笔者的参考文章.

参考代码

#include<stdio.h>
void DfsFullPermutation(int *a, int *b, int n , int depth)   //全排列递归函数 
{
	int i;
	if(depth==n+1)  //如果深度与要求输入整数匹配 
	{
		for(i=1;i<=n;i++)
		{
			printf("%d ",a[i]);  //输出a中的数字 
		}
		printf("\n");
		return;
	}
	for(i=1;i<=n;i++)
	{
		if(b[i]==0)  //b数字判断该数字是否用过 
		{
			b[i]=1;  //使用过的数,b变为1 
			a[depth]=i;//a在该深度的数为未使用过的数 
			DfsFullPermutation(a,b,n,depth+1);//继续确定下一个数 
			b[i]=0;//回溯的时候一定要把上次使用过的数变为没有使用过 
		}
	}
}
int main()
{	
	int n,i;
	int a[100]={0};
	int b[100]={0};
	scanf("%d",&n);
	DfsFullPermutation(a,b,n,1);
	return 0;
}

补充测试的数据

只要检查N=3是不是对的就行了。

思考

为什么不是广度优先遍历

首先是正确性,只有遍历状态空间,才能得到所有符合条件的解,这一点 BFS 和 DFS 其实都可以;
在深度优先遍历的时候,不同状态之间的切换很容易 ,可以再看一下上面有很多箭头的那张图,每两个状态之间的差别只有 1处,因此回退非常方便,这样全局才能使用一份状态变量完成搜索;
如果使用广度优先遍历,从浅层转到深层,状态的变化就很大,此时我们不得不在每一个状态都新建变量去保存它,从性能来说是不划算的;
如果使用广度优先遍历就得使用队列,然后编写结点类。队列中需要存储每一步的状态信息,需要存储的数据很大,真正能用到的很少 。
使用深度优先遍历,直接使用了系统栈,系统栈帮助我们保存了每一个结点的状态信息。我们不用编写结点类,不必手动编写栈完成深度优先遍历。

不回溯可不可以

可以。搜索问题的状态空间一般很大,如果每一个状态都去创建新的变量,时间复杂度是 O(N)。在候选数比较多的时候,在非叶子结点上创建新的状态变量的性能消耗就很严重。

就本题而言,只需要叶子结点的那个状态,在叶子结点执行拷贝,时间复杂度是 O(N)。路径变量在深度优先遍历的时候,结点之间的转换只需要 O(1)。

最后,由于回溯算法的时间复杂度很高,因此在遍历的时候,如果能够提前知道这一条分支不能搜索到满意的结果,就可以提前结束,这一步操作称为 剪枝

剪枝

回溯算法会应用「剪枝」技巧达到以加快搜索速度。有些时候,需要做一些预处理工作(例如排序)才能达到剪枝的目的。预处理工作虽然也消耗时间,但能够剪枝节约的时间更多;
提示:剪枝是一种技巧,通常需要根据不同问题场景采用不同的剪枝策略,需要在做题的过程中不断总结。

由于回溯问题本身时间复杂度就很高,所以能用空间换时间就尽量使用空间。

总结

做题的时候,建议 先画树形图 ,画图能帮助我们想清楚递归结构,想清楚如何剪枝。拿题目中的示例,想一想人是怎么做的,一般这样下来,这棵递归树都不难画出。

在画图的过程中思考清楚:

分支如何产生;
题目需要的解在哪里?是在叶子结点、还是在非叶子结点、还是在从跟结点到叶子结点的路径?
哪些搜索会产生不需要的解的?例如:产生重复是什么原因,如果在浅层就知道这个分支不能产生需要的结果,应该提前剪枝,剪枝的条件是什么,代码怎么写?

回溯题单推荐

全排列.
全排列 II.

  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值