例题描述
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。 现在,请你按照字典序将所有的排列方法输出。 输入格式 共一行,包含一个整数 n。 输出格式 按字典序输出所有排列方案,每个方案占一行。 数据范围 1≤n≤7 输入样例: 3 输出样例: 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
参考代码(C++版本)
#include <iostream> using namespace std; int n; const int N = 10; int path[N];//存放dfs时,路径上的值 bool st[N];//记录当前这个位置的状态,用于判断是否被用过 void dfs(int u) { //u已经递归到最终的n这层,表示已经对前n个位置部署好了相应的数字,属于搜索完成的情况,此时可以输出这条路径中存的值 if(u == n) { for(int i = 0; i < n;i++) cout << path[i]<<' '; cout << endl; return; } for(int i = 1; i <= n;i++) { //判断当前这个值有没有用过 if(!st[i]) { path[u] = i;//把这个数存到路径数组中 st[i] = true;//标记这个数已经用过了 dfs(u+1);//第u个位置的数据已经部署好了,现在需要走到下一个位置去部署数据 st[i] = false;//结束递归,回溯,恢复现场 } } } int main() { cin >> n; dfs(0); return 0; }
问题剖析
什么是深度优先遍历(DFS)
DFS —— 不撞南墙不回头的执著boy
基本模型
void dfs(int step) { 判断边界 尝试每一种可能 for(int i=1;i <= n;i++>) { 继续下一步 dfs(step+1); 恢复现场 } 返回 } //模型中的每一种尝试就是一种"拓展"。每次站在盒子面前的时候,其实都有n种拓展,但不是每种拓展都会拓展成功
形象的比喻:
钥匙丢了,找钥匙的这个过程就和执着的dfs一样。无论从哪个房间开始找都是可以的,可以从主卧,可以从客厅,然后就是对每一个位置挨着挨着的找,不放过任何一个死角,所有的抽屉、储物柜、床上、床下和衣柜等等位置,找不到?换下一个位置继续找,直到找到为止。
温馨提示:把房间找乱了,走到下一个位置找之前,记得还原房间之前的状态,否则家妻可能要家法伺候了。
形象的模拟
小明手中有三张卡片分别是1,2,3。现在要把这三张卡片放到三个编号为1,2,3的盒子里,并且每个盒子只能放一张卡片。问现在有多少种不同的放法了?
现在小明走到盒子面前,心想我是先放1号卡片,还是先放2号卡片,还是先放3号卡片了?
很明显,这三种情况都是可取的。为了放卡片的的时候不犹豫,就约定每次走到盒子面前,都是先放1号,再放2号,最后放3号。
说做就做,将1号卡片放到了1号盒子中
放好之后,小明来到2号盒子之前,本来按照之前的顺序是应该按照1,2,3的顺序放。但是小明现在手中只有2号和3号卡片了,于是将2号卡片投到2号盒子中。
放好之后,小明走到3号盒子面前。
现在小明在3号盒子之前,按照之前的约定,应该是1号,2号,3号的顺序放置。但是小明手中现在只有3号卡片了,于是只能将3号卡片投入3号盒子中。
放置好以后,小明再向后走一步,到4号盒子面前,诶?没有4号盒子,确实也应该没有,因为我们的卡片也已经放完了
当小明走到第4号盒子的时候,已经就出现一种排列方式了,即"1,2,3"
是不是到这里就结束了呢?当然不是的,产生一种序列之后,小明应该返回尝试其他可能。现在小明需要退回到3号盒子。
好了,小明退回到3号盒子面前了,需要取回3号盒子中的卡片,尝试能不能放其他的卡片从而产生一种新的序列了?
取回3号卡片以后,小明想再往3号盒子里面放卡片的时候,发现手中仍然只有3号卡片,没有别的选择,于是不得不再退回一步,退回到2号盒子面前。
小明退回到2号盒子后,取回2号卡片。现在小明手中有两张卡片了,分别是2号和3号卡片。
按照之前的约定,现在需要往2号盒子放3号卡片了(上次已经放过3号卡片了)。放好之后,小明来到3号盒子面前。将手中仅有的2号卡片放到3号盒子中,此时生成新的序列"1,3,2"
按照刚才的做法去模拟,就可以得到"2,1,3",“2,3,1”,“3,1,2”,“3,2,1”
总结
通过这个例子,我们应该可以清楚,深度优先遍历的关键在于"当下该如何做"。至于"一下步如何做"则与"当下该如何做"的逻辑一样。
比如结合函数模型和小明放卡片的样例来看:
- dfs(int step)函数的主要功能就是解决当我们走到第step个盒子的时候应该怎么办。通常情况下就是把每一种可能都尝试一遍(一般是使用for循环来遍历)。当现在这一步解决了便进入下一步dfs(step+1)。下一步的解决方式和当前这一步的解决方式完全一样。
- dfs在搜索到一个新的结点时,立即对该新的结点进行遍历(回到样例是遍历我现在手中的卡片,哪些可以按照约定使用);结合树的结构来看,由于总是对新的结点调用遍历,因此看起来是向着"深"的方向前进
- 从放卡片到取卡片,可以观察出,先放入的卡片最后取出,因此可以有"先入后出"的性质,就可以使用栈结构实现,也可以使用递归。
详解例题
现在我们要拿捏的就是按照什么样的顺序一步一步解决放好当前这个数据。对于下一步,只用考虑调用函数递归进去就好。
1.主函数中定一个深度优先遍历的起点(起点不影响结果)
dfs(0);
2,编写dfs函数
- 递归函数的核心1:基线条件。考虑什么时候结束递归。对于深搜而言,到达最后的边界了,也就开始回头了。
- 尝试每一种可能 for(int i = 1; i <= n;i++)
- 判断当前这个数据有没有被用过 if(!st[i])
- 将这个没有用过的数据加入到存放路径搜索中获得的信息的数组path[N]中 path[u] = i;
- 将数据标记为用过了 st[i] = true;
- 处理完当下以后,处理下一步 dfs(u+1);
- 处理回来之后,恢复现场 st[i] = false;