第四章

图论初步

Part 1 基本概念

一个graph是指资料 ,其中 是集合,;即 的元素是 的二元子集。

为了避免记号混淆,我们总是假定

的元素称为图 顶点vertex 的元素称为图 edge

为图 顶点集 为图 边集

顶点集是 的图也称为集合 上的图。我们讨论的图,顶点集都是有限的。

画图

我们常把一个图画成图形:为每个顶点画一个点,若两个顶点构成边则画一条线连接对应的两点。点和线的形状无关紧要,只要能表现出哪些顶点对构成边,哪些顶点对不构成边。

上的图,边集

画图工具:graph editor

习惯说法与记号

  • 把图 的顶点集记为 ,边集记为 。此记法与这两个集合的实际名称无关:图 的顶点集 仍记为 而非
  • 我们并不总是严格区别一个图和它的顶点集或边集。比如,我们会说顶点 (而非 ),边
  • 常简写为 (或 )。
  • 记号 表示 的全部二元子集构成的集合。换言之, 上的图所有可能的边的集合。
  • 的顶点数称为它的,记作 ;边数记作
  • 空图 简写为 。空图和只有一个顶点的图称为平凡图

点边关系,点点关系,边边关系

是图 的一个顶点, 的一条边,若 则称顶点 关联于边 ,称 的边。与一条边关联的两个点称为其端点,称其连接这两个点。

是图 的顶点,若 的边,则称 邻接adjacent相邻;此时也称 互为邻点neighbour。与点 相邻的点构成的集合记作 ,或简写为 ;称为 邻域邻居

是图 的边,,若它们有共同的端点,则称 邻接

称两两不邻接的一族边或点独立

完全图

若图 的所有顶点两两邻接,则称 完全图

个顶点上的完全图记作 也称为三角形

❓ 有多少条边?

两个图的关系

  • 同态和同构
  • 子图

同态和同构

为图, 为图之间的映射。若 保持顶点的邻接性,即

则称 图同态。此时,对于 的像集里的每个顶点 ,它的原像集 里一族相互独立的顶点。

是双射且其逆 也是同态,即使得

则称 图同构,也称图 同构,记作 。从图 映到其自身的同构也称为自同构

子图

为图。

  • ,则 子图subgraph,记作 。我们也说 包含 。若 ,则 真子图

  • 包含 里所有的两端点都在 里的边,则 导出子图induced subgraph;我们说 导出张成 ,记作 。换言之,对任意子集 上的这样一个图,它的边恰是图 里两端点都在 里的边。
  • 张成整个图 ,即若 ,则 生成子图spanning subgraph

图的运算(操作)

  • 二元运算:并,交,删点,加/减边
  • 一元运算:补图,线图

并,交,删点,加/减边

是图。分别定义为

是一族顶点(通常是图 的),我们把 记为 。换言之, 是从 里删除 里的所有顶点和与之关联的边而得到的图。若 是独点集,我们写 而非 。我们把 简写为

对于 的子集 ,定义 。同上, 简记为

补图,线图

是图。

补图

线图

顶点的度

为非空图。顶点 degree 定义为点 处边的数量;根据图的定义,这也等于 的邻点的数量。度是 的顶点称为孤立点。图 最小度记为 ,而图 最大度记为 。 若 的所有顶点的度都是 ,则称 -正则的,或简称正则的。

练习

证明下述结果。设 为图,有

作为推论,任一图里度是奇数的顶点总有偶数个。

3-正则图

❓ 连通的3-正则图是什么样的?注意到 3-正则图必有偶数个顶点。

习题 abc262_e Red and Blue Graph

给你一个有 个顶点和 条边的图。

把图上的每个顶点涂成红色或蓝色,有 种方式。求满足下列条件的染色方式的数量,模

  • 恰有 个顶点被染成红色,有 个顶点被染成蓝色。
  • 两端点颜色不同的边有偶数条。
限制

解法

  • 设红红边有 条,红蓝边有 条。注意到 ,所以 是偶数当且仅当红点的度之和是偶数。

  • 条件变成:选择 个点使得它们的度之和是偶数。

  • 只要这 个点里度是奇数的点有偶数个即可。

代码

const int mod = 998244353; //是素数

int inverse(long long x) { // 模p下的逆元
    if (x == 1) return 1;
    return mod - mod / x * inverse(mod % x) % mod;
}

const int maxn = 2e5 + 5;
int deg[maxn]; // 度
long long f[maxn]; // 阶乘

long long choose(int n, int m) { // 组合数
    if (m < 0 || m > n) return 0;
    return
        f[n] * inverse(f[m] * f[n - m] % mod) % mod;
}
int main() {
    int n, m, k;
    cin >> n >> m >> k;
    while (m--) {
        int u, v;
        cin >> u >> v;
        deg[u]++;
        deg[v]++;
    }
    int odd = 0;
    for (int i = 1; i <= n; i++)
        if (deg[i] % 2)
            odd++;
    
    f[0] = 1;
    for (int i = 1; i <= n; i++)
        f[i] = f[i - 1] * i % mod;

    long long ans = 0;
    for (int i = 0; i <= min(odd, k); i += 2)
        ans += choose(odd, i) * choose(n - odd, k - i) % mod;
    cout << ans % mod << '\n';
}

路径

非空图 称为路径path,若它形如

其中诸 互异。我们说顶点 链接并称之为 端点;而顶点 内部顶点。路径的边数称为其长度。长度为 的路径记作 。路径的长度容许是


里的路径

关于路径的习惯记号和说法

我们常把路径上的点依其自然顺序写出,记如 并称 为一条 的路径,或 之间的路径。

对于 ,我们用下列记号表示 子路径

我们用类似的直观记号表示路径的串接;例如,若三条路径的并 仍是路径,我们把它简记为

路径,-路径

给定顶点集 ,我们称 路径,若 。换言之, 只有两个端点分别在 里。一如先前,我们把 路径简写为 路径, 路径简写为 路径,如此等等。

给定图 ,若路径 非平凡且只有两端点落在 里,我们称 是一条 -路径。注意,-路径的边全都不在 里。

称两条或多条路径独立,若其中每一条路径都不包含另一条路径的内部顶点。例如,两条 路径独立当且仅当它们的公共顶点只有

是一条路径且 ,则图 称为cycle。同路径的记法一样,我们常把环表为其上点的排列;上述环 也可表为 。环的长度是它的边数(或顶点数);长为 的环称为 元环,记作 .

里包含的环的长度的最小值称为 围长girth,记作 ;而环长的最大值称为 周长。若 里没有环,我们置围长为 ,周长为

若一条边连接环上两点但它本身不是环的边,则称之为环的。若 里的一个环是导出子图,则称之为导出环,它也就是一个没有弦的环。


与其弦 ,导出环

距离,直径,中心,半径

里两顶点 距离distance 里最短的 路径的长度,记作 ;若这样的路径不存在则命 。图 里两顶点之间的距离的最大值称为 直径diameter,记作 。换言之

的一个顶点是中心的,若从任意顶点到它的距离的最大值最小。此距离是 半径radius,记作 。换言之,

练习

设图 连通。证明

证明 根据定义,显然有 。设 ,对任意顶点 ,我们有 ,因此

围长和直径的关系

命题 每个有环的图 都满足

证明 里一个最短环。若 ,则 上有这样两个顶点:它们在 上的距离至少是 。在图 里,这两顶点的距离不超过 ;因此二者之间的任何最短路径 都不是 的子图。故 含有一条 -路径 。此路径 连同环 里的两条 路径中较短者,就构成一个比 短的环,矛盾。

途径

里一个长为 途径walk 是一个非空的点边交替序列 ,对所有 都有 。若 则称此途径是封闭的closed

若途径里的顶点相异,它就成为路径。

:有的书上把这里定义的途径称为路径,而把我们前面定义的路径称为简单simple路径。

连通度

  • 连通图,连通块
  • 连通度
  • 割点,桥

连通图,连通块

若图 非空且他的任意两个顶点都被 中的路径链接,则称 连通的connected。若 连通,我们也称 自身(在 里)连通。

为图。称 的极大连通子图为 连通块或连通分量。易见,连通块是导出子图,它们的顶点集是 的划分。


此图有三个连通块,加粗的部分是每个连通块里的一个极小连通生成子图。

连通度

称图 -连通的(),若 并且对每个满足 的子集 都有图 是连通的。

每个非空图都是 -连通的,而 -连通图恰是非平凡的连通图。

使得 -连通图的最大整数 称为 连通度,记作 。依此, 当且仅当 不连通或是 ,而对任意

换言之,删除 中任意 个点, 仍连通。删除 中某 个点, 不连通或只剩一个点。

边连通度

并且对每个少于 条边的子集 都是连通的,则称 -边连通的。使得 -边连通的最大整数 称为 边连通度,记作 。特别地,若 不连通,我们有

换言之,删除 中任意 条边, 仍连通。删除 中某 条边, 不连通。

分离 ⭐

为了今后表述方便,我们采用一个术语:分离。

为图,

使得 里每一条 路径上都有一个点或一条边在 里,我们说 分离separate顶点集 。注意此时必有 。若 分离 ,我们说 分离两顶点 ;若 分离 里某两个顶点,也说 分离

命题 ⭐

若图 非平凡,则

证明 是因为和一个固定的顶点关联的所有边分离图 。为证明 ,令 是一族 条边使得 不连通。根据 的定义,这样的一族边存在;而且 里每条边连接 的两个连通块。我们证明

先设 有一个顶点 不与 里的边关联。令 的含有 的连通块。那么 里与 的边关联的顶点使 分离。由于 里没有一条边两端点都在 里,至多有 条这样的边,所以

再设每个顶点都与 的边关联。令 是任一顶点, 里含有点 的连通块。那么 的每个满足边 的邻点 都在 里,并且这些 都与 里的边关联;因此 。由于点 的邻居 和其他点分离,故有 ——除非没有其他点,即除非 。但是由于 是任一点。此时 是完全图,有

割点,桥

是图, 上一顶点, 上一条边。若从图 里删除点 之后,连通块的数量增加,则 割点cutvertex。若从图 里删除边 后,连通块的数量增加,则 bridge。桥也称割边


此图有割点 和桥

图在程序里的表示

  • 邻接矩阵
  • 邻接表
    • 链式前向星
    • vector 数组

邻接表和邻接矩阵

链式前向星:用数组实现邻接表

const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
struct Edge {
    int to;
    int next;
};
Edge edge[maxm];
int head[maxn];
int e_cnt;

void init() {
    memset(head, -1, sizeof head);
    e_cnt = 0;
}

void add(int from, int to) {
    edge[e_cnt] = {to, head[from]};
    head[from] = e_cnt++;
}

用 std::vector 实现邻接表

const int maxn = 1e5 + 5;
vector<int> g[maxn];

void add(int from, int to) {
    g[from].push_back(to);
    g[to].push_back(from);
}
//对于边uv,add(u, v) 和 add(v, u)

特殊图

  • 树和森林

  • 二分图

  • 欧拉图

树和森林

无环acyclic图,即不含有任何环的图,也称为森林forest。连通的森林称为tree。树里度是 的顶点称为叶子leaf,其他顶点称为内部顶点

不难验证,每个非平凡的树都有叶子。从树里删除一个叶子,剩下的图仍是树。

树的等价定义

对于图 ,下列命题等价:

  • 是树;
  • 的任两个顶点被唯一的路径链接;
  • 是极小连通的,即 是连通的但是对每条边 都不连通;
  • 是极大无环的,即 无环但对 的任何两个不相邻的顶点 有环;
  • 连通且边数比点数少

生成树

为图, 的生成子图。若 是树,则称 生成树spanning tree

每个连通图都含有一个生成树spanning tree。每个非空图都含有一个生成森林spanning forest

有根树

有时宜将树里的某个顶点特别看待,这样的顶点称为树的root。树 连同一个固定的根 就成为有根树rooted tree

有根树上有一个自然的偏序:

关于有根树的术语和习惯

  • 有根树的顶点常称为节点node结点
  • 节点到根的距离是其深度depth,深度相同的节点在同一level。节点深度的最大值是有根树的高度height
  • 画有根树时,根画在最高处,同一层节点画在同一高度。
  • 是有根树的节点,若 在从根到 的路径上,则 祖先ancestor后代descendant。特别地, 本身的祖先和后代。

在右图中,子节点孩子children父节点parent,边 父边parent edge 互为兄弟siblings。无子节点的点是叶子

最近公共祖先lowest common ancestor,记作

的后代生成的子图称为以 为根的子树subtree,简称子树 ;也称之为 的子树

习题 abc385_e 雪花树

可通过下述过程生成的树称为雪花树:

  1. 选择正整数
  2. 准备一个顶点。
  3. 准备 个顶点,每个都跟第 2 步的那个顶点相连。
  4. 给第 3 步的 个顶点每个加上 个叶子。

右图是 的雪花树。第 2,3,4 步加的节点分别以红,蓝,绿 三色表示。

给你一个有 )个点的树 。要从 中删除一些点使得剩下的图是雪花树。最少要删多少个点?

解析

我们把上述第一个顶点称为雪花树的中心。雪花树在以中心为根时,根有 个子节点,而根的每个子节点都有 个子节点。

若在树 里节点 个邻点度都不小于 ,则存在一个以 为中心,有 个点的雪花树。于是想到把 的邻点按度从大到小排序,以 的第 个邻点的度为

代码

int main() {
  int n; cin >> n;
  vector<vector<int>> g(n + 1);
  for (int i = 0; i < n - 1; i++) {
    int u, v; cin >> u >> v;
    g[u].push_back(v);
    g[v].push_back(u);
  }

  int ans = 0;
  for (int i = 1; i <= n; i++) {
    vector<int> deg;
    for (int j : g[i])
      if (g[j].size() > 1)
        deg.push_back(g[j].size());
    sort(deg.rbegin(), deg.rend());//从大到小排序
    for (int j = 0; j < deg.size(); j++)
      ans = max(ans, (j + 1) * deg[j] + 1);
  }
  cout << n - ans << '\n';
}

另一种写法

int main() {
  int n; cin >> n;
  vector<vector<int>> g(n + 1);
  for (int i = 0; i < n - 1; i++) {
    int u, v; cin >> u >> v;
    g[u].push_back(v);
    g[v].push_back(u);
  }

  auto cmp = [&](int x, int y) { // lambda 表达式
    return g[x].size() > g[y].size();
  };

  int ans = 0;
  for (int i = 1; i <= n; i++) {
    sort(g[i].begin(), g[i].end(), cmp);
    for (int j = 0; j < g[i].size() && g[g[i][j]].size() > 1; j++)
      ans = max(ans, (j + 1) * (int) g[g[i][j]].size() + 1);
  }  
  cout << n - ans << '\n';
}

二分图

的整数, 是图。若能把 划分成 个子集使得每条边的两端点都在不同的子集里,则称 -分图-部图-分图常写作二分图bipartite graph

若一个 -分图里每一对来自不同子集的点都构成边,则称它是完全的。若一个完全 -分图的顶点集被分成的 个子集的大小是 ,我们记之为 。形如 的二分图称为星图star菊花图


二分图 的三种画法

二分图的性质

二分图不含有奇环odd cycle(为什么?)。实际上,此性质完全刻画了二分图。

命题 一个图是二分图当且仅当它不含有奇环。

abc260_f Find 4-cycle

个点和 条边。点和边都从 开始编号。边 连接点 和点

编号为 的点之间没有边,编号为 的点之间也没有边。

里含有四元环,任选一个,输出环上的四个点的编号(顺序任意)。若 里不含有四元环,输出 -1。

分析

  • 二分图里的四元环形如右图。
  • 特殊之处:二分图左边点多(三十万)而右边点少(三千)。
  • 对于右边的每一对点 ,我们寻求左边的两个点 使得 都构成边。为此用变量 来记录这样的 。最初,
  • 对于左边的每一个点 ,我们枚举与它相邻的每一对(右边的)点 ;若 ,则 ;否则就找到了左边的两个点:
  • 时间 。此解法应用了鸽巢原理

代码

vector<int> g[300005];
int f[3005][3005];

int main() {
  int S, T, M;
  cin >> S >> T >> M;
  while (M--) {
    int u, v;
    cin >> u >> v;
    g[u].push_back(v - S);
  }
  for (int a = 1; a <= S; a++) {
    for (int i = 0; i < g[a].size(); i++)
      for (int j = 0; j < i; j++) {
        int c = min(g[a][i], g[a][j]);
        int d = max(g[a][i], g[a][j]);
        if (f[c][d] == 0) {
          f[c][d] = a;
        } else {
          cout << f[c][d] << ' ' << a << ' ' << c + S << ' ' << d + S;
          return 0;
        }
      }
  }
  cout << -1;
}

柯尼斯堡Königsberg的七座桥

欧拉环游

如果图上的一个封闭途径走过每条边恰好一次,那么称之为一个欧拉环游Euler tour或欧拉回路。若在一个图上可进行欧拉环游,则称之为欧拉的Eulerian,也称这样的图为欧拉图

定理 一个连通图是欧拉的当且仅当它的每个点的度都是偶数。

图的概念的扩展

  • 有向图

  • 重图

  • 带权图

有向图

一个有向图是指资料 ,其中 是两个不相交的集合,而 是两个映射。

称为图 的顶点集, 称为图 的边集。

映射 为每条边 指派起点 终点 。我们称边 的。

一个有向图上可能有多条从顶点 到顶点 的边,称这些边为chóng

,边 称为自环

关于有向图的术语

  • 有向图的边也称arc
  • 忽略一个有向图边的方向所得的无向图称为其无向版本
  • 以点 为起点(或终点)的边是 出边(或入边), 的出边的数量是 出度out-degree(或入度in-degree)。
  • 路径和环的定义与图类似,但要求边的方向一致。若图上有从点 到点 的路径,则称从 可到达 。若图上任意两点都相互可到达,则称图是强连通的strongly connected

有向图在程序里的表示

重图

容许图里有重边或自环就得到重图的概念。形式地说,一个重图multigraph是指一对不相交的集合 ,连同一个映射 为每条边指派一个或两个顶点为端点。我们仍然以 表达 是边 的端点,不过这不能唯一确定 。也不妨说重图是边的方向被“忘记”了的有向图。

图就是没有重边和自环的重图。前面介绍的适用于图的术语几乎都可以用于重图,不过也有些许差别:

  • 重图上可能有长度是 的环。
  • 一个顶点处的自环对它的度的贡献是

在 OI 里,我们不使用重图这个术语,而是把图和重图统称为无向图undirected graph;为了强调一个无向图(或有向图)没有重边和自环,我们说它是简单simple无向图(或有向图)。

带权图

一个有向图或无向图 连同一个权值函数 为每条边指定一个权值weight,就成为带权图weighted graph

对于带权图,我们定义路径的权值是路径上边的权值之和。

基环树

每个顶点的出度(或入度)都是 的有向图,称为基环内向(或外向森林。若一个基环内向(或外向)森林的无向版本是连通的,则它是基环内向(或外向

习题 abc311_c Find it!

有一个有 个点和 条边的有向图。点从 编号。第 条边从点 连向点 )。

找出一个有向环。

限制

解析

每个点的出度都是 ,这图是基环内向森林。从任一点出发,在图上走,走过的点打上标记,当走到一个标记过的点 时,就是在某个基环上。从 出发在基环上绕一圈。

int main() {
  int n; cin >> n;
  vector<int> a(n + 1);
  for (int i = 1; i <= n; i++)
    cin >> a[i];

  vector<bool> vis(n + 1);
  int v = 1;
  while (!vis[v]) {
    vis[v] = true;
    v = a[v];
  }

  vector<int> cycle;
  int u = v;
  do {
    cycle.push_back(u);
    u = a[u];
  } while (u != v);

  cout << cycle.size() << '\n';
  for (int x : cycle)
    cout << x << ' ';
  cout << '\n';
}

有向无环图,拓扑排序

不含环的有向图称为有向无环图directed acyclic graph,简称 dag。

在 dag 的顶点集上有一个自然的偏序: 若从 可到达

把顶点排列成符合此偏序的操作称为拓扑排序topological sort

拓扑排序算法

对给定的 dag,重复下述过程:

  • 找到一个入度为 的顶点 ,输出 ,删除 的所有出边。

代码

const int maxn = 1e5 + 5;
vector<int> g[maxn];

vector<int> toposort(int n) {
  vector<int> deg(n + 1);
  for (int i = 1; i <= n; i++)
    for (int j : g[i])
      deg[j]++;
  
  vector<int> p;
  for (int i = 1; i <= n; i++)
    if (deg[i] == 0)
      p.push_back(i);
  
  for (int i = 0; i < p.size(); i++)
    for (int j : g[p[i]])
      if (--deg[j] == 0)
        p.push_back(j);

  if (p.size() != n) //不是dag
    return {};

  return p;
}

习题 P1347 排序

个变量,,记作 A,B,C 等。给出 个形如 A < B 的关系。判断这 个关系是否矛盾。若不矛盾,判断这些关系能否确定 个变量的顺序。若能,求出凭前多少个关系就能确定。

解析

每个关系相当于一条有向边。依次读入每个关系,往图里加一条边,做一次拓扑排序;若发现矛盾就结束,若拓扑序不唯一,即某时刻有不止一个点入度为零,就做个标记。

代码

vector<int> g[26];
int n, m;

vector<int> toposort() {
  vector<int> indeg(n);
  for (int i = 0; i < n; i++)
    for (int j : g[i])
      indeg[j]++;

  vector<int> p;
  for (int i = 0; i < n; i++)
    if (indeg[i] == 0)
      p.push_back(i);

  bool ok = true;
  for (int i = 0; i < p.size(); i++) {
    if (p.size() != i + 1)
      ok = false;
    for (int j : g[p[i]])
      if (--indeg[j] == 0)
        p.push_back(j);
  }
  if (p.size() != n) //有环
      return {};
  if (!ok) //拓扑序不唯一
      p.push_back(-1);
  return p;
}
int main() {
  cin >> n >> m;
  for (int i = 1; i <= m; i++) {
    char a, b, c;
    cin >> a >> c >> b;
    g[a - 'A'].push_back(b - 'A');

    vector<int> p = toposort();
    if (p.empty()) {
      cout << "Inconsistency found after " << i << " relations.\n";
      return 0;
    }

    if (p.back() != -1) {
      cout << "Sorted sequence determined after " << i << " relations: ";
      for (int i = 0; i < n; i++)
        cout << (char) (p[i] + 'A');
      cout << ".\n";
      return 0;
    }
  }
  cout << "Sorted sequence cannot be determined.\n";
}

习题 tupc2023_c Topological Sort ⭐

给你一个 的排列

求满足下列条件的有向无环图的数量,模

  • 个点,点从 编号,无重边。
  • 字典序最小的拓扑序等于

限制:

例子

输入

3
1 3 2

输出

4

解析

考虑递推。从序列 开始,第一步看 ,第三步看 。这样每次在开头加一项,维护非自由边的数量。

为例。

代码

const int mod = 998244353;
//快速幂
long long power(long long x, long long n) {
  int ans = 1;
  while (n) {
    if (n & 1)
      ans = ans * x % mod;
    x = x * x % mod;
    n >>= 1;
  }
  return ans;
}
int main() {
  int n; cin >> n;
  vector<int> p(n);
  for (int i = 0; i < n; i++)
    cin >> p[i];
  vector<int> a;
  long long prod = 1;
  long long non_free = 0;
  for (int i = n - 1; i >= 0; i--) {
    while (!a.empty() && p[i] > p[a.back()]) {
      prod = prod * (power(2, a.back() - i) - 1) % mod;
      non_free += a.back() - i;
      a.pop_back();
    }
    a.push_back(i);
  }
  long long tot = (long long) n * (n - 1) / 2;
  prod = prod * power(2, tot - non_free) % mod;
  cout << prod << '\n';
}

一般我们不区别同构的图。例如,我们通常写 $G=G'$ 而非 $G\iso G'$,当我们谈到 $17$ 顶点的完全图,不用管这些顶点是什么。---

Note: 围长(girth)和周长(circumference)都有“一圈有多长”的意思。 girth 指物体有多粗,例如树干,人的腰围等。circumference 指一个物体在平面上的边界的长度,例如圆的周长,国家的边界线长度,城的周长等

Note:比路径更宽松的概念