【方便复制】
请结合PPT及讲解达到最佳的复习效果。
以下代码基于无向图。
邻接矩阵转换成邻接表
输入格式
第一行包含两个整数 n n n 和 m m m。
接下来 m m m 行,每行包含三个整数 u u u, v v v, w w w,表示点 u u u 和点 v v v 之间存在一条权值为 w w w 的边。
输入
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出
邻接矩阵表示法:
- 1 2 3
1 - 2 -
2 2 - 4
3 - 4 -
邻接表表示法(括号内为权值):
与1相连的边有:4(3) 3(2) 2(1)
与2相连的边有:3(2) 1(1)
与3相连的边有:4(4) 2(2) 1(2)
与4相连的边有:3(4) 1(3)
c++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010, INF = 0x3f3f3f3f;
int n, m;
int g[N][N]; // 邻接矩阵
struct Node
{
int id; // 编号
int w; // 权值
Node* next;
Node(int _id, int _w) : id(_id), w(_w), next(NULL){}
} *head[N]; // 邻接表
void add(int a, int b, int w)
{
Node* p = new Node(b, w);
p->next = head[a];
head[a] = p;
}
void convert() // 邻接矩阵转换成邻接表
{
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (g[i][j] < INF / 2)
add(i, j, g[i][j]);
}
int main()
{
memset(g, 0x3f, sizeof g);
cin >> n >> m;
while (m -- )
{
int a, b, w;
cin >> a >> b >> w;
g[a][b] = g[b][a] = min(g[a][b], w);
}
puts("邻接矩阵表示法:");
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= n; j ++ )
if (g[i][j] > INF / 2) cout << '-' << ' ';
else cout << g[i][j] << ' ';
cout << endl;
}
convert();
puts("\n邻接表表示法(括号内为权值):");
for (int i = 1; i <= n; i ++ )
{
printf("与%d相连的边有:", i);
for (auto p = head[i]; p; p = p->next)
printf("%d(%d) ", p->id, p->w);
cout << endl;
}
return 0;
}
邻接表转换成邻接矩阵
输入格式
第一行包含两个整数 n n n 和 m m m。
接下来 m m m 行,每行包含三个整数 u u u, v v v, w w w,表示点 u u u 和点 v v v 之间存在一条权值为 w w w 的边。
输入
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出
邻接表表示法(括号内为权值):
与1相连的边有:4(3) 3(2) 2(1)
与2相连的边有:3(2) 1(1)
与3相连的边有:4(4) 2(2) 1(2)
与4相连的边有:3(4) 1(3)
邻接矩阵表示法:
- 1 2 3
1 - 2 -
2 2 - 4
3 - 4 -
c++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010, INF = 0x3f3f3f3f;
int n, m;
int g[N][N]; // 邻接矩阵
struct Node
{
int id; // 编号
int w; // 权值
Node* next;
Node(int _id, int _w) : id(_id), w(_w), next(NULL){}
} *head[N]; // 邻接表
void add(int a, int b, int w)
{
Node* p = new Node(b, w);
p->next = head[a];
head[a] = p;
}
void convert() // 邻接表转换成邻接矩阵
{
for (int i = 1; i <= n; i ++ )
{
for (auto p = head[i]; p; p = p->next)
{
int j = p->id; // 与i相邻的点的编号
g[i][j] = g[j][i] = p->w;
}
}
}
int main()
{
memset(g, 0x3f, sizeof g);
cin >> n >> m;
while (m -- )
{
int a, b, w;
cin >> a >> b >> w;
// 假设没有重边
add(a, b, w);
add(b, a, w);
}
puts("邻接表表示法(括号内为权值):");
for (int i = 1; i <= n; i ++ )
{
printf("与%d相连的边有:", i);
for (auto p = head[i]; p; p = p->next)
printf("%d(%d) ", p->id, p->w);
cout << endl;
}
convert();
puts("\n邻接矩阵表示法:");
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= n; j ++ )
if (g[i][j] > INF / 2) cout << '-' << ' ';
else cout << g[i][j] << ' ';
cout << endl;
}
return 0;
}
带权值的用邻接表存的有向图的dfs
输入:
4 5
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 100010, INF = 0x3f3f3f3f;
int g[N][N];
bool st[N];
struct Node
{
int id, w;
Node* next;
Node(int _id, int _w) : id(_id), w(_w), next(NULL) {}
}* head[N];
void add(int a, int b, int w)
{
Node* p = new Node(b, w);
p->next = head[a];
head[a] = p;
}
void dfs(int u)
{
st[u] = true;
cout << u << ' ';
for (Node* p = head[u]; p; p = p->next)
if (!st[p->id])
dfs(p->id);
}
int main()
{
memset(g, 0x3f, sizeof g);
int n, m;
cin >> n >> m;
while (m -- )
{
int a, b, w;
cin >> a >> b >> w;
add(a, b, w);
// g[a][b] = w;
}
for (int i = 1; i <= n; i ++)
if (!st[i])
dfs(i);
return 0;
}
DFS 邻接表
5 5
1 2
1 3
1 4
2 3
3 4
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510;
bool st[N];
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL) {}
}* head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a];
head[a] = p;
}
void dfs(int u)
{
st[u] = true;
cout << u << ' ';
for (Node* p = head[u]; p; p = p->next)
if (!st[p->id])
dfs(p->id);
}
int main()
{
int n, m;
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
// g[a][b] = w;
}
for (int i = 1; i <= n; i ++)
{
printf("%d: ", i);
for (Node* p = head[i]; p; p = p->next)
printf("%d -> ", p->id);
puts("NULL");
}
puts("-------------------");
for (int i = 1; i <= n; i ++)
if (!st[i])
dfs(i);
return 0;
}
DFS 邻接矩阵
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510;
int n, m;
int g[N][N];
bool st[N];
void dfs(int u)
{
st[u] = true;
cout << u << ' ';
for (int i = 1; i <= n; i ++)
if (g[u][i] && !st[i])
dfs(i);
}
int main()
{
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
g[a][b] = 1;
}
for (int i = 1; i <= n; i ++)
{
for (int j = 1; j <= n; j ++)
cout << g[i][j] << ' ';
cout << endl;
}
puts("----------------");
for (int i = 1; i <= n; i ++)
if (!st[i])
dfs(i);
return 0;
}
DFS非递归
#include <iostream>
#include <cstring>
#include <stack>
using namespace std;
const int N = 510;
int n, m;
bool st[N];
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL) {}
}* head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a];
head[a] = p;
}
void dfs(int u)
{
stack<int> s;
s.push(u);
st[u] = true;
while (s.size())
{
int t = s.top();
s.pop();
cout << t << ' ';
for (Node *p = head[t]; p; p = p->next)
{
int j = p->id;
if (!st[j])
{
st[j] = true;
s.push(j);
}
}
}
}
int main()
{
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
}
for (int i = 1; i <= n; i ++)
if (!st[i])
dfs(i);
return 0;
}
BFS
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 510;
int n, m;
bool st[N];
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL) {}
}* head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a];
head[a] = p;
}
void bfs(int u)
{
queue<int> q;
q.push(u);
st[u] = true;
while (q.size())
{
int t = q.front();
q.pop();
cout << t << ' ';
for (Node *p = head[t]; p; p = p->next)
{
int j = p->id;
if (!st[j])
{
st[j] = true;
q.push(j);
}
}
}
}
int main()
{
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
}
for (int i = 1; i <= n; i ++)
if (!st[i])
bfs(i);
return 0;
}
求拓扑排序
基于BFS
求拓扑排序的基本思路是:
-
如果图中含有回路,则说明该图不具有拓扑排序,直接返回失败。可以用 Kahn 算法检测是否含有回路。
-
选取入度为 0 的节点,该节点可以第一步输出。
-
输出该节点后,将它的邻接节点入度都减 1。
-
不断重复选择入度为 0 的节点输出,并更新其他节点入度。
-
若最终输出的节点数与总节点数相同,说明找出了一个拓扑序列。
具体算法如下:
-
输入图,统计每个节点的入度,建立入度表。
-
使用队列存储入度为 0 的节点。
-
循环检测队列是否为空:
- 不空,出队首节点 u ,输出 - 并更新u的相邻节点v的入度值 - 如果入度变为0,加入队尾
-
检查是否输出的节点数等于总节点数,是则找到拓扑序列,否则图含回路。
-
返回拓扑排序结果
主要思路是利用入度代表节点的前驱关系,入度为0表示无前驱可输出,更新入度后继续输出下一个节点。
重复此过程找出符合前后关系的拓扑序列。
需要注意的是在更新入度时判断是否产生新的入度为0的节点加入队列,实现层层递推输出关系。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m, q[N];
int d[N]; // 每个点的入度
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL){}
} *head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a]; // 头插法
head[a] = p;
}
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++)
if (!d[i]) q[++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++];
// 在结点t的所有邻边中找所有入度为0的点,然后将其入队
for (Node* p = head[t]; p; p = p->next)
{
int j = p->id; // t的临边的编号
if (-- d[j] == 0)
q[++ tt] = j;
}
}
return tt == n - 1;
}
int main()
{
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
d[b] ++;
add(a, b);
}
if (topsort())
{
for (int i = 0; i < n; i ++) cout << q[i] << ' ';
cout << endl;
}
else puts("-1");
return 0;
}
基于DFS
深度优先遍历判断图中拓扑排序(是否无环)的算法如下:
-
选择一个起始节点,将其标记为”正在访问”状态。
-
检查当前节点的所有相邻节点:
* 如果相邻节点为”未访问”状态,则递归地进行深度优先搜索这个节点 -
如果在搜索过程中发现这个节点已经被标记为”正在访问”,则说明存在环路,返回false
-
如果相邻节点状态为”已访问”,忽略该节点
-
所有相邻节点均处理完成后,将当前节点标记为”已访问”状态,并返回上层递归
-
如果整个图都搜索完,没有返回false,则说明图中不存在环路,返回true。
主要思路是:
-
使用”未访问”、”正在访问”和”已访问”三种节点状态进行标记
-
如果搜索某个节点时发现其状态为”正在访问”,则说明从此节点开始一定存在一个环路
-
整个深度优先搜索结束没有返回false,则该图不存在环路
通过遍历每个节点及其相邻节点,并检查其访问状态就可以判断是否存在环路。时间复杂度为O(V+E),空间复杂度为O(V)。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m, q[N], top;
int st[N]; // 0表示没有遍历过,1表示在递归当中,2表示已经遍历完了
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL){}
} *head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a]; // 头插法
head[a] = p;
}
bool dfs(int u) // 如果有环的话返回false
{
st[u] = 1; // 标记当前点在递归中
for (Node* p = head[u]; p; p = p->next)
{
int j = p->id;
if (!st[j]) // 如果没有搜过的话(0)就直接搜
{
if (!dfs(j)) return false; // 如果搜的过程中出现了环,直接false
}
else if (st[j] == 1) return false; // 否则如果发现这个点在递归当中,直接false
}
q[top ++] = u;
st[u] = 2; // 标记当前点已经遍历完了
return true;
}
// 基于深搜的话要遍历所有点,因为这个图不一定是联通的
bool topsort()
{
for (int i = 1; i <= n; i ++)
if (!st[i] && !dfs(i)) // 如果没遍历的话就遍历它,如果发现有环的话直接false
return false;
return true;
}
int main()
{
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
}
if (topsort())
{
// 因为得到的是拓扑排序的逆序序列,所以要倒序输出
for (int i = n - 1; i >= 0; i -- ) cout << q[i] << ' ';
cout << endl;
}
else puts("-1");
return 0;
}
BFS:
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 510;
int n, m;
bool st[N];
int ss = 1, tt = 2; // s起点,t终点
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL) {}
}* head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a];
head[a] = p;
}
void bfs(int u)
{
queue<int> q;
q.push(u);
st[u] = true;
while (q.size())
{
int t = q.front();
q.pop();
if (t == tt)
{
printf("存在%d-%d的路径\n", ss, tt);
return;
}
for (Node *p = head[t]; p; p = p->next)
{
int j = p->id;
if (!st[j])
{
st[j] = true;
q.push(j);
}
}
}
}
int main()
{
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
}
bfs(ss);
return 0;
}
DFS:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510;
bool st[N];
int s = 1, t = 2; // s起点,t终点
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL) {}
}* head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a];
head[a] = p;
}
void dfs(int u)
{
st[u] = true;
if (u == t) // 若遍历到了终点
{
printf("存在%d-%d的路径", s, t);
return;
}
for (Node* p = head[u]; p; p = p->next)
if (!st[p->id])
dfs(p->id);
}
int main()
{
int n, m;
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
}
for (int i = 1; i <= n; i ++)
{
printf("%d: ", i);
for (Node* p = head[i]; p; p = p->next)
printf("%d -> ", p->id);
puts("NULL");
}
puts("-------------------");
dfs(s);
return 0;
}
问题描述
假设图用邻接表表示,设计一个算法,输出从顶点 i i i 到顶点 j j j 的所有简单路径
输入格式
第一行包含三个整数 n n n , m m m 和 k k k。
接下来 m m m 行,每行包含两个整数 x x x 和 y y y,表示存在一条从点 x x x 到点 y y y 的有向边 ( x x x, y y y)。
接下来 k k k 行,每行包含两个整数 s s s 和 t t t,表示要求输出顶点 s s s 到顶点 t t t 的所有简单路径。
输入
6 8 3
1 2
1 3
2 4
3 4
4 5
5 3
2 7
7 4
1 4
2 5
1 5
输出
1 -> 4 的所有简单路径:
1 3 4
1 2 7 4
1 2 4
2 -> 5 的所有简单路径:
2 7 4 5
2 4 5
1 -> 5 的所有简单路径:
1 3 4 5
1 2 7 4 5
1 2 4 5
c++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m, k;
bool st[N];
vector<int> path;
struct Node
{
int id; // 编号
int w; // 权值
Node* next;
Node(int _id) : id(_id), next(NULL){}
} *head[N]; // 邻接表
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a];
head[a] = p;
}
void dfs(int s, int t)
{
if (s == t)
{
for (auto x : path) printf("%d ", x);
puts("");
return ;
}
for (auto p = head[s]; p; p = p->next)
{
int j = p->id;
if (!st[j])
{
st[j] = true;
path.push_back(j);
dfs(j, t);
path.pop_back();
st[j] = false;
}
}
}
int main()
{
cin >> n >> m >> k;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
}
while (k --)
{
int s, t;
cin >> s >> t;
printf("\n%d -> %d 的所有简单路径:\n", s, t);
path.push_back(s);
st[s] = true;
dfs(s, t); // 输出顶点s到顶点t的所有简单路径
st[s] = false;
path.pop_back();
}
return 0;
}
算法思想
这个代码输出从顶点i到顶点j的所有简单路径的算法思想如下:
-
用邻接表表示图,构建邻接表头head,每个节点存储下一个邻接节点指针。
-
深度优先搜索dfs函数,递归搜索各条路径。
-
path向量存储当前路径,st数组标记每个节点是否访问过。
-
dfs函数入口是s节点,终止条件是找到t节点。
-
对s节点的每一个邻接节点p递归搜索:
- 将p加入path并标记为已访问 - 继续递归dfs搜索下一个节点 - 返回后删除p,恢复未访问状态
-
每经过一个节点并回溯,输出当前path向量中的路径,即找到一条从s到t的路径。
-
重复5-6步骤搜索s节点的所有可能路径,输出所有简单路径。
所以这个算法是使用深度优先搜索和回溯方法,结合path向量和st访问标记数组来记录和输出所有可能的路径,得到从初始节点到目标节点的所有简单路径。
PRIM算法(Prim’s algorithm)是一个用于计算加权无向图的最小生成树(MST)的贪心算法。
算法步骤:
-
从图中选择任意一个顶点作为第一个最小生成树中的顶点。
-
不断地添加图中与当前最小生成树顶点之间边权最小的边,直至所有的顶点都连通为止。
-
每次添加新边之前,都要检查该边是否会形成环路,如果会形成环路则丢弃该边,选择下一条边权最小的边。
这个算法的思路是:
- 从一个顶点开始构建最小生成树
- 每次都选择与当前最小生成树间边权最小的边来扩展树的顶点
- 重复这个过程直到所有的顶点都被包含在树中
具体步骤实现较简单,只需要使用优先队列或堆来维护候选边的边权,每次取出队首的边就可以找到下一步要添加的边。PRIM算法是一个非常经典且高效的最小生成树算法。
朴素版Prim()算法
时间复杂度: O ( n 2 ) O(n^2) O(n2)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010, INF = 0x3f3f3f3f;
int n, m; // n 为点数,m 为边数
int g[N][N]; // 稠密图用邻接矩阵存
int dist[N]; // 连通块外每个点到连通块内的最短距离
bool st[N]; // 当前每一个点是否已经在连通块内
int prim() {
memset(dist, 0x3f, sizeof dist); // dist 初始化为正无穷
dist[1] = 0; // 将第一个点距离设为0,表示第一个点加入集合
int res = 0; // 答案表示当前边权和
for (int i = 0 ; i < n ; i ++) { // 一共要加 n 个点
int t = -1; // t 表示当前边权最小的点
for (int j = 1 ; j <= n ; j ++) { // 枚举所有点
// 如果((当前这个点还没有在集合中)并且(t还没有选过其他点或者t选择的点没有当前点好))
if (!st[j] && (t == -1 || dist[j] < dist[t]))
t = j; // 更新成当前点
}
// 如果距离当前集合最小的点的长度都是正无穷的话,则不连通,无解,直接返回正无穷
if (dist[t] == INF) return INF;
// 否则的话标记当前点,表示当前点已经在集合中了
st[t] = true;
res += dist[t]; // 每条边的长度需要加到res中
// 更新其他所有点
for (int j = 1 ; j <= n ; j ++)
dist[j] = min(dist[j], g[t][j]);
}
return res;
}
int main() {
memset(g, 0x3f, sizeof g); // 将所有边初始化为正无穷
cin >> n >> m;
while (m --) {
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c); // 存在自环和重边
}
int res = prim();
if (res == INF) puts("impossible");
else cout << res << endl;
return 0;
}
Kruskal算法
克鲁斯卡尔算法步骤如下:
-
将所有边根据权值从小到大进行排序。如果权值相同,则以任意但一致的顺序进行排序。
-
初始化一棵森林F(即一组树),其中每个图中的节点都是一个单独的树。
-
取排序后的第一条边。如果该边连接森林F中的不同树,则将该边添加到森林中。如果形成环路,则丢弃该边。
-
继续取排序列表中的下一条边。只有当该边不形成环路时,才将其添加到F中。
-
算法在F成为一个包括所有节点的单一树时终止。这将发生在森林中的边数达到 |V|-1时,其中|V|是图中节点数。
-
结果森林F就是最小生成树。
克鲁斯卡尔算法的主要特点:
-
只要边权满足三角不等式,它就能产生最小生成树。
-
时间复杂度是O(ElogE),E是图中的边数。
-
它是贪心算法,因为它总是选择不会形成环路的最小权值边。
时间复杂度: O ( m l o g m ) O(mlogm) O(mlogm)
#include <iostream>
#include <algorithm>
using namespace std;
// N是一个较大的数,用来容纳各个节点
// M是边的数量上限
// INF是一个很大的数,用于表示无限
const int N = 510, M = 100010, INF = 0x3f3f3f3f;
// n是节点数量,m是边的数量
// p是并查集数组
int n, m, p[N];
// Edge结构体存储每条边的端点和权值
// 重载<运算符用于排序边
struct Edge {
int a, b, c;
bool operator< (const Edge& t) const {
return c < t.c;
}
} e[M];
// 并查集查找函数,路径压缩
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
// 读取节点和边的数量
cin >> n >> m;
// 读取每条边的信息
for (int i = 0 ; i < m ; i ++)
cin >> e[i].a >> e[i].b >> e[i].c;
// 初始化并查集数组
for (int i = 1 ; i <= n ; i ++)
p[i] = i;
// 对边进行升序排序
sort(e, e + m);
// res记录结果,cnt记录生成树的边数
int res = 0, cnt = 0;
for (int i = 0 ; i < m ; i ++) {
// a,b是这条边的两个端点,c是权值
int a = e[i].a, b = e[i].b, c = e[i].c;
// 如果两个端点不在同一个集合,加入生成树
if (find(a) != find(b)) {
res += c;
// 使用联通操作联通两个集合
p[find(a)] = find(b);
cnt ++;
}
}
// 输出结果
if (cnt == n - 1)
cout << res << endl;
else
puts("impossible");
return 0;
}
PRIM算法与Kruskal算法相比,主要优势有:
-
PRIM算法可以保证生成的最小生成树是连通的。它是从一个顶点开始逐步扩张生成树,因此生成的最小生成树一定是连通的。而Kruskal算法可能生成非连通子图,需要额外判断。
-
PRIM算法适用于任何加权无向图,而Kruskal算法需要图中所有边的边权进行排序,不适用于动态图或边权可能重复的情况。
-
PRIM算法在多数实现中时间复杂度略优于Kruskal算法。PRIM算法常用的优先队列实现时间复杂度是O(ElogV),而Kruskal算法需要对所有边进行排序,时间复杂度一般是O(ElogE)。
-
PRIM算法的实现相对简单,使用优先队列维护候选边即可。Kruskal需要实现排好序的边集合合并以判断是否形成环路,实现上较为复杂。
-
PRIM算法空间复杂度通常比Kruskal算法低,因为不需要维护所有边的列表。
所以总体来说,PRIM算法生成的最小生成树一定是连通的,时间和空间复杂度优于Kruskal算法。但Kruskal算法在理解和证明上略更简单。两种算法都很经典,选择时需要结合具体情况。
Dijkstra算法是用于在加权有向图中求单源最短路径的一种算法。
Dijkstra算法的基本步骤:
- 初始化:将源节点s到自己的距离设为0,其他节点到s的距离都设为无限大。
- 选择距离源节点最近的节点u,将它从候选列表中删除。
- 修松u节点的邻接边:对u的每个邻节点v,计算u到v的距离之和,如果小于以前的距离,就更新v节点的距离值,同时在候选列表中移除旧的距离记录,加入新的记录。
- 重复步骤2和3,直到候选列表为空,算法终止。此时算法得到的就是单源最短路径树。
时间复杂度:
- 朴素版Dijkstra:
O
(
N
2
)
O(N^2)
O(N2) 通常用于稠密图
- 堆优化版Dijkstra: O ( M l o g N ) O(MlogN) O(MlogN) 通常用于稀疏图
- N为点数,M为边数
Dijkstra算法适用于边权非负的单源最短路径问题。它采用贪心策略,每次选择期望使总距离最小的节点入树,从而逐步求出最优解。是这一问题的典型算法之一。
输入样例
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例
3
朴素版dijkstra:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, null = 0x3f3f3f3f;
int n, m;
int g[N][N]; // 存储每条边(用邻接矩阵存,因为是稠密图)
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, null, sizeof dist);
dist[1] = 0;
for (int i = 1 ; i <= n ; i ++)
{
// 在还未确定最短路的点中,寻找距离最小的点
int t = -1;
for (int j = 1 ; j <= n ; j ++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[t] = true;
// 用t更新其他点的距离
for (int j = 1 ; j <= n ; j ++)
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
return dist[n] == null ? -1 : dist[n];
}
int main()
{
memset(g, null, sizeof g);
cin >> n >> m;
for (int i = 0 ; i < m ; i ++)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
cout << dijkstra() << endl;
return 0;
}
堆优化版:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int N = 150010, null = 0x3f3f3f3f;
int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool st[N];
typedef pair<int, int> PII;
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
int dijkstra()
{
memset(dist, null, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; // 小顶堆
heap.push({0, 1}); // first 存储距离,second 存储节点编号
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver] ; i != -1 ; i = ne[i])
{
int j = e[i];
if (distance + w[i] < dist[j])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
return dist[n] == null ? -1 : dist[n];
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 1 ; i <= m ; i ++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 10010, INF = 0x3f3f3f3f; //定义常量
struct Edge{ //结构体定义边
int a, b, c;
} edges[M];
int n, m, k; //n个点,m条边,k次迭代
int dist[N], last[N]; //dist存储单源最短路径长度,last存储上一次迭代的值
void bellman_ford(){ //Bellman-Ford算法主函数
memset(dist, 0x3f, sizeof dist); // dist初始化为无穷大
dist[1] = 0; //源点路径长度设置为0
for (int i = 0; i < k; i ++) { //k次迭代
memcpy(last, dist, sizeof dist); //last记录本次迭代前的值
for (int j = 0; j < m; j ++) {//枚举每条边
Edge e = edges[j]; //取出一条边
//松弛操作,更新dist值
dist[e.b] = min(dist[e.b], last[e.a] + e.c);
}
}
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < m; i ++)
{
int a, b, c;
cin >> a >> b >> c;
edges[i] = {a, b, c};
}
bellman_ford();
if (dist[n] > INF / 2) puts("impossible");
else cout << dist[n] << endl;
return 0;
}
Floyd
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f;
int n, m, k, d[N][N];
// 这段代码实现了Floyd算法来求解有向图或无向图所有点对之间的最短距离。
void floyd() {
// 对中间顶点k从1遍历到n
for (int k = 1; k <= n; k ++)
// 对起始顶点i从1遍历到n
for (int i = 1; i <= n; i ++)
// 对终点顶点j从1遍历到n
for (int j = 1; j <= n; j ++)
// 对于每个点对<i,j>,计算经过每个中间顶点k的路径长度之和
// 并与当前直接路径长度取最小值更新到d[i][j]中
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
//这段Floyd算法计算了所有的点对<i, j>之间通过中间顶点k的最短距离,时间复杂度为O(n^3)。
int main() {
memset(d, 0x3f, sizeof d);
cin >> n >> m >> k;
while (m --) {
int a, b, c;
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c);
}
for (int i = 1; i <= n; i ++) d[i][i] = 0;
floyd();
while (k --) {
int a, b;
cin >> a >> b;
if (d[a][b] > INF / 2) puts("impossible");
else cout << d[a][b] << endl;
}
return 0;
}