图的遍历
深度优先搜索DFS
从某个顶点v0
出发,访问此顶点,然后依次从v0
的未被访问的邻接点出发递归地进行同样的DFS,直
到图中所有与v0
有路径相同的顶点都被访问到,这样就完成了图中一个连通分量的遍历。
一个实例:
递归DFS
算法思路:
类似于树的先序遍历,首先visit(V)
并置已访问过标志,然后for(V的每个邻接点)
:递归地执行DFS。
代码实现:
//对邻接矩阵存储的图进行DFS,只遍历一个连通分量
void DFS_r_core(const Mat &m, int v, vector<int> &res, vector<bool> &visited) {
int N = m.size();
visited[v] = true;
res.push_back(v);
for (int i = 0; i < N; ++i) {
if (m[v][i] != X && m[v][i] != 0 && !visited[i]) {
DFS_r_core(m, i, res, visited);
}
}
}
vector<int> DFS_recursively(const Mat &m, int v) {
vector<bool> visited(m.size(), false);
vector<int> res;
DFS_r_core(m, v, res, visited);
return res;
}
迭代DFS
算法思路:
非递归遍历需要我们自己维护一个栈。
首先将首节点访问并压入栈,然后只要栈非空,就执行下面的循环体:
取栈顶元素t
,对于t
的每个邻接点,如果没有访问过,就访问,标记,入栈并break出for循环,转到下一次whiile循环遍历刚才入栈的那个节点。
如果for循环是从终止条件退出的,说明这个t
没有未访问过的邻接点,直接弹出即可,否则还保留在栈中。
代码实现:
vector<int> DFS_iteritively(const Mat &m, int v) {
int N = m.size();
stack<int> stk;
vector<bool> visited(N, false);
vector<int> res;
stk.push(v);
visited[v] = true, res.push_back(v);
int t;
while (!stk.empty()) {
t = stk.top();
int i = 0;
for (; i < N; ++i) {
if (m[t][i] != X && m[t][i] != 0 && !visited[i]) {
visited[i] = true, res.push_back(i); //访问
stk.push(i);
break;
}
}
if (i == N) {
stk.pop();
}
}
return res;
}
遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间取决于图的存储结构。
- 采用邻接矩阵存储时,时间复杂度为
O(V^2)
- 采用邻接表存储时,时间复杂度为
O(V+E)
广度优先搜索BFS
BFS类似于树的层序遍历过程,从v0
出发,在访问了v0
后依次访问v0
的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问他们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的邻接点”,直到图中的所有顶点都被访问到。
BFS遍历图的过程就是以v0
为起点,由近及远,依次访问和v0
有路径相通且路径长度为1,2,3…的顶点。
需要借助一个队列把访问过的顶点依次保存下来。
一个实例:
代码实现:
void BFS(const Mat &m, int v) {
queue<int> q;
vector<bool> visited(m.size(), false);
visited[v] = true;
cout << v << endl;
q.push(v);
while (!q.empty()) {
int t = q.front();
q.pop();
for (int i = 0; i < m.size(); ++i) {
if (!visited[i] && m[t][i] < X && m[t][i] != 0) {
visited[i] = true;
cout << i << endl;
q.push(i);
}
}
}
}
复杂度分析:
- 采用邻接矩阵存储时,时间复杂度为
O(V^2)
- 采用邻接表存储时,时间复杂度为
O(V+E)
最小生成树算法
什么是最小生成树:
一句话,最小生成树是图G的所有生成树{T}
中边的总权值最小的那棵树(好像废话)
最小生成树是不唯一的,只有当图中各个边的权值各不相等时,该图的最小生成树唯一。
Prim算法
算法思路:
代码实现:
//返回距离当前MST最近的顶点,若不存在返回-1
int find_closest_vertex(vector<int> &dist) {
int v = -1;
int mindist = X;
for (int i = 0; i < dist.size(); ++i) {
if (dist[i] != 0 && dist[i] < mindist) {
mindist = dist[i];
v = i;
}
}
return v;
}
/*
* 功能:求m的最小生成树,保存在mst中,并返回最小路径和
* 参数:m:待解图的邻接矩阵
* mst:传出的最小生成树,应保证与m的大小一致
* d:该图是否为有向图,若为无向图传false
* 返回值:成功返回最小路径和,若该图不连通,返回-1
* */
int Prim(const Mat &m, Mat &mst, bool d) {
int total_weight = 0;
int cnt = 0;
vector<int> dist(m[0]);
vector<int> parent(m.size(), 0);
dist[0] = 0;
parent[0] = -1;
cnt++;
while (1) {
int v = find_closest_vertex(dist);
if (v == -1)
break;
total_weight += dist[v];
mst[parent[v]][v] = dist[v];
if (!d)
mst[v][parent[v]] = dist[v];
dist[v] = 0;
cnt++;
for (int i = 0; i < m.size(); ++i) {
if (dist[i] != 0 && m[v][i] < X && m[v][i] < dist[i]) {
dist[i] = m[v][i];
parent[i] = v;
}
}
}
return cnt == m.size() ? total_weight : -1;
}
时间复杂度:O(V^2)
,不依赖于E
,故其适合求解稠密图。
Kruskal算法
算法思路:
伪码描述:
代码实现:
struct Edge {
int v1, v2, weight;
Edge(int v11, int v22, int ww)
: v1(v11), v2(v22), weight(ww) {
}
bool operator<(const Edge &e1) const {
return weight > e1.weight;
}
};
int Find(vector<int> &S, int x) {
if (S[x] < 0) {
return x;
} else {
return S[x] = Find(S, S[x]);
}
}
void Union(vector<int> &S, int x, int y) {
int r1 = Find(S, x);
int r2 = Find(S, y);
if (S[r1] < S[r2]) {
S[r2] = r1;
} else {
if (S[r1] == S[r2])
S[r2]--;
S[r1] = r2;
}
}
bool Check(vector<int> &S, int x, int y) {
return Find(S, x) == Find(S, y);
}
int Kruskal(const Mat &m, Mat &mst, bool d) {
int N = m.size();
mst = vector<vector<int>>(N, vector<int>(N, X));
for (int i = 0; i < N; ++i) {
mst[i][i] = 0;
}
priority_queue<Edge> edges;
vector<int> vset(N, -1);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
if (((!d && i < j) || d) && (m[i][j] != 0 && m[i][j] != X)) {
edges.push(Edge(i, j, m[i][j]));
}
}
}
int ecnt = 0;
int total_weight = 0;
while (ecnt < N - 1 && edges.size() > 0) {
Edge e = edges.top();
edges.pop();
if (!Check(vset, e.v1, e.v2)) {
mst[e.v1][e.v2] = e.weight;
total_weight += e.weight;
ecnt++;
if (!d) {
mst[e.v2][e.v1] = e.weight;
}
Union(vset, e.v1, e.v2);
}
}
return ecnt == N - 1 ? total_weight : -1;
}
主要是借助最小堆和并查集完成寻找最小边和判断是否形成回路的问题。
最短路径算法
Dijkstra算法
算法思路:
代码实现:
int find_closest_vertex2(vector<int> &dist, vector<bool> &collected) {
int v = 0;
int mindist = X;
for (int i = 0; i < dist.size(); ++i) {
if (!collected[i] && dist[i] < mindist) {
mindist = dist[i];
v = i;
}
}
return mindist == X ? -1 : v;
}
/*
* 功能:求图m中以start为源点的最短路径
* 参数:m:图的邻接矩阵
* dist:传出参数,记录start到各个顶点的最短距离
* path:传出参数,path[i]表达到达i的上一步节点
* 返回值:该图是否能求得最短路径
* */
bool Dijkstra(const Mat &m, vector<int> &dist, vector<int> &path, int start) {
int N = m.size();
vector<bool> collected(N, false);
dist = vector<int>(m[start]); //start到各个顶点的距离
path = vector<int>(N, -1); //记录路径
for (int i = 0; i < N; ++i) {
if (m[start][i] != 0 && m[start][i] < X) {
path[i] = start;
}
}
dist[start] = 0;
collected[start] = true;
while (1) {
int t = find_closest_vertex2(dist, collected);
if (t == -1)
break;
collected[t] = true;
for (int i = 0; i < N; ++i) {
if (!collected[i] && m[t][i] < X) {
if (m[t][i] < 0)
return false;
if (dist[t] + m[t][i] < dist[i]) {
dist[i] = dist[t] + m[t][i];
path[i] = t;
}
}
}
}
return true;
}
时间复杂度:O(V^2)
Floyd算法
算法思路:
代码实现:
bool Floyd(const Mat &m, Mat &path, Mat &dist) {
int N = m.size();
dist = Mat(m);
path = vector<vector<int>>(N, vector<int>(N, -1));
for (int k = 0; k < N; ++k) {
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
if (m[i][j] < 0 && i == j) {
return false;
}
if (dist[i][k] + dist[k][j] < dist[i][j]) { //注意这里可能溢出
dist[i][j] = dist[i][k] + dist[k][j];
path[i][j] = k;
}
}
}
}
}
floyd
算法求出的是每一对顶点之间的最短路径,时间复杂度为固定的O(V^3)
拓扑排序
算法思路:
- 首先遍历图得到所有节点的入度到in_degree
- 然后遍历in_degree数组,将所有入度为0的节点压入队列q
- 然后
while (!q.empty)
弹出队头元素t加入拓扑序列,再将每一个被t
指向的顶点的入度-1,若减为0则压入队列q - 返回cnt == N
代码实现:
/*
* 功能:对有向图m进行拓扑排序,拓扑序列存储在top_order中
* 参数:m:待求解的有向图的邻接矩阵
* top_order:结果序列,传出参数
* 返回值:当前图是否有拓扑序列(如果有环则不存在拓扑序列,返回false)
* */
bool topological_sort(const Mat &m, vector<int> &top_order) {
int N = m.size();
vector<int> in_degree(N, 0);
top_order = vector<int>(N, -1);
//统计所有节点的入度
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
if (m[i][j] != 0 && m[i][j] != X) {
in_degree[j]++;
}
}
}
//将所有入度为0的节点压入队列q
queue<int> q;
for (int i = 0; i < N; ++i) {
if (in_degree[i] == 0) {
q.push(i);
}
}
int cnt = 0;
while (!q.empty()) {
int t = q.front();
q.pop();
top_order[cnt++] = t; //取出队头元素t加入拓扑序列
for (int i = 0; i < N; ++i) {
if (m[t][i] != 0 && m[t][i] != X) {
in_degree[i]--; //对于每个被t指向的节点,入读--;如果被减为0,加入队列q
if (in_degree[i] == 0) {
q.push(i);
}
}
}
}
return cnt == N; //若没有把所有节点都统计在内,说明有环,无拓扑序列
}