搜索与图论(1)

目录

1.深度优先搜索(DFS)

2.宽度优先遍历(BFS)

3.树与图的储存

4.树与图的深度优先遍历

5.树与图的宽度优先遍历

6.拓扑排序


1.深度优先搜索(DFS)

深搜,顾名思义,就是优先考虑搜索的深度,尽量往深了搜,就像一个执着的人,一条道走到黑。如果没有路可走了就往回退一步,继续寻找可以走的路,同时还要恢复搜索之前的一切(恢复现场),这就是回溯

如图所示:

搜索轨迹看起来就像一棵树,虽然图有点丑,但确实有一点像吧

代码实现:

void dfs(int u) {
    if(u == n) { //如果搜到终点 
	 //输出答案 
	 return ; 
    } 
    for(int i = 1; i <= n; i++) {
   	if(!vis[i]) { //如果没有访问过 
	    //记录
	    vis[i] = true; //标记已经走过 
	    dfs(u + 1); //往深的一层继续搜索
	    //如果没路了
			
	    //撤销记录
	    vis[i] = 0; //恢复现场 
	} 
    }
}


------------

2.宽度优先搜索(BFS)

也叫广度优先搜索,即广搜。它的搜索顺序与深搜不同,它优先考虑的是搜索的宽度,就像一个稳重的人,先搜索下一步所有的可能,再在每种可能的基础上进行扩展,直到搜到答案。

如图所示:

搜索轨迹看起来像水波,逐渐向远处扩散。

代码实现:

void bfs() { //宽搜
	//宽搜要用队列哦 
	q.push(1); //压入起点
	while(!q.empty()) { //若队列不空 
		int t = q.front(); //取出队头 
		q.pop(); //弹掉 
		if(……) { //若搜到答案 
			//输出答案
			return ; 
		} 
		//扩展队列
	} 
}

还能再具体一点吗?

宽搜实现过程如图:

在此总结一下深搜和宽搜的区别:

深搜对应的数据结构是 stack(栈),空间复杂度 \(O(h)\) (\(h\) 为深度),时间复杂度 \(O(2^h)\) , 它不具有最短性

宽搜对应的数据结构是 queue(队列),空间复杂度 \(O(2^h)\),时间复杂度 \(O(h)\)可用于求最短路(只有当边权值固定为 \(1\) 时才可用),因为在第一次搜到时必定是最短路。


3.树与图的储存

因为树是一种特殊的图(无环连通图),图分为两种,有向图和无向图,无向图又是一种特殊的有向图,所以这里我们只用考虑有向图的储存方式即可触类旁通。
有向图的存储通常有两种方式,分别为邻接矩阵、邻接表。

邻接矩阵

这是一种使用较少的方法,它的实质是定义一个二维数组用于储存这张图的信息。比如定义 \(mapp[a][b]\) 储存的是节点a到节点b的边的信息,若有权重,则储存权重;若无权重,则为一个布尔值,\(0\) 表示无边,\(1\) 表示有边。

邻接表

它的实质是对于每个节点,都定义了一个单链表,用于存储这个点可以走到哪个点。

如图所示:

以上两种存储方式的空间复杂度都为 \(O(n^2)\) 且运行速度较慢,那有没有一个相对较快、内存占用低的方式呢?

我们将邻接表进行优化,便得到了一个新方式:链式前向星。

与普通邻接表不同,链式前向星是一个链式结构,而普通邻接表是线性结构,无论是时间上还是空间上,链式前向星都完胜普通邻接表。

那我们怎么来实现它呢?代码如下:

const int N = 10010;
int h[N], e[N], ne[N], idx; //h[N]记录每个节点的最后一条出边,e[N]记录每
条边要到达的点,ne[N]记录了从相同节点出发的下一条边在e[N]数组中的储存位置,
idx表示读到了第几条边

void add(int a, int b) { //插入一条由a指向b的边 
	e[idx] = b, ne[idx] = h[a], h[a] = idx++; 
} 

这里有更加详细的介绍


4.树与图的深度优先遍历

同上,我们只考虑如何遍历有向图。实现方式和深搜类似。

时间复杂度:\(O(n + e)\)

代码实现:

void dfs(int u) { //从节点u开始
	vis[u] = true; //标记已被访问过 
	for(int i = h[u]; i != -1; i = ne[i]) { //遍历此节点的每一条边 
		int j = e[i];
		if(!vis[j]) dfs(j); //若未被访问,则继续深搜 
	}
}

5.树与图的宽度优先遍历

实现方式和宽搜类似。

时间复杂度:\(O(n + e)\)

代码实现:

int d[N]; //储存第一个节点到其他节点的距离(最小距离) 
void bfs() {
	q.push(1);
	memset(d, -1, sizeof(d));
	d[1] = 0; //第一个点到自己的距离当然为0
	while(!q.empty()) {
		int t = q.front();
		for(int i = h[t]; i != -1; i = ne[i]) {
			int j = e[i];
			if(d[j] == -1) { //若第一次搜到 
				d[j] = d[t] + 1; //记录距离 
				q.push(j); //扩展队列 
			}
		}
	} 
	printf("%d", d[n]); //输出 
} 

6.拓扑排序

拓扑序列是针对有向图说的,无向图是没有拓扑序列的。它的定义是:如果一个由点组成的序列 \(A\) 满足,对于图中的每条边 \((x, y)\)\(x\)\(A\) 中都出现在 \(y\) 之前,则 \(A\) 是该图的一个拓扑序列

为什么说“一个拓扑序列”呢?我们不难发现,对于某些有向无环图,不只有一个拓扑排序,比如:

这个图就有两个拓扑序列,分别为 \(1 2 3\)\(1 3 2\)

我们如何求一个有向无环图的拓扑序列呢?

其实需要用到上面讲的宽度优先遍历。首先我们思考,什么样的节点可以放在序列的前面呢?显然,所有入度为 \(0\) 的点都是可以作为起点的,因为没有任何一条边指向它,即没有任何一个点能在它的前面,所以我们先让所有入度为 \(0\) 的点入队,此时剩下的节点一定就为入度不为 \(0\) 的节点了,接下来又该怎么办呢?

你看,所有入度为 \(0\) 的点都已经放在前面了,假设其中有个节点 \(A\) ,有条边由 \(A\) 指向 \(B\) ,那意思是不是说, \(B\) 一定在 \(A\) 的后面?答案是肯定的,这样我们就保证了节点 \(A\) 的所有出边所连的节点都是一定满足以上规律的,既然这样,这些边对于 \(A\) 便没有任何作用了,我们将这些边删掉。删掉后,如果 \(B\) 也变成了入度为 \(0\) 的点,那又可以将 \(B\) 放入队列,以此类推。

代码实现如下:

int h[N], e[N], ne[N], idx;
int n, m, d[N];
int q[N];
void topsort() {
	int hh = 0, tt = -1; //手写队列
	for(int i = 1; i <= n; i++) { //所有入度为0的点入队
		if(!d[i]) q[++tt] = i;
	} 
	while(hh <= tt) {
		int t = q[hh++];
		for(int i = h[t]; i != -1; i = ne[i]) { //遍历队头节点的
        						所有出边
			int j = e[i];
			d[j]--; //删除此边
			if(!d[j]) q[++tt] = j; //若B也变成入度为0的点,
            					则将B放入队列
		} 
	}
	for(int i = 0; i < n; i++) {
		printf("%d ", q[i]); //输出(这里体现出了手写队列的好处,弹出操作只是将队头指针往后移动,但排好序的序列刚好是从下标0开始的,没有被“弹掉”
	}
	return ;
} 

这时候就有有一个疑问了,这样做真的可以使这个有向无环图的所有节点入队吗?答案是可以的。我们可以浅浅地证明一下。

这里采用反证法来证明(主要是蒟蒻不知道怎么从正面证明)首先,只有入度为 \(0\) 的点才能被压入队列,假设存在一个有向无环图,我们用以上方法不能求出它的拓扑排序,则当程序进行到一定程度时找不到入度为 \(0\) 的点,且仍有点未入队。所以此时所有的节点至少有一个入边,不妨设每个节点有且仅有一条入边,我们从节点 \(A\) 开始找起,则必定有一个节点 \(B\) 指向它,那么也必定有一个节点 \(C\) 指向节点 \(B\) \(\dots\dots\) 以此类推,假设此图有 \(n\) 个节点,但我们会找出 \(n + 1\) 个点。根据抽屉原理,则必定有两个节点重合,即这两个节点为同一节点,则必然存在一个环,不合题意,故假设错误,所以对于所有的有向无环图,我们用以上方法都能求出它的拓扑排序,证明完毕。

这样一来,我们不仅证明了这个方法的可行性,还得到了一条重要性质:对于所有的有向无环图,至少存在一个入度为 \(0\) 的节点。

posted @ 2023-09-26 15:05  Brilliant11001  阅读(12)  评论(0编辑  收藏  举报