对图论进行简单的复习
图论算法模版
Dijkstra
Dijkstra的思想为(假设已完成初始化):
- 从未求得最短路径的顶点中找到一个离源点最近的点 t,确定其当前离源点的距离
dist[t]
为源点到该点的最短路径长 - 更新与 t 相邻的点到源点的最短距离
基础的Dijkstra采用邻接矩阵存图,假设共 n 个顶点,则上述操作需要进行 n 轮,每轮找点以及更新操作复杂度均为O(n),所以总时间复杂度为O(n2)
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
using i64 = long long;
using pii = pair<int, int>;
constexpr int INF = 0x3f3f3f3f;
constexpr int MOD = 1e9 + 7;
constexpr double eps = 1e-9;
constexpr int N = 1e5 + 7;
void solve()
{
int n, m;
cin >> n >> m; // n个点 m条边
vector g(n + 1, vector<int>(n + 1, INF));
for (int i = 0; i < m; i++)
{
int x, y, z;
cin >> x >> y >> z;
g[x][y] = min(g[x][y], z);
}
vector<int> dist(n + 1, INF);
vector<bool> st(n + 1, false);
auto Dijkstra = [&]() -> void
{
for (int k = 0; k < n; k++)
{
dist[1] = 0;
int t = 0;
for (int i = 1; i <= n; i++) // 找出一个 在所有还未确定最短路径的点中离原点最近的点
{
if (!st[i] && (!t || dist[i] < dist[t]))
{
t = i;
}
}
st[t] = true; // 确定原点到i点的最短路径已找到
for (int i = 1; i <= n; i++)
{
if (!st[i] && dist[t] + g[t][i] < dist[i]) // 更新与i点相连的点到原点的最短路径长
{
dist[i] = dist[t] + g[t][i];
}
}
}
};
Dijkstra();
if (dist[n] > INF / 2)
{
cout << -1 << endl;
}
else
cout << dist[n] << endl;
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int T = 1;
// cin >> T;
while (T--)
{
solve();
}
return 0;
}
堆优化的Dijkstra
堆优化的Dijkstra在王道的考研数据结构书中并没有详细介绍,但是提了一嘴有这个东西。这里简单写一下堆优化的版本,此处堆就不手写了,而是用STL中的priority_queue(省事好用)。下方Prim的堆优化版本与此类似。
既然每次只找一个离源点最近的点,那就不需要每次都遍历一轮n个点,可以每次更新距离后,将更新后的距离和被更新的点打包成一个二元组放入小根堆,这样每次找点就可以在小根堆中直接取到距离最小的点
堆优化后采用邻接表存图,每次找点为O(1),共n个点为O(n),更新距离为O(mlogn),其中log为小根堆所需,所以总时间复杂度为 O(n + mlogn)
int h[N], ne[N], ver[N], w[N];
int tot = 0;
void add(int x, int y, int z)
{
ver[++tot] = y, w[tot] = z, ne[tot] = h[x], h[x] = tot;
}
int n, m;
cin >> n >> m; // n个点 m条边
for (int i = 0; i < m; i++)
{
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
}
vector<int> dist(n + 1, INF);
vector<bool> st(n + 1, false);
auto Dijkstra = [&]() -> void
{
dist[1] = 0;
priority_queue<pii, vector<pii>, greater<pii>> q; // 小根堆
q.push({0, 1}); // 1为源点,初始1到1距离为零
while (!q.empty())
{
auto t = q.top(); // 每次取距离最近的点
q.pop();
int vertex = t.second, distance = t.first;
if (st[vertex]) continue; // 若该点最短路径已确定 则跳过
st[vertex] = true;
for (int i = h[vertex]; i; i = ne[i]) // 更新相邻点到源点的最短距离
{
int j = ver[i];
if (dist[vertex] + w[i] < dist[j])
{
dist[j] = dist[vertex] + w[i];
q.push({dist[j], j});
}
}
}
};
Floyd
Floyd的思想本质为动态规划
f[k][i][j]
数组含义:从1
到k
的节点作为中间经过的节点时,从i
到j
的最短路径长度。
状态转移方程f[k][i][j] = min(f[k-1][i][j] , f[k-1][i][k] + f[k-1][k][j])
,表示f[k][i][j]
①i
到j
的最短路径不经过k
节点;②经过k
节点。
根据动态规划,f[k]
只可能与f[k-1]
有关系,所以可以省略最外层的维度k
,直接带入循环求解。
显而易见,复杂度为O(n3)
void floyd() {
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
Prim
prim 算法采用的是一种贪心的策略。
每次将离连通部分最近的点和该点对应的边加入到连通部分,连通部分逐渐扩大,最后将整个图连通起来。
Prim类似于Dijkstra,复杂度为O(n2)
constexpr int INF = 0x3f3f3f3f;
int main()
{
int n, m;
cin >> n >> m; // 点数 边数
vector g(n + 1, vector<int>(n + 1, INF));
for (int i = 0; i < m; i++)
{
int u, v, w;
cin >> u >> v >> w;
g[u][v] = g[v][u] = min(g[u][v], w);
}
vector<int> dist(n + 1, INF);
vector<bool> st(n + 1, false); // 是否已经加入生成树
auto prim = [&]() {
int res = 0;
dist[1] = 0;
for (int i = 1; i <= n; i++)
{
int t = 0;
for (int j = 1; j <= n; j++) // 找出离连通部分的距离最近的点
{
if (!st[j] && (!t || dist[j] < dist[t]))
{
t = j;
}
}
if (dist[t] == INF) // 孤立点
{
cout << "No Minimum Spanning Tree" << endl;
return INF;
}
st[t] = true;
res += dist[t];
for (int j = 1; j <= n; j++)
{
if (g[t][j] < dist[j]) // 区别于最短路的该点到源点的距离最近
{
dist[j] = g[t][j]; // 最小生成树要求该点离连通部分的距离最近
// pre[j] = t; // 加一个数组用于记录传递关系 可以回溯树的生成路径
}
}
}
return res; // 最小生成树的边权之和
};
// ......
}
类似于Dijkstra,Prim也有堆优化版本,具体就不写了,可以参考上方的Dijkstra的堆优化。
用优先队列代替堆,优化的Prim算法时间复杂度O(mlogn)。适用于稀疏图,但是稀疏图的时候求最小生成树,Kruskal 算法更加实用。
Kruskal
- 将所有边按照权值的大小进行升序排序,然后从小到大分别判断。
- 如果当前边与之前选择的所有边不会组成回路,就选择这条边;反之,舍去。
使用并查集判断是否会产生回路。 - 直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。
共m条边,每次优先队列操作logm,并查集在路径压缩后查询操作的用时是O(1)级别的,总时间复杂度为 O(mlogm)
struct Edge {
int u, v;
int worth;
friend bool operator<(Edge a, Edge b) { // 优先队列的比较需要双形参
return a.worth > b.worth; // 对应优先队列就是小根堆
}
};
void main()
{
int n, m;
cin >> n >> m; // n个顶点 m条边
priority_queue<Edge> q;
for (int i = 0; i < m; i++)
{
int u, v, w;
cin >> u >> v >> w;
q.push({u, v, w});
}
vector<int> fa(n + 1); // 用于并查集
auto find = [&](auto &self, int x) -> int { // 并查集查找祖宗
if (fa[x] != x) return fa[x] = self(self, fa[x]); // 路径压缩
return x;
};
auto kruskal = [&]() {
int res = 0; // 最小生成树的边权之和
int num = 0; // 加入的边数
iota(fa.begin(), fa.end(), 0); // 初始 每个点的祖宗都是自己
while (!q.empty())
{
auto t = q.top();
q.pop();
int fu = find(find, t.u), fv = find(find, t.v);
if (fu == fv) continue; // 两点同属一个连通块 再连会形成回路
fa[fv] = fu; // 将两个连通块连通
res += t.worth;
++num;
}
if (num < n - 1) // 少于n-1条边 未能将n个点都连通
{
cout << "No Minimum Spanning Tree" << endl;
}
else cout << res << endl;
};
kruskal();
}
拓扑排序
- 一个有向图,如果图中有入度为 0 的点,就把这个点及与这个点所连的边都删掉。
- 一直进行上面的处理,如果所有点都能被删掉,则该图可以进行拓扑排序。
constexpr int N = 1e5 + 7;
int h[N], ne[N], ver[N];
int tot = 0;
void add(int u, int v)
{
ver[++tot] = v, ne[tot] = h[u], h[u] = tot;
}
void solve()
{
int n, m;
cin >> n >> m; // 顶点数 边数
vector<int> in(n + 1); // 入度
for (int i = 0; i < m; i++)
{
int x, y;
cin >> x >> y;
add(x, y);
in[y]++;
}
vector<int> q(n); // 滚动数组 存储拓扑序 同时模拟队列
auto topSort = [&]() -> bool {
int tail = -1, head = 0;
for (int i = 0; i < n; i++)
{
if (!in[i + 1]) // 所有初始入度为0的点加入队列
{
q[++tail] = i + 1;
}
}
while (head <= tail) // 队列非空
{
int t = q[head++]; // 队头取点
for (int i = h[t]; i; i = ne[i])
{
if (--in[ver[i]] == 0) // 将t点及从t出发的边删除后 所有入度为0的点加入队列
{
q[++tail] = ver[i];
}
}
}
return tail == n - 1; // 是否排序所有点
};
if (topSort())
{
for (int i = 0; i < n; i++)
{
cout << q[i] << ' ';
}
}
else cout << -1 << endl;
}
——分割一下 以上为考研学习(王道书所讲算法)
bellman - ford算法
Bellman - ford 算法是求含负权图的单源最短路径的一种算法。其原理为连续进行松弛,每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环
bellman - ford算法的具体步骤:
for n次
for 所有边 a,b,w (松弛操作) // a -> b 的边 权值为 w
dist[b] = min(dist[b], last[a] + w)
注意:last[] 数组是上一轮迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点
constexpr int N = 510, M = 10010;
struct Edge
{
int u, v, w;
}edges[M];
int n, m;
int dist[N];
int last[N];
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n; i++) // 这里的n可以换成其他数字,假设换成k,则最终求出的dist[]表示最多经过k条边的最短距离
{
memcpy(last, dist, sizeof dist); // last为dist数组的备份
for (int j = 0; j < m; j ++ )
{
auto e = edges[j];
dist[e.v] = min(dist[e.v], last[e.u] + e.w);
}
}
}
spfa(队列优化的Bellman-Ford算法)
int n;
int h[N], w[N], e[N], ne[N], idx;
int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N]; // 存储每个点是否在队列中
bool spfa() // 如果存在负环,则返回true,否则返回false。
{
// 不需要初始化dist数组
// 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
q.push(i);
st[i] = true;
}
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}