DFS解全排列问题(1)
写了好几版DFS,但是都被删了,实在是不知道从何下手。最后干脆回头一想,不要原理,从实际问题出发,从代码反推原理,终于跟着网页的讲解有了思路。
这里先附上我参考的两个链接:
假设问题:
输入一个数,输出1~n的全排列
首先考虑第一种排列方法,自然排列:
for(int i=1;i<=n;i++)
{
a[step]=i;//step代表第几个数,i代表具体的数值
}
然后考虑,如果在某个位置上,某个数已经被用过,那么在后面的排列中,这个数不能再出现,这个位置上也不能存入数字了,所以我们需要一个数组来标记我们排列过的位置。
for(int i=1;i<=n;i++)
{
if(vis[i]==0)//vis[i]==1表示将数字i放入第step个位置中
{
a[step]=i;
vis[i]=1;
}
}
接着考虑,现在step位置的数字已经排列好了,那么后面的step+1,以及后面的数字怎么排列的问题,其实也很简单,还是按照step的思路去排列:
void dfs(int step)
{
for(int i=1;i<=n;i++)
{
if(vis[i]==0)//要注意进行恢复初始状态,这里才能再次调用
{
a[step]=i;
vis[i]=1;
dfs(step+1);//递归调用,接着排列step+1
vis[i]=0;//这里必须要恢复初始状态,否则下一次不能进行判断
}
}
}
最后考虑终止条件,这个反而更简单,只要step的值大于n+1时,说明n个数已经排列完成了
void dfs(int step)
{
if(step==n+1)//一定是n+1,此时说明已经找到了解,可以直接输出或者返回了
{
for(int i=1;i<=n;i++)
{
cout<<a[i];
}
cout<<endl;
return;
}
for(int i=1;i<=n;i++)
{
if(vis[i]==0)//要注意进行恢复初始状态,这里才能再次调用
{
a[step]=i;
vis[i]=1;
dfs(step+1);//递归调用,接着排列step+1
vis[i]=0;//这里必须要恢复初始状态,否则下一次不能进行判断
}
}
}
现在已经基本得到了DFS的模型函数了,其核心就在于要有边界判断,要在当前遍历每一种可能,然后进行递归,因为每一种状态都是同样的选择。
完整代码:
#include<iostream>
#include<string.h>
#include<stdio.h>
using namespace std;
int n;
int a[12];
bool vis[12];
void dfs(int x){
if(x == n+1){ //此处为n+1 而不是n,因为如果在第n次时就输出,那么第n个数是没有存进去的
for(int i = 1; i<= n; i++) {
printf("%d ",a[i]);
}
printf("%d\n");
return;
}
for(int i = 1; i<=n; i++){ //遍历n个数字中哪一个没有放置过
if(vis[i] == false) {
a[x] = i;
vis[i] = true;
dfs(x+1);
vis[i] = false;
}
}
return;
}
int main(){
scanf("%d",&n);
memset(vis,false,sizeof(vis));//将所有的vis[]数组全部初始化为0
dfs(1); //此时放置第1个数字
return 0;
}
这里我自己对于递归语句哪里总是有点疑惑,到底是怎样的一个实现流程,我对程序进行了单步调试,终于明白了这个过程,终于感到了调试功能的强大。顺便说一下,这里我使用的是vscode。
假设我输了3,相当于列出1~3的所有全排列,可能你们认为太少了,但是用来搞清楚语句的调用顺序和执行原理,是完全够了,下面进入正题;
看主函数,输入3之后,初始化所有的vis标记数组;
第一轮
进入dfs(1)函数
此时x=1,肯定不满足边界条件,n+1=4,所以直接进入循环
此时从1到3进行循环
vis[1]==false,满足条件
然后a[1]=1;
将vis[1]=1置1,代表已经被标记;
然后再递归调用数字2,相当于将数字2进行排列,dfs(1)相当于对1进行了位置安放;执行语句dfs(2);
第二轮
此时,x=2,还是不满足x==(n+1=4);
直接进入循环,还是从1到3循环
vis[1]在上一轮中已经被置为1了,所以此时不满足,再回到循环,i++,i变成了2;
再次判断,vis[2]此时还是空的==false,所以进入if语句
执行a[2]=2;
vis[2]=true;//将vis[2]进行标记,说明这个位置已经被排列了
然后再递归调用数字3,执行dfs(3)
再进行第3轮的判断
第三轮
此时,x=3,依旧不满足边界条件,继续进入循环
前面两次的vis[1]和vis[2]都已经被置为true,直接i++,直到i=3,
此时,vis[3]==false;进入if语句
执行a[3]=3;
vis[3]=true;
然后再执行递归调用dfs(4)
注意:
此时再回到一开始的边界判断时,x=4,已经满足了边界条件了,所以可以进入边界语句的判断
此时从1到3的循环,然后将答案挨着输出,此时a[i]数组里存的是刚刚保存的值
这里有个很难理解的地方,就是递归之后的vis[i]归零的过程,这里需要掌握递归的执行流程,调研递归前面的语句和后面的语句是不一样的。建议去看一下这方面的内容,也可以加深自己对程序执行流程的理解。
我们执行完了之后,此时vis[i]里面的值都为true,
此时应该反向的去执行递归后面的语句,
即应该执行vis[3]=false;
vis[2]=false;
此时又重新进入循环,vis[1]和vis[2]仍然为true,但是vis[3]却已经是false了,重新进入if判断
执行a[2]=3;
vis[3]=true;
递归调用dfs(3);
重复同样的流程,直到遍历完所有的可能。