事实上,无向图可以看成是一个特殊的有向图,因此它们的表示方法和相关的算法其实都是一样的。
一、BFS
这个是图BFS的模板,这个比较容易错误的一点是结点的已访问标记应该在入队时标记还是应该在出队时标记。正确的做法肯定是在结点要入队时就要做好标记,尽管队列中的元素还没有被访问,但是既然已经放到队列当中那必然会按照入队顺序访问到。每一个结点只能入队一次并且出队一次,如果在一个结点出队的时候才标记为已经访问,那么在队列中但还没有被访问到的那些结点有可能会多次入队,这样就会造成一个结点被访问多次的错误。
尽管这是一个对图做BFS的模板,但是这个模板也可以用于遍历树,只要去掉访问标记就可以。BFS处理任何问题的模板基本上都是这个样子。
#define NUM_NODE 100
//使用vector可以方便的形成邻接表和邻接矩阵,不用自己再动手建立
vector<vector<int>> graph;
vector<bool> visited = vector<bool>(NUM_NODE, false);
//表示从结点v开始广度优先遍历图,这个函数只能遍历其中的一个联通分量
//要写一个for循环来判断visited来确定是否需要遍历其他的连通分量
void bfs(int v)
{
for(int i = 0; i < visited.size(); ++i)
visited[i] = false;
queue<int> que;
que.push(v);
visited[v] = true;
while(!que.empty())
{
int temp = que.front();
que.pop();
//visited[temp] = true;
cout << temp << " ";
//标记的目的是为了防止再次访问到这个结点,那分为在一个顶点入队的时候标记和出队的
//时候标记,在出队列的时候标记会造成错误
int size = graph[temp].size();
for(int i = 0; i < size; ++i)
{
if(!visited[graph[temp][i]])
{
que.push(graph[temp][i]); visited[graph[temp][i]] = true;
}
}
}
return;
}
二、DFS
这个dfs的模板是基于上面bfs中的图的定义写的,在上面的定义中graph是一个邻接表。可以看到图的dfs算法和树的dfs算法相比较同样也是仅仅差了一个访问标记。同bfs一样,几乎所有dfs解题的模板都是这样子。
void dfs(int v)
{
visited[v] = true;
cout << v << " ";
int size = graph[v].size();
for(int i = 0; i < size; ++i)
{
if(!visited[graph[v][i]])
dfs(graph[v][i]);
}
return;
}
三、拓扑排序
这是力扣上面207.课程表那道题目我的代码,也算是用BFS实现的一个拓扑排序的模板,拓扑排序就是要把一个图中的顶点按照一定的顺序排序,保证如果顶点a有一条边指向b,那么a一定出现在b的前面。
思路大概是:
- 统计入度不为0的结点的个数nums;以及将所有的入度为0的顶点放入队列que中初始化队列。
- 然后就是BFS的框架了,从队列中取出一个入度为0的结点,把所有它指向的边都删除,也就是把它指向的邻接点的入度减一,若某个邻接点入度变为0,把nums减一,并且把该节点入队列。直到队列为空。
- 判断nums是否为0,若是那么说明图中没有环,若不是说明有nums个结点形成了一个环。
因为基本上代码和广度优先搜索一样,所以这也算是:广度优先搜索可以判断一个有向图是否是有向无环图吧
这个是这道题目的官方题解,它用了BFS和DFS两种方法来解决这道题目。。。那其实这个题解就解释了DFS和BFS都可以用来判断一个图是不是有向无环图,因为拓扑排序就是使用这两种算法来实现的。使用DFS来判断一个图中是不是含有环的算法把每一个结点分成了三种状态,这个还挺巧妙的
class Solution {
public:
/*
本质上就是一道拓扑排序的题目,判断这个图是不是有向无环图即可
拓扑排序是使用了队列的,要把是所有的入度为0的结点全部都放进队列里面。
对于[ai, bi],顶点bi指向ai
先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi],表示如果要学习课程 ai 则必须先学习课程 bi
numCourses是课程的数量
*/
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
//size是这个图当中边的数量,numCourses是顶点的数量
int size = prerequisites.size();
//先统计每一个结点的入度,并且构建出邻接表表示的图
vector<int> numOfentry(numCourses, 0);
vector<vector<int>> graph(numCourses, vector<int>());
for(int i = 0; i < size; ++i)
{
++numOfentry[prerequisites[i][0]];
graph[prerequisites[i][1]].emplace_back(prerequisites[i][0]);
}
//统计非0入度结点的个数并且把0入度的结点全部都放入到队列当中去
int nums = 0;
queue<int> que;
for(int i = 0; i < numCourses; ++i)
{
if(numOfentry[i] == 0)
que.push(i);
else
++nums;
}
//只要在这个队列执行的过程中把出队列的这些顶点依次放入数组当中就得到了拓扑序列,因此
//这也算是实现了拓扑排序
while(!que.empty())
{
int temp = que.front();
que.pop();
int size = graph[temp].size();
for(int i = 0; i < size; ++i)
{
if((--numOfentry[graph[temp][i]]) == 0)
{
que.push(graph[temp][i]);
--nums;
}
}
}
return nums == 0;
}
};