算法学习--从深度优先搜索到全排列问题(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/XUCHEN1230/article/details/84892910

直接进入主题,关于深度优先搜索,发源于数据结构,起初是用来进行图的遍历,经过科研人员长时间的研究和总结,已经运用到实际的生产生活中去,用以解决需要大量重复、排列组合的相关问题。

参考书目:

《算法导论》、《啊哈!算法》、《数据结构(李建忠翻译版)》。

关于基本数据结构图的相关内容,用于本算法中的内容,主要是关于伪代码中的一些解释:

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;
		}
	}
}

可以进行测试;

结果如下。

做个笔记,方便以后复习。

猜你喜欢

转载自blog.csdn.net/XUCHEN1230/article/details/84892910