第四章

图论初步

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:比路径更宽松的概念