直接进入主题,关于深度优先搜索,发源于数据结构图,起初是用来进行图的遍历,经过科研人员长时间的研究和总结,已经运用到实际的生产生活中去,用以解决需要大量重复、排列组合的相关问题。
参考书目:
《算法导论》、《啊哈!算法》、《数据结构(李建忠翻译版)》。
关于基本数据结构图的相关内容,用于本算法中的内容,主要是关于伪代码中的一些解释:
1.首先理解邻接链表:对于无向图而言,G的邻接链表表示千万不能理解为方向。这幅图仅仅表示这个结点与其他哪几个结点相连接。只有对于有向图,才是有确定方向的。
无向图
有向图
2.关于Adj[u]:就是与u相邻接结点的集合。就是在这个集合内的点,与u都有线相连;
3.u.d 和 u.f 两个时间戳,本文暂时不讨论;
下面给出伪代码,再来分析:
DFS(G)
for each vertex u ∈ G.V
u.code = WHITE
u.π = nul
time = 0
for each vertex u ∈ G.V
if u.color == WHITE
DFS-VISIT(G,u)
DFS-VISIT(G,u)
time = time + 1
u.d = time
u.color = GRAY
for each v ∈ G:Adj[u]
if v.color == WHITE
v.π = u
DFS-VISIT(G,u)
u.color = BLACK
time = time + 1
u.f = time
这是原版的伪代码,来自《算法导论》,按照书上的说法,这就是最基本的深度优先算法。输入图可以是有向图,也可以是无向图。
下面对伪代码进行分析,理解其原理:
//vertex:顶点;
//v.π 前驱结点
//WHITE:未被访问,GARY:已被访问,BLACK:该结点的所有邻接点都访问完
//关于 time 本文都不讨论
DFS(G)
for each vertex u ∈ G.V //初始化图的每一个顶点
u.code = WHITE //为每个结点上白色,表示都未被访问
u.π = NUL //前驱结点均为空
time = 0
for each vertex u ∈ G.V //遍历每一个结点
if u.color == WHITE //如果这个点未被访问过
DFS-VISIT(G,u) //开始访问
DFS-VISIT(G,u)
time = time + 1
u.d = time
u.color = GRAY //将这个点涂成灰色,表示已被访问
for each v ∈ G:Adj[u] //遍历其所有的邻接点
if v.color == WHITE //若该邻接点未被访问
v.π = u //标记其前取电,避免迷路
DFS-VISIT(G,u) //遍历
u.color = BLACK //该结点的所有邻接点都访问完,涂成黑色
time = time + 1
u.f = time
通过这个分析可以知道,在深度优先遍历中,从一个点出发,开始往相关联的邻接点出发,然后再往相关联的邻接点出发;若此路不通,则返回前驱结点,再往前驱结点的相关联的邻接点。。。然后循环往复,由此可以看出递归的思想。
对每一个点重复与前面相同的操作,然后找到进入下一个点入口。这是深度优化搜索的思想。
另外我认为重要的是,对搜索的数据之间,应当存在必要的联系。能够利用他们的关联关系进行相关的遍历。后面有个例子,我将说明如何在同一个数组元素之间建立联系,不断遍历的。
那么总结归纳深度优先搜索的要点如下:
1.结点之间存在一些联系,这是我们遍历的条件;
2.对每个结点进行的操作相同;
3.能够找到进入下一个结点的入口;
4.递归的边界应当清楚。
由上述四个要点,回过头来看伪代码:
DFS(G)
for each vertex u ∈ G.V
u.code = WHITE
u.π = nul
time = 0
for each vertex u ∈ G.V //结点之间相互的联系,可以进行遍历; 边界
if u.color == WHITE //进入下一个结点的入口
DFS-VISIT(G,u) //操作
DFS-VISIT(G,u)
time = time + 1
u.d = time
u.color = GRAY //涂色操作
for each v ∈ G:Adj[u] //结点之间相互的联系,可以进行遍历; 边界
if v.color == WHITE //进入下一个节点的入口
v.π = u
DFS-VISIT(G,u) //操作
u.color = BLACK
time = time + 1
u.f = time
均能够找到相适应的语句。
下面看看《啊哈!算法》提供的实例:
简而言之题目如下:
1-9个数,全排列。
本题当然能用枚举的方法解决。
但我们对简单情况,1,2,3 的全排列进行思考和归纳可以发现:
我们得到第一个排列:1,2,3,的过程是,
确定第一个数1,再确定第二个数2,再确定第三个数3;
我们得到第一个排列:1,3,2,的过程是,
确定第一个数1,在确定第二个数3,在确定第三个是2;
等等等等。。。
我们似乎可以得到这样的过程:
确定一个数{
确定一个数{
确定一个数{
}
}
}
看吧,这也是一个递归的过程吧?这样我们就能暂时认为,这是个递归的操作。
我们具体怎么确定第一个数?第二个数?第三个数呢?
假定我们随意确定一个数:1
那么,我们再次确定数时,只能在剩下的2,3中寻找;
假定确定第二个数:2
那么,我们再次确定数时,只能是3.
到此,我们大概能得出这样一个方法:
确定一个数{
遍历(关联的所有数){
若(这个数没被选定)
我可以选择
}
}
这就是我们要写函数的雏形。
//**************************************************************************************************************************************************
现在把这个雏形具体实现,
两个要点:
一、找到关联,实现遍历;
二、选择一个数;
因为这三个数都存放在数组中,那我们遍历的代码就可以是:
for(int i=0;i<3;i++){
}
那么如何跟伪代码一样,实现图的连通关系呢?
利用循环嵌套,可以让线性的数组结构产生犹如图般的多边结构:
for(int i=0;i<3;i++){
for(int j=0;j<3;j++)}
{
{
在复杂情况时,循环嵌套过多,可以改成递归。
现在遍历语句的模样已经有了。再实现选择语句。
有了上面可以参考深度优化搜索,我们可以获得灵感:涂色
为选中的数字涂色,在下一次选择时不选他,而选择其他未涂色的。
就可以得到这么几句代码:
if (color[i] == 0) {
color[i] = 1;
}
约定0未被访问,1被访问;
然后,我们将循环嵌套改造成递归:
//arry为原数据数组
dfs(int i,int n){
for (i; i < n; i++) {
if (color[i] == 0) {
color[i] = 1;
dfs(0);
color[i] = 0;
}
}
}
我们在这里实现了,遍历所有的结点及其邻接点;以及对他们进行选择性的访问。
下面完成所有的细节:
void dfs(int i, int *color, int *arry, int n,int *output,int k) {
if (k == n) {
for (int j = 0; j < n; j++) {
cout << output[j];
}
cout << endl;
return;
}
//output是输出容器;
for (i; i < n; i++) {
if (color[i] == 0) {
output[k] = arry[i];
color[i] = 1;
dfs(0, color, arry,n,output,k+1);
color[i] = 0;
}
}
}
output[k]存储数据输出的样式;
i总是为0能保证充分的遍历,每个点都能与其他n-1个点发生联系;
k+1让output能向下走,继续向后存储数据;
k作为输出判据;
如果弄懂了上述伪代码的深度优先搜索过程,全排列这个过程不难理解。
#include<iostream>
using namespace std;
void dfs(int i, int *color, int *arry, int n, int *output, int k);
int main()
{
int i = 0;
int arry[3] = { 1,2,4};
int color[] = { 0,0 ,0};
int output[3];
int cnt = 0;
dfs(i, color, arry, 3, output, 0);
return 0;
}
void dfs(int i, int *color, int *arry, int n,int *output,int k) {
if (k == n) {
for (int j = 0; j < n; j++) {
cout << output[j];
}
cout << endl;
return;
}
//output是输出容器;
for (i; i < n; i++) {
if (color[i] == 0) {
output[k] = arry[i];
color[i] = 1;
//cout << i << "sd" << endl;
dfs(0, color, arry,n,output,k+1);
color[i] = 0;
}
}
}
可以进行测试;
结果如下。
做个笔记,方便以后复习。