JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

图:为连接关系建模

树就是装了辅助轮的图——一个根,没有环。把这些限制去掉,你就得到了**[[graph|图]]**:这整个领域里最灵活的结构,能为路网、朋友关系、网页链接和任务依赖建模。这篇导读给你词汇(顶点、边,有向/无向/带权),以及在 C++ 里存一张图的两种标准方法。

点与线

把所有细节剥掉,一张[[graph|图]]不过就是*点和线*。点是顶点(也叫节点)——那些“东西”——线是——它们之间的关系。这就是全部定义,而它的空泛正是它的力量:每当你有*一些东西*以及*东西之间的连接*,你就有了一张图。城市与道路。人与友谊。网页与链接。任务与“必须先于”的箭头。全是同一个抽象对象。

你已经认识一个特例:[[tree|树]]不过是一张恰好有单一根、没有环、且除一个节点外人人有一个父亲的图。图把这三条规则统统扔掉。它可以有环(A→B→C→A)、有许多“父亲”的节点、互不相连的孤岛,甚至一个顶点连着自己的边。这种自由,正是图为何能把杂乱的现实世界建模得这么好。

  (0)---(1)        an undirected graph, 5 vertices:
   |   / |         edges = { 0-1, 0-2, 1-2, 1-3, 3-4 }
   | /   |
  (2)   (3)---(4)

  note the cycle 0-1-2-0: trees can't do this, graphs can.
一张小小的无向图。顶点是圆圈,边是连线;环路 0-1-2-0 是一个环。

边的三种风味

边有几种风味,而选对哪一种,就完成了把一个问题正确建模的一半。

  1. 无向 — 边是双向的。友谊:你认识我,我就认识你。画成一条朴素的线。
  2. 有向 — 边有方向,画成箭头。推特关注:你可以关注一个不回关你的人。A→B 不蕴含 B→A。
  3. 带权 — 每条边带一个数字:距离、成本、行程时间。路网图的边以公里数加权。

这些可以自由组合。路网通常是无向且带权的(路两头都能走,且无论哪个方向都是 5 公里)。项目排期是有向且无权的(“打地基”必须先于“砌墙”,没得商量)。航班网络是有向且带权的(单程票,要花一笔票价)。在写代码前先把风味叫准,能让你不至于把错的东西建了模。

存一张图:邻接表 vs 邻接矩阵

点和线是一个想法;计算机需要把它们作为数据。有两种标准表示法。[[graph|邻接矩阵]]是一张 n×n 的方格,单元 `[i][j]` 在 i 到 j 有边时为 1(或为权值),否则为 0。简单,而且“i→j 之间有边吗?”是瞬时的 O(1) 查询——但它总要用掉 n² 的空间,哪怕图里只有寥寥几条边。

[[adjacency-list|邻接表]]则为每个顶点只保留一份它的邻居清单。它只用与边数一样多的空间,这正是它成为现实世界中那些大而稀疏的图的默认选择的原因(一座城市有数百万个路口,但每个只接触寥寥几条路)。在 C++ 里它无比自然:一个 `vector` 套着 `vector`。

graph:  0---1      adjacency LIST (a vector per vertex):
        |  /|        adj[0] = {1, 2}
        | / |        adj[1] = {0, 2, 3}
        2   3        adj[2] = {0, 1}
                     adj[3] = {1}

  same graph as a MATRIX (m[i][j]=1 if edge i-j):
        0 1 2 3        adjacency list:  ~edges of space
      0[0 1 1 0]       matrix:          always n*n space
      1[1 0 1 1]
      2[1 1 0 0]       sparse, big graph -> use the LIST
      3[0 1 0 0]
同一张图的两种存法。表只存真实存在的边;矩阵为每一对可能的顶点都留一格。
#include <vector>
int n = 4;
std::vector<std::vector<int>> adj(n);   // adjacency list

auto addEdge = [&](int u, int v) {       // UNDIRECTED:
    adj[u].push_back(v);                 //   add both directions
    adj[v].push_back(u);                 //   (drop one line if directed)
};
addEdge(0,1); addEdge(0,2); addEdge(1,2); addEdge(1,3);
C++ 中的邻接表。无向图两个方向都加;有向图只加一个方向。