BFS(广度优先搜索)是一种图的遍历算法,用于从一个起始节点出发,逐层访问图中的所有节点。其基本流程如下:
假设我们有一个无向图,节点间的连接如下:
应该如何实现这种算法呢? 首先我们应该用一个队列来维护节点,进入BFS这个接口的时候,我们应该传入从哪个节点开始进行BFS。 然后用一个队列来维护节点,先将第一个节点push进队列中,然后将这个节点输出之后,先将这个节点保存起来然后再把这个节点pop掉,然后将与这个节点相连的节点push进队列(注意:这里可能会出现重复的节点,比如我们拿上面的例子为例,第一次push的时候我们将C节点已经push进队列了,但是第二层访问完了之后,到第三层的时候,B和C相连还会遍历一次C,所以这里我们应该用一个vector进行标记,标记这个节点被访问过没有),进入循环的条件是队列是否为空。 代码展示:
void BFS(const V& src)
{
size_t srci = GetVertexIndex(src);
queue<int> q;
q.push(srci);//队列
vector<bool> visited(_vertexs.size(), false);//标记数组
visited[srci] = true;
int levelsize = 1;
size_t n = _vertexs.size();
while (!q.empty())
{
for (int i = 0;i < levelsize;i++)
{
int front = q.front();
cout << front << ':' << _vertexs[front] << ' ';
q.pop();
//把front顶点的邻接顶点入队列
for (size_t i = 0;i < _vertexs.size();i++)
{
if (_matrix[front][i] != MAX_W && visited[i] == false)
{
q.push(i);
visited[i] = true;
}
}
}
cout << endl;
//levelzie是下一层的数据个数
levelsize = q.size();
}
}
DFS(深度优先搜索)是一种图的遍历算法,从起始节点开始,尽可能深入探索每个分支,直到无法再继续,然后回溯到上一个节点,继续探索其他分支。它适用于有向图和无向图。
DFS和BFS一样,还是给定一个起点,从这个起点开始进行,但是DFS的方式和BFS完全不同,DFS是一条路走到黑,从当前节点一直走,走到不能走为止,当走到不能走时,进行回溯,回溯到上一个岔口,然后向刚刚没有走过的路口继续走,走到尽头的时候又进行回溯,就一直这样递归,直到把所有节点遍历完为止。
代码展示:
void _DFS(size_t srci, vector<bool>& visited)
{
//进来直接访问这个点
cout << srci << ':' << _vertexs[srci] << endl;
visited[srci] = true;
//找到下一个srci相邻的没有访问过的点,去往深度遍历
for (size_t i = 0;i < _vertexs.size();i++)
{
//如果下一个节点没有被访问过直接遍历
if (_matrix[srci][i] != MAX_W && visited[i] == false)
{
_DFS(i, visited);
}
}
}
void DFS(const V& src)
{
//获取起点下标
size_t srci = GetVertexIndex(src);
vector<bool> visited(_vertexs.size(), false);
_DFS(srci, visited);
}
注意:DFS中也需要用一个bool数组进行标记,当前位置是否被访问过。
最小生成树是一个图的子集,包含图中的所有节点,并且是连通的,同时边的总权重最小。最小生成树的特点是没有回路,并且连接了图中的所有节点。
常用的求解最小生成树的算法有:
下面的图就是上面的图的最小生成树的其中之一。最小生成树是不止一个的。如果我们选择上面那个8,最小生成树又不一样。
克鲁斯卡尔算法是一种用于求解最小生成树(MST)的贪心算法。它通过选择边的方式逐步构建最小生成树,优先选择权重最小的边,并确保不形成回路。
克鲁斯卡尔算法的基本步骤如下:
(u, v)
,检查 u
和 v
是否在同一连通分量中(使用并查集)。u
和 v
的连通分量合并。V-1
(V
为节点数)时,算法结束。O(E log E)
,其中 E
是边的数量,主要由排序边的时间决定。O(V)
,用于存储并查集。根据克鲁斯卡尔算法的步骤:
代码展示:
W Kruskal(Self& minTree)
{
size_t n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(n);
for (auto& e : minTree._matrix) e.resize(n, MAX_W);
//优先级队列,优先级默认是大的优先级高,所以这里要控制
priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
for (int i = 0;i < n;i++)
{
for (int j = 0;j < n;j++)
{
//i<j保证只访问一次
if (i < j && _matrix[i][j] != MAX_W);
{
//将edge插入进去,由于无向图会出现重复添加的情况,所以无向图走一半即可
minq.push(Edge(i, j, _matrix[i][j]));
}
}
}
//选出n-1条边
//开辟一个n个顶点的并查集
size_t size = 0;
//总的权值
W totalW = W();
UnionFindset ufs(n);
while (!minq.empty())
{
//选择一条边
Edge eg = minq.top();
minq.pop();
//观察两个顶点是否在同一个集合
if (!ufs.IsInSet(eg._srci, eg._dsti))
{
cout << _vertexs[eg._dsti] << "->" << _vertexs[eg._srci] << ':' << eg._w << endl;
//将边添加到mintree中
minTree._AddEdge(eg._srci, eg._dsti, eg._w);
//将顶点添加到并查集当中
ufs.Union(eg._srci, eg._dsti);
totalW += eg._w;
++size;
}
}
if (size == n - 1) return totalW;
else return W();
}
普利姆算法是一种用于求解最小生成树(MST)的贪心算法。它从一个节点开始,通过逐步选择连接已访问节点和未访问节点的最小权重边来扩展生成树,直到所有节点都被包含。
普利姆算法的基本步骤如下:
O(E log V)
,其中 E
是边的数量,V
是节点数量,主要由最小堆操作决定。O(V^2)
普林姆算法和克里姆林算法还是有很大的差别的,普林姆算法需要起始点,然后将起始点相连最小的边入到边集当中。
假设这个图我们以a点为例,和a相连的边是b和h点,ab边明显小于ah边,所以我们选择ab边
这里引入了b点,所以我们的选择是bc边,bh边还有ah边,很明显这里我们选择bc边也可以,ah边也可以,所以我们就选择ah边吧。
这里我们可以选择的边种最小的是hg边。
接下来我们肯定选择gf,因为gf最小
最后可以推出最小生成树
Prim算法的思路很简单,实现过程我们还是用优先级队列,但是在选择的时候我们需要判断一下是否形成环。 代码展示:
W Prim(Self& minTree, const V& src)
{
//初始化
size_t srci = GetVertexIndex(src);
size_t n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(n);
for (auto& e : minTree._matrix)e.resize(n, MAX_W);
//选过的顶点
vector<bool> X(n, false);
vector<bool> Y(n, true);
X[srci] = true, Y[srci] = false;
//从X->Y集合中连接的边里面选出最小的边
priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
for (size_t i = 0;i < _vertexs.size();i++)
{
//把srci连接的边添加到队列中
if (_matrix[srci][i] != MAX_W)minq.push(Edge(srci, i, _matrix[srci][i]));
}
cout << "Prim开始选边" << endl;
size_t size = 0;
W totalW = W();
while (!minq.empty())
{
Edge min = minq.top();
minq.pop();
//如果最小边的目标点也在起点集合
if (X[min._dsti])
{
cout << "构成环:";
cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
}
else
{
minTree._AddEdge(min._srci, min._dsti, min._w);
cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
size++;
totalW += min._w;
if (size == n - 1) break;
//将dsti添加到X集合
X[min._dsti] = true;
Y[min._dsti] = false;
for (size_t i = 0;i < _vertexs.size();i++)
{
//将目标点连接的边添加到集合当中并且需要判断目标点是否已经在集合当中
if (_matrix[min._dsti][i] != MAX_W && X[i] == false)minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
}
}
}
if (size == n - 1)return totalW;
else return W();
}
通过本文的讲解,我们深入了解了图论中的三种经典算法:广度优先搜索(BFS)、深度优先搜索(DFS)和最小生成树(Kruskal算法和Prim算法)。这些算法在计算机科学、数据分析、人工智能等领域有着广泛的应用。
从简单的图遍历到复杂的网络优化问题,这些算法都展现了其强大的解决问题能力。然而,图论是一个庞大的领域,还有许多更深入、更复杂的算法等待我们去探索。例如,拓扑排序、强连通分量、最短路径问题等。
希望本文能为你打开图论算法的大门,激发你对算法学习的兴趣。在未来的学习中,我们可以继续深入研究图论算法,并将其应用到实际的项目中。