DFS入门——全排列

1、深度优先搜索

深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。

如果你不太懂图,你也可以试着理解这么一段话:“对于每条路,既然选择了,就走到底,把它给走完。”

那么这些路怎么表示出来呢?

2、树与树状图

那就得用到树或树状图了。

不知道大家在学数据结构中树的时候有没有过这么一个疑问:“这玩意儿为啥叫树啊?难道就因为它和咱大自然中的树有一些相似的特点吗?就因为有,有分支,有树叶?”

可我想:“这也不对啊!它们长得也不像啊?”
比如,正常的一棵树,长下面这样。
在这里插入图片描述

                        图1

而数据结构中的一棵树长这样
在这里插入图片描述

                          图2 

一个根长在下面,分支和叶子都在上面,一个根在上面,而分支和叶子都在下面,具有如此大的差异,后者咋就能称之为数据结构中的树呢?

但这或许就是我们大多数人都不懂得换个角度看待事物的原因吧,假如我们把图2给倒过来看,就变成了这样
在这里插入图片描述

                         图3

稍加修饰一些,就变成了这样
在这里插入图片描述

                          图4

想必咱小学一年级的时候就学会了一项叫做“读图识景”的技能吧。通过细致地分析图4一番咱就能够发现:如果从这个角度来看,或许数据结构中的树就好理解多了。

A点作为树根在埋在地底里,高度为0.而B、C、D作为第一个分支上的点,高度为1,由于B,C,D都是从A点衍生出来的,因而A作为了B,C,D的父母,而B,C,D彼此就作为了兄弟。E、F点分别作为兄弟B和兄弟C的父母,高度为2,它们是亲戚,可能是堂兄表兄的关系,然而并不是亲兄弟。E、F点后面无其余点了,因此E、F称之为了叶子或树叶。

现在应该明白了数据结构中树的名字是怎么来的吧,科学家既然那么聪明,因此我们能想到的他们想必也会考虑到。他们既然从大自然中发现了一个新规律,因而给一个新理论或者新发明命名的时候,我想应该是不会取一些与新发明与新理论毫不相干或具有本质性差异的名称的。

好的,现在咱讲完了树,就再来讲讲树状图吧,啥是树状图呢。

树状图,亦称树枝状图。树形图是数据树的图形表示形式,以父子层次结构来组织对象。是枚举法的一种表达方式。树状图也是咱中学时学习概率问题所需要画的一种图形。其实说白了就是数据结构中树的一个雏形,或者说是一种特殊情况,本质上与树并无较大的差别。

咱来看中学时可能会遇到的一道题目吧。
问题描述:一个口袋中装有红、白、绿三个小球,另一个口袋中装有(除颜色外其余都相同)红、白两个小球。现从两个口袋中各取一只小球,求两个小球颜色一样的概率是多少?

解:由题意可画出树状图如图2或图3:

在这里插入图片描述
从图中可以看出,两只小球颜色的可能性共6种,而两只小球颜色一样的可能性只有(红,红),(白,白)共2种,所以两只小球颜色一样的概率(记为事件A)为P(A)=2/6=1/3

3、回溯

回溯是一个汉语词语,读音huí sù,英文recall;look back upon;trace,解释是上溯,向上推导,向内推导。它的释义应该从两个字来理解,其中回,指的是还,走向原来的地方:如回家。 掉转:回首(回头看)。回顾。回眸。回暧。妙手回春。而溯,指的是逆着水流的方向走:溯流而上。 追求根源或回想:回溯。追溯。上溯。追本溯源。

将回溯的思想运用至深度优先搜索就是用采用一种试错的思想,它尝试分步的去解决一个问题。在树的回溯中,沿着树的深度遍历树的结点,尽可能深的搜索树的分支。当某个结点v的所在边都己被探寻过或者在搜寻时结点不满足条件,则回溯到发现结点v的那条边的起始结点。整个进程反复进行直到所有结点都被访问为止或已经找到有效的解答。

如对于图4的一条深度优先回溯的路径就可以为A-B-E-C-F-D,其求解过程大致为:
1)、先访问A,当前搜索路径为A,观察A是否具有子结点,发现有B,C,D三个子结点,选择第一个子结点B作为下一个访问点,进行下一步。
2)、访问B,当前搜索路径A-B,观察B是否具有子结点,发现有子结点E,则将E作为下一个访问点,进行下一步。
3)、访问E,当前搜索路径为A-B-E,观察E是否具有子结点,发现并无子结点,回溯至子结点B。
4)、B无其他子结点,回溯至结点A。
5)、将A的第二个子结点C作为访问点,访问C,当前搜索路径为A-B-E-C,观察C是否具有子结点,发现有子结点F,进行下一步。
6)、访问F,当前搜索路径为A-B-E-C-F,观察F是否具有子结点,发现并无子结点,回溯至C。
7)、C无其他子结点,回溯至A。
8)、将A的第三个子结点D作为访问点,访问D,当前搜索路径为A-B-E-C-F-D,观察D是否具有子结点,发现并无子结点,回溯至A。
9)、至此所有子结点已访问完,搜索结束。最终得出的搜索路径为A-B-E-C-F-D

4、visit数组

上面的流程理解还是比较好理解的,但是我们怎么才能用程序实现呢?我们写程序时怎样才能判断是访问某结点时初次访问还是回溯时的访问呢?其中最关键的一点就是需要用到visit数组了。

英文visit的中文意思是访问,探访。设visit数组的下标为i,则对于visit数组的每一个元素visit[i],有两种状态,一种是未访问,记为0或false,一种是已访问,记为1或true。

在引入visit数组后,上述的流程则可以变更为:

设结点数组为char data[6]={A,B,C,D,E,F},对应的visit数组为int visit[6]={0,0,0,0,0,0};
1)、因为visit[0]为0,访问A,置visit[0]为1,当前搜索路径为A,观察A是否具有子结点,发现有B,C,D三个子结点,选择第一个子结点B作为下一个访问点,进行下一步。
2)、因为visit[1]为0,访问B,置visit[1]为1,当前搜索路径A-B,观察B是否具有子结点,发现有子结点E,则将E作为下一个访问点,进行下一步。
3)、因为visit[4]为0,访问E,置visit[4]为1当前搜索路径为A-B-E,观察E是否具有子结点,发现并无子结点,回溯至子结点B。
4)、因为visit[1]为1,不访问B,且因B无其他子结点,回溯至结点A,因为visit[0]为1,不访问A。
5)、将A的第二个子结点C作为访问点,因为visit[2]为0,访问C,置visit[2]为1,当前搜索路径为A-B-E-C,观察C是否具有子结点,发现有子结点F,进行下一步。
6)、因为visit[5]为0,访问F,置visit[5]为1,当前搜索路径为A-B-E-C-F,观察F是否具有子结点,发现并无子结点,回溯至C。
7)、因为visit[2]为1,不访问C,且因C无其他子结点,回溯至A,因为visit[0]为1,不访问A。
8)、将A的第三个子结点D作为访问点,因为visit[3]为0,访问D,置visit[3]为1,当前搜索路径为A-B-E-C-F-D,观察D是否具有子结点,发现并无子结点,回溯至A,因为visit[0]为1,不访问A。
9)、至此所有结点已访问完,搜索结束。最终得出的搜索路径为A-B-E-C-F-D

5、函数的流程机制

对于这段伪代码:

 int main(){
    f();
    cout << "good job!" << endl;
    return 0;
  }

执行时在内存空间中先执行主函数main(),将压入栈中。
在这里插入图片描述
而执行函数main()的过程中需执行函数f(),因而把函数f()也压入栈中

在这里插入图片描述
当f()执行完之后,f()出栈
在这里插入图片描述
此时开始执行main中剩下的语句,输出"good job!",至此执行完毕。main()出栈,程序执行结束。

同理,对于函数dfs(int level),其结构伪代码为:

void dfs(int level){
    if(level == 2)  return;
    dfs(level + 1);
    cout << level << ":good job!" << endl;
    return;
}

若执行函数dfs(int level)是level初始参数值为1,则流程如下所示:

1)、执行dfs(1),将dfs(1)压入栈中。
在这里插入图片描述
开始执行函数dfs(1)中的语句,此时level等于1,不满足判定条件level == 2,执行dfs(2),将dfs(2)压入栈中

在这里插入图片描述

2)、开始执行函数dfs(2)中的语句,此时level等于2,满足判定条件level == 2,函数执行完毕,dfs(2)出栈
在这里插入图片描述
3)、开始执行dfs(1)中剩下的语句,输出"1:good job!",执行完毕,dfs(1)出栈

在这里插入图片描述
在理解了函数的执行机制后,对于visit数组的元素值变化个人就可以概括为"入栈时置1(或true),出栈时置0(或false)了",因此,上面第5点加入visit数组后的流程其实并不是完全正确的,要实现真正意义上的回溯,整个流程结束后visit数组的每个元素值都应该为0(或false)才对。

至此,可大致设计出回溯思想的深度优先搜索的算法框架伪代码为:

bool visit[];
void dfs(){
     if(返回条件 == true){
     return;
     }
     if(!visit[i]){
     visit[i] = true;
     //operation1
     dfs();
     //operation2
     visit[i] = false;
     }
     return;
}

6、用回溯的深度优先搜索解决全排列问题

回溯的深度优先搜索实现数字全排列的问题,关键在于对全排列的理解。
全排列需要一个入口,能够链接到每个待排列数字,这个入口很巧妙,恰巧就可以是主函数main(),因而当从main()中执行搜索函数dfs()时,dfs()中应设计一个循环,循环的次数表明待排列数字的个数,对于循环中每个待排列数字,仅有当其对应的visit数组元素为0时我们才可以访问,我们访问时置对应的visit数组元素为1,当其访问完并出栈后,则置对应的visit数组元素为0。

例如对于实现数字1,2,3的全排列,其树状图应为:

在这里插入图片描述

根据以上思路,我们可以设计出如下代码:

#include<iostream>
using namespace std;
const int n = 3;
int a[n];
bool visit[n];
void dfs(int step){
	if(step >= n){
		for(int i = 0; i < n; i++){
			 cout << a[i] << " ";
		}
		cout << endl;
		return;
	}
	for(int i = 0; i < n; i++){
		if(!visit[i]){
			visit[i] = true;
			a[step] = i + 1;
			dfs(step + 1);
			visit[i] = false;
		}
	}
	return;
}
int main() {
    dfs(0);
}

当常量n为2时,得到的结果为

在这里插入图片描述

当常量n为3时,得到的结果为

在这里插入图片描述

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值