未命名文件-1

图的简介

简介

图论是数学的一个分支,图是由若干个点和边所组成的集合。点代表事物,而边代表事物之间的关系。

概念

图的构成

图是一个二元组 \(G=(V,E)\)\(V\) 是一个非空集合,表示点,\(E\) 表示边。图分为有向图和无向图,其实就是边有没有方向的区别。边上是可以有权值的,有权值的图成为有权图。

  • 在有向图中,一个点的前驱结点的数量就是这个结点的入度 \(d^-(v)\),后继结点的数量就是这个结点的出度,记作 \(d^+(v)\),这个结点的度就是入度和出度的和,记作 \(d(v)\) \((d(v)=d^+(v)+d^-(v))\)
  • 在无向图中,一个结点的邻点数量就是这个结点的度,记作 \(d(v)\)

对于一个有向图:

\[\sum_{v\in G}d^+(v)=\sum_{v\in G}d^-(v)=|E| \]

对于一个结点,不同度数的情况也不一样:

度数 名称 英文名称
\(d(v)=0\) 孤立点 isolated vertex
\(d(v)=1\) 叶子结点 leaf vertex
\(2\mid d(v)\) 偶点 even vertex
\(2\nmid d(v)\) 奇点 odd vertex
$d(v)= V -1$

一个图当中的所有结点的度的最大值记作 \(\Delta(G)\),所有结点的度的最小值记作 \(\delta(G)\)。说人话就是 \(\Delta(G)=\max\limits_{v\in G}d(v)\)\(\delta(G)=\min\limits_{v\in G}d(v)\)

简单图

  • 自环:如果有一条边 \((u,v)\),其中 \(u=v\),那么 \((u,v)\) 就是一个自环;
  • 重边:如果有两条边完全相等,就称作重边;
  • 简单图:如果一个图里面没有自环和重边,这个图就被称作为简单图。

存图

直接存边

直接存下集合 \(E\),这样空间牺牲较小。但是牺牲了很大的访问速度。可以用一个数组 \(e\) 表示每一条边,第 \(i\) 条边为 \(e_i\),那么假如第 \(i\) 条边接通了 \(u\)\(v\),权值为 \(w\)\(e_i=(u,v,w)\)

邻接矩阵

可以使用一个 \(|V|\times|V|\) 的二维数组 \(e\),对于 \(u\)\(v\) 两个点,如果他们之间有边的话,\(e_{(u,v)}=1\)。如果是一张带权图的话,那么 \(e_{(u,v)}=w\);反之若 \(u\)\(v\) 不连通,\(e_{(u,v)}=\infin\)。空间可能会过大。

邻接表

可以使用 \(|V|\) 个动态数组,第 \(i\) 个动态数组表示为 \(i\) 的邻边,那么如果 \(u\)\(v\) 有边,就直接将 \(v\) 存到第 \(u\) 个动态数组里面就行了。这种存图方法理论上来讲是最为优秀的,但是不可以 \(\mathcal O(1)\) 判断任意两点是否有边。

邻接表还有链表优化版本,叫做链式前向星,感兴趣可以自己百度,再次不做阐述。

复杂度对比

假设点数为 \(n\),边数为 \(m\)\(i\) 的邻点数量为 \(f(i)\)

存图方法 空间复杂度 判断任意两点是否有边 遍历任意点的邻边 遍历整张图
直接存边 \(\mathcal O(m)\) \(\mathcal O(m)\) \(\mathcal O(m)\) \(\mathcal O(nm)\)
邻接矩阵 \(\mathcal O(n^2)\) \(\mathcal O(1)\) \(\mathcal O(n)\) \(\mathcal O(n^2)\)
邻接表 \(\mathcal O(m)\) \(\mathcal O(f(i))\) \(\mathcal O(f(i))\) \(\mathcal O(n+m)\)

算法

最短路

Bellman-Ford

松弛操作是求最短路的基础。Bellman-Ford 是一种可以求单源最短路径的算法,基于松弛操作。

我们可以想到,假设 \(d(i)\) 表示源点到 \(i\) 的最短路径,\(e(u,v)\) 表示 \(u\)\(v\) 的路径长度,那么 \(d(v)=d(i)+e(i,v)\),也就是两条最短路径组成了一条长的最短路径。反过来想,若 \(d(i,j)\) 的是 \(i\)\(j\) 的最短路径,那么我们取这条路径上面的任意一点 \(k\)\(d(i,k)\)\(d(k,j)\) 都是最短路径。

Bellman-Ford 就是基于这种思想,因为最短路径的长度最大为 \(n-1\),那么我们就循环 \(n\) 次,然后枚举所有的边,进行松弛操作。这种算法的时间复杂度是 \(\mathcal O(nm)\) 的。

vector<ll> bellmanFord(ll s) {
  vector<ll> d(n + 1, 1e9);
  d[s] = 0;
  for (ll i = 0; i <= n; i++) {
    for (ll j = 0; j <= n; j++) {
      for (auto k : e[j]) {
        d[k.to] = min(d[k.to], d[j] + k.v);
      }
    }
  }
  return d;
}

Dijkstra

Dijkstra 也是一种求单源最短路的算法。我们首先枚举 \(n\) 个点,假设是 \(i\),然后找离 \(i\) 最近的结点 \(j\),然后从从结点 \(j\) 开始,往周围拓展,通过松弛操作更改路径。这种算法与 Bellman-Ford 不同的是使用了一种贪心的思路,找最近的点转移必定会找出更近的路径。但是凡事都有例外,如果是带着负边权的图,那么 Dijkstra 就无能为力了。算法时间复杂度为 \(\mathcal O(n^2)\)

接下来是一种重要的优化:堆优化。我们知道,这里找到最近的点是需要 \(\mathcal O(n)\) 的时间的,那么我们其实可以开一个优先队列,每次都能够获取到最近的点。这种思路就是以从小到大的路径长度作为拓扑序,不断地向后转移,进行松弛操作。这种算法的时间复杂度为 \(\mathcal O((n+m)\log_2 m)=\mathcal O(m\log_2 m)\)

这里使用的是优先队列,如果使用二叉堆的话,其实在优先队列内就可以进行更改了,而 STL 中实现这种操作的是 set,但是 set 的常数巨大,所以我们必须手写。可以将时间复杂度降到 \(\mathcal O(m\log_2 n)\)

这里使用的是优先队列的方法。

vector<ll> dijkstra(ll s) {
  vector<ll> d(n + 1, 1e9), f(n + 1, 0);
  for (q.push({s, 0}), d[s] = 0; q.size(); q.pop()) {
    if (f[(t = q.top()).to]) {
      continue;
    }
    f[t.to] = 1;
    for (auto i : e[t.to]) {
      if (d[t.to] + i.v < d[i.to]) {
        q.push({i.to, d[i.to] = d[t.to] + i.v});
      }
    }
  }
  return d;
}

SPFA

SPFA (Sortest Path Faster Algorithm) 是一种可以快速求负边权最短路的算法,他其实是 Bellman-Ford 的升级版,但是在 Bellman-Ford 的操作中,其实有很多点是不需要进行松弛操作的,因此我们就把需要进行松弛操作的点给存进队列里面,然后等到下次又可以进行松弛操作的时候进行更改。SPFA 在随机图上跑得非常快,基本上能够达到 \(\mathcal O(n+m)\) 的速度,但是需要卡也是非常简单的,最坏会达到 \(\mathcal O(nm)\) 的巨大复杂度。最后,请大家记住一点:

关于 SPFA,她死了。

posted @ 2023-12-09 17:31  haokee  阅读(28)  评论(0)    收藏  举报