算法笔记 / A LEAF IN THE GARDEN

图基础

算法笔记 · 算法理论 / 图论。来自 Daily Learning 的学习笔记。

定义

由顶点集合VV和边集合EE组成的数据结构,记为G=(V,E)G=(V,E)。

V≠∅V \neq \emptyset,E⊆V×VE \subseteq V \times V,∣E∣≤∣V∣2|E| \leq |V|^2。

分类

按方向性

  • 无向图:EE中任意两条边(u,v)(u,v)和(v,u)(v,u)视为等价。
  • 有向图:EE中任意一条边(u,v)(u,v)的方向是从uu指向vv。

按密度

  • 稠密图:∣E∣≈∣V∣2|E| \approx |V|^2。
  • 稀疏图:∣E∣≪∣V∣2|E| \ll |V|^2。

没有明确界限,有时取 ∣E∣<∣V∣log⁡2∣V∣|E| \lt |V|\log_2|V| 为稀疏图。

按权重

  • 无权图:EE中每条边的权重都为1。
  • 带权图:EE中每条边都有一个权重。

带权图又称为网。

基本术语

完全图

GG中任意两顶点间都有边,称为完全图。

  • 无向完全图:nn个顶点,∣E∣=n(n−1)/2|E|=n(n-1)/2
  • 有向完全图:nn个顶点,∣E∣=n(n−1)|E|=n(n-1)

邻接

有边相连的顶点

  • A邻接到B:A→BA\rightarrow B 边存在
  • A邻接自B:A←BA\leftarrow B 边存在

度

与顶点邻接的边数。

  • 入度:以该顶点为终点的边数。
  • 出度:以该顶点为起点的边数。

无向图的总度数为2∣E∣2|E|。 有向图的总度数为∣E∣|E|。

路径

顶点序列v1,v2,⋯ ,vkv_1,v_2,\cdots,v_k,其中vi,vi+1v_i,v_{i+1}之间有边,则称v1,v2,⋯ ,vkv_1,v_2,\cdots,v_k为一条路径。

  • 简单路径:不含重复顶点的路径。
  • 回路:首尾顶点相同的路径。
  • 简单回路:不含重复顶点的回路。
  • 长度:∑i=1kwi\sum_{i=1}^k w_i,wiw_i为边(vi,vi+1)(v_i,v_{i+1})的权重。

子图

取原图的一部分顶点和边,构成满足图的定义的数据结构称为子图。

连通性

如果从任意一顶点出发,都可以到达图中其他任意顶点,则称该图是连通的。

  • 连通分量:图中互相连通的极大子图。
  • 强连通性:有向图的连通性。

nn个顶点的连通图边数

  • 无向图 ≥n−1\geq n-1
  • 有向图 ≥n\geq n

生成树

连通无向图包含所有顶点的极小连通子图。

生成树边数 =n−1= n-1

图的存储

按点存储

邻接矩阵

AnnA_{nn}表示 nn 个顶点的邻接矩阵

  1. 无权图
  • Aij=1A_{ij}=1 表示 ii 和 jj 之间有边,
  • Aij=0A_{ij}=0 表示 ii 和 jj 之间无边。
  1. 带权图
  • AijA_{ij} 表示 ii 和 jj 之间边的权重。

要用特殊值标记无边的情况,如 −1-1 或 +∞+\infty

优点

  • 容易判断是否有边
  • 容易计算顶点的度

缺点

  • 空间复杂度 O(n2)O(n^2),稀疏图浪费空间
  • 统计边的数量效率低

代码

// 带权图
int A[MAXN][MAXN];
void init(int n){
    for(int i = 0;i < n;i++){
        for(int j = 0;j < n;j++) 
            // 无边设为无穷
            A[i][j] = INF;
        // 与自身权值0
        A[i][i] = 0;
    }
}
void add_edge(int u, int v, int w){
    A[u][v] = w;
    // 如果是无向图,还需要添加反向边
}

邻接表

链表数组,每个顶点对应一个链表,链表中存储与该顶点相邻的顶点。

链表里一般存储顶点的下标,方便快速访问。

优点

  • 空间复杂度 O(n+m)O(n+m),适合稀疏图
  • 访问边的效率高

缺点

  • 需要遍历链表,稠密图访问效率低
  • 增加或删除边时,需要修改链表,效率低

代码

// 无权图
vector<list<int>> adj;
void init(int n){
    adj.resize(n);
}
void add_edge(int u, int v){
    adj[u].push_back(v);
    // 如果是无向图,还需要添加反向边
}

按边存储

边集数组

把所有边存成数组,每个元素包含边的起点、终点、权值。

优点

  • 修改边容易
  • 可以排序权值,用于Kruskal算法

缺点

  • 访问效率低

代码

struct Edge {
    int from, to, w;
};
vector<Edge> edges;
void add_edge(int from, int to, int w) {
    edges.emplace_back(from, to, w);
}

链式前向星(推荐)

用三个数组(或结构体)

  • headuhead_u:以 uu 为起点的第一条边编号
  • nextinext_i:编号为 ii 的边的下一条边编号
  • toito_i:编号为 ii 的边的终点
  • wiw_i:编号为 ii 的边的权重

还需要记录边的数量,添加边时用

代码

vector<int> head;
struct Edge {
    int nxt, to, w;
    Edge(int u, int v, int w): 
        nxt(head[u]), to(v), w(w){}
};
vector<Edge> edges;
int cnt;
void init(int n){
    head.resize(n, -1);
    // 如果是无向图,要开双倍空间
    cnt = 0;
}
void add_edge(int u, int v, int w) {
    cnt++;
    // 类似于头插法
    edges.emplace_back(u, v, w);
    head[u] = cnt;
}