图论基础

同步发表于 cnblogs

定义与记号

涉及常见或可能用到的概念的定义。关于更多,见参考资料。

基本定义

  • :一张图 G G G 由若干个点和连接这些点的边构成。称点的集合为 点集 V V V,边的集合为 边集 E E E,记 G = ( V , E ) G = (V, E) G=(V,E)
  • :图 G G G 的点数 ∣ V ∣ |V| V 称为 ,记作 ∣ G ∣ |G| G
  • 无向图:若 e ∈ E e \in E eE 没有方向,则称 G G G无向图。无向图的边记作 e = ( u , v ) e = (u, v) e=(u,v) u , v u, v u,v 之间无序。
  • 有向图:若 e ∈ E e \in E eE 有方向,则称 G G G有向图。有向图的边记作 e = u → v e = u \to v e=uv e = ( u , v ) e = (u, v) e=(u,v) u , v u, v u,v 之间有序。无向边 ( u , v ) (u, v) (u,v) 可以视为两条有向边 u → v u \to v uv v → u v \to u vu
  • 重边:端点和方向(有向图)完全相同的边称为 重边
  • 自环:连接相同点的边称为 自环

相邻相关

  • 相邻:在无向图中,称 u , v u,v u,v 相邻 当且仅当存在 e = ( u , v ) e=(u,v) e=(u,v)
  • 邻域:在无向图中,点 u u u邻域 为所有与之相邻的点的集合,记作 N ( u ) N(u) N(u)
  • 邻边:在无向图中,与 u u u 相连的边 ( u , v ) (u, v) (u,v) 称为 u u u邻边
  • 出边 / 入边:在有向图中,从 u u u 出发的边 u → v u \to v uv 称为 u u u出边,到达 u u u 的边 v → u v \to u vu 称为 u u u入边
  • 度数:一个点的 度数 为与之关联的边的数量,记作 d ( u ) d(u) d(u) d ( u ) = ∑ e ∈ E ( [ u = e u ] + [ u = e v ] ) d(u) = \sum_{e \in E} ([u = eu] + [u = ev]) d(u)=eE([u=eu]+[u=ev])。每个点的自环对其度数产生 2 的贡献。
  • 出度 / 入度:在有向图中,从 u u u 出发的边的数量称为 u u u出度,记作 d + ( u ) d^+(u) d+(u);到达 u u u 的边的数量称为 u u u入度,记作 d − ( u ) d^-(u) d(u)

路径相关

  • 途径:连接一串结点的序列称为 途径,用点序列 v 0 ⋯ v k v_0 \cdots v_k v0vk 和边序列 e 1 ⋯ e k e_1 \cdots e_k e1ek 描述,其中 e i = ( v i − 1 , v i ) e_i = (v_{i-1}, v_i) ei=(vi1,vi)。通常写为 v 0 → v 1 → ⋯ → v k v_0 \to v_1 \to \cdots \to v_k v0v1vk
  • :不经过重复边的途径称为
  • 回路 v 0 = v k v_0 = v_k v0=vk 的迹称为 回路
  • 路径:不经过重复点的迹称为 路径,也称 简单路径。不经过重复点比不经过重复边强,所以不经过重复点的途径也是路径。注意题目中的简单路径可能指迹。
  • :除 v 0 = v k v_0 = v_k v0=vk 外所有点互不相同的途径称为 ,也称 简单环

连通性相关

  • 连通:对于无向图的两点 u , v u, v u,v,若存在途径使得 v 0 = u v_0 = u v0=u v k = v v_k = v vk=v,则称 u , v u, v u,v 连通
  • 弱连通:对于有向图的两点 u , v u, v u,v,若将有向边改为无向边后 u , v u, v u,v 连通,则称 u , v u, v u,v 弱连通
  • 连通图:任意两点连通的无向图称为 连通图
  • 弱连通图:任意两点弱连通的有向图称为 弱连通图
  • 可达:对于有向图的两点 u , v u, v u,v,若存在途径使得 v 0 = u v_0 = u v0=u v k = v v_k = v vk=v,则称 u u u 可达 v v v,记作 u ⇒ v u \Rightarrow v uv
  • 关于点双连通 / 边双连通 / 强连通,见对应章节。

特殊图

  • 简单图:不含重边和自环的图称为 简单图
  • 基图:将有向图的所有有向边替换为无向边得到的图称为该有向图的 基图
  • 有向无环图:不含环的有向图称为 有向无环图,简称 DAG \texttt{DAG} DAG Directed   Acyclic   Graph \texttt{Directed Acyclic Graph} Directed Acyclic Graph)。
  • 完全图:任意不同的两点之间恰有一条边的无向简单图称为 完全图 n n n 阶完全图记作 K n K_n Kn
  • :不含环的无向连通图称为 。树是简单图,满足 ∣ V ∣ = ∣ E ∣ + 1 |V|=|E|+1 V=E+1。若干棵(包括一棵)树组成的连通块称为 森林。相关知识点见 “树论”。
  • 稀疏图 / 稠密图 ∣ E ∣ |E| E 远小于 ∣ V ∣ 2 |V|^2 V2 的图称为 稀疏图 ∣ E ∣ |E| E 接近 ∣ V ∣ 2 |V|^2 V2 的图称为 稠密图。这两个概念没有严格定义,用于讨论时间复杂度为 O ( ∣ E ∣ ) O(|E|) O(E) O ( ∣ V ∣ 2 ) O(|V|^2) O(V2) 的算法。

子图相关

  • 子图:满足 V ′ ⊆ V V' \subseteq V VV E ′ ⊆ E E' \subseteq E EE 的图 G ′ = ( V ′ , E ′ ) G' = (V', E') G=(V,E) 称为 G = ( V , E ) G = (V, E) G=(V,E)子图,记作 G ′ ⊆ G G' \subseteq G GG
  • 导出子图:选择若干个点以及两端都在该点集的所有边构成的子图称为该图的 导出子图。导出子图的形态仅由选择的点集 V ′ V' V 决定,称点集为 V ′ V' V 的导出子图为 V ′ V' V 导出的子图,记作 G [ V ′ ] G[V'] G[V]
  • 生成子图 ∣ V ′ ∣ = ∣ V ∣ |V'| = |V| V=V 的子图称为 生成子图
  • 极大子图(分量):在子图满足某性质的前提下,称子图 G ′ G' G极大 的,当且仅当不存在同样满足该性质的子图 G ′ ′ G'' G′′ G ′ ⊂ G ′ ′ ⊆ G G' \subset G'' \subseteq G GG′′G。称 G ′ G' G 为满足该性质的 分量,如连通分量,点双连通分量。极大子图不能再扩张。例如,极大的连通的子图称为原图的连通分量,也就是我们熟知的连通块。

约定

  • 一般记 n n n 表示点集大小 ∣ V ∣ |V| V m m m 表示边集大小 ∣ E ∣ |E| E

拓扑排序

计算方法

常用的拓扑排序算法包括基于深度优先搜索( DFS \texttt{DFS} DFS)的方法和基于入度表( Kahn \texttt{Kahn} Kahn 算法)的方法。这里,我将描述基于入度表的方法,这种方法利用队列来实现:

  1. 初始化入度表:遍历图中所有的边,统计每个顶点的入度(即指向该顶点的边的数量)。
  2. 将入度为 0 0 0 的顶点入队:所有在图中入度为 0 0 0 的顶点,都可以作为拓扑排序的起点,将它们加入到一个队列中。
  3. 循环执行以下步骤,直到队列为空
    • 从队列中取出一个顶点 u u u(即当前排序的下一个顶点),并将其输出为结果序列的一部分。
    • 遍历从顶点 u u u 出发的所有边 ( u , v ) (u, v) (u,v),将每个相邻顶点 v v v 的入度减 1 1 1(表示边 $ (u, v) $ 被移除)。如果某个顶点 v v v 的入度降为 0 0 0,则将 v v v 入队。

DAG \texttt{DAG} DAG 的拓扑序性质很好,常用于解决建图题或图论类型的构造题,常常会将图转化为 DAG \texttt{DAG} DAG,进行 dp   /   dfs \texttt{dp / dfs} dp / dfs 求解。

例 1: B3644 【模板】拓扑排序 / 家谱树

题目描述

有个人的家族很大,辈分关系很混乱,请你帮整理一下这种关系。给出每个人的后代的信息。输出一个序列,使得每个人的后辈都比那个人后列出。

1 1 1 行一个整数 N N N 1 ≤ N ≤ 100 1 \le N \le 100 1N100),表示家族的人数。接下来 N N N 行,第 i i i 行描述第 i i i 个人的后代编号 a i , j a_{i,j} ai,j,表示 a i , j a_{i,j} ai,j i i i 的后代。每行最后是 0 0 0 表示描述完毕。

输出一个序列,使得每个人的后辈都比那个人后列出。如果有多种不同的序列,输出任意一种即可。

代码
// B3644 【模板】拓扑排序 / 家谱树
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10000;                                     // 最大顶点数,根据需要修改
int n, x;                                                   // 顶点数
vector<int> Edge[MAXN];                                     // 邻接表表示图
int in_degree[MAXN];                                        // 入度数组
void toposort() {
    queue<int> Q;
    for(int i = 1; i <= n; i++) 
        for(int j : Edge[i]) in_degree[j]++;                // 初始化入度表
    for(int i = 1; i <= n; i++) 
        if(in_degree[i] == 0)  Q.push(i);                   // 将所有入度为0的顶点入队
    while(!Q.empty()) {                                     // 进行拓扑排序
        int u = Q.front(); Q.pop();
        cout << u << " ";                                   // 输出顶点
        for(int i : Edge[u]) {                              // 遍历u的所有邻接点
            in_degree[i]--;
            if(in_degree[i] == 0) 
                Q.push(i);
        }
    }
    cout << endl;
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
        while (cin >> x && x) 
            Edge[i].push_back(x);
    toposort();
    return 0;
}

例 2: CF463D Gargari and Permutations *1900 \texttt{*1900} *1900

题目描述

给你 k k k 个长度为 n n n 的排列,求这些排列的最长公共子序列的长度。

思路

O ( k n 2 ) O(kn^2) O(kn2) 求出拓扑序。然后按照拓扑序来 dp \texttt{dp} dp 计算。

代码
	#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int n, m, h[maxn], f[maxn], pos[maxn];
bool flg[maxn][maxn];
vector<int> g[maxn];
int dfs(int u) {
    if (f[u] != -1) return f[u];
    f[u] = 0;
    for (int v : g[u]) f[u] = max(f[u], dfs(v));
    return ++f[u];
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    memset(f, -1, sizeof f);
    memset(flg, 1, sizeof flg);
    cin >> n >> m;
    for (int k = 0; k < m; k++) {
        for (int i = 1, x; i <= n; i++) { cin >> x, pos[x] = i; }
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) { flg[i][j] &= pos[i] < pos[j]; }
        }
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (flg[i][j]) g[i].push_back(j);
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) { ans = max(ans, dfs(i)); }
    cout << ans << '\n';
    return 0;
}

最短路问题算法

Floyd \texttt{Floyd} Floyd 算法

基本原理

Floyd-Warshall 算法是一种计算图中所有顶点对之间最短路径的算法。

算法流程

  1. 初始化距离矩阵,对角线为0,其他为两点之间的边权重,若无直接边则为无穷大。
  2. 对每个顶点 $k $,更新所有顶点对 $ (i, j) $ 的距离:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
  3. 重复步骤2,直到所有点都被考虑过。

适用场景

适用于计算任意两点间的最短路径,特别是点数量不是很大时效果好。

代码

void floydWarshall() {
    for (int k = 1; k <= n; k++) 
        for (int i = 1; i <=n; i++) 
            for (int j = 1; j <= n; j++) 
                if (dist[i][k] + dist[k][j] < dist[i][j]) 
                    dist[i][j] = dist[i][k] + dist[k][j];
}

Dijkstra \texttt{Dijkstra} Dijkstra 算法

基本原理

Dijkstra \texttt{Dijkstra} Dijkstra 算法用于在加权图中找到一个顶点到其他所有顶点的最短路径。

算法流程

  1. 初始化距离数组,源点距离为 0 0 0,其余为无穷大。
  2. 使用优先队列(或堆)来存储所有节点,优先级为节点的当前距离。
  3. 从队列中取出距离最小的节点,更新其相邻节点的距离。
  4. 重复步骤3,直到队列为空或找到目标节点。

适用场景

适用于无负权边的图。

例 1 CF449B Jzzhu and Cities *2000 \texttt{*2000} *2000

题目描述

n n n 个点, m m m 条带权边的无向图,另外还有 k k k 条特殊边,每条边连接 1 1 1 i i i

问最多可以删除这 k k k 条边中的多少条,使得每个点到 1 1 1 的最短距离不变。

思路

跑一遍 Dijkstra \texttt{Dijkstra} Dijkstra,计算出相等路径的条数,判断删除。

代码
// 无向图
#include <bits/stdc++.h>
#define pii pair <int, int>
using namespace std;
const int N = 1e5 + 10;
int n, m, k, x, y, z, ans;
int dist[N], cnt[N], vis[N];
vector < pii > edge[N];
vector < pii > spe;

priority_queue<pii, vector<pii>, greater<pii>> pq;

void dijkstra(int start) {
	for (int i = 1; i <= n; i++) dist[i] = 1e9;
    dist[start] = 0;
    pq.push({0, start});

    while (!pq.empty()) {
        int d = pq.top().first;
        int u = pq.top().second;
        pq.pop();

        if (vis[u]) continue;
		vis[u] = 1;
        for (pii i : edge[u]) {
            if (dist[i.first] == dist[u] + i.second) cnt[i.first]++;
            if (dist[i.first] > dist[u] + i.second) {
                dist[i.first] = dist[u] + i.second;
                cnt[i.first] = 1;
                pq.push({dist[i.first], i.first});
            }
        }
    }
}


int main() {
    cin >> n >> m >> k;
    for (int i = 1; i <= m; i++) {
        cin >> x >> y >> z;
        edge[x].push_back({y, z});
        edge[y].push_back({x, z});
    }
    for (int i = 1; i <= k; i++) {
        cin >> y >> z;
        spe.push_back({y, z});
        edge[1].push_back({y, z});
        edge[y].push_back({1, z});
    }
    dijkstra(1); // 算出每一个的距离。
    for (pii i : spe) {
        if (dist[i.first] < i.second) ans++;
        if (dist[i.first] == i.second && cnt[i.first] > 1) 
            ans++, cnt[i.first]--;
    }
    cout << ans << '\n';
}

SPFA \texttt{SPFA} SPFA 算法

关于 SPFA \texttt{SPFA} SPFA, 他 __ 了。

基本原理
SPFA \texttt{SPFA} SPFA Bellman-Ford \texttt{Bellman-Ford} Bellman-Ford 算法的一种改进,用于求解单源最短路径问题。它通过使用队列优化了算法的效率。

算法流程

  1. 初始化距离数组,源点距离为0,其余为无穷大。
  2. 将源点入队。
  3. 当队列非空时,取出队首元素,遍历其所有出边。
  4. 如果通过当前点可以使得到达某个点的距离更短,则更新距离并将该点入队(如果它当前不在队列中)。
  5. 重复步骤3和4,直到队列为空。

适用场景
适用于含负权边但无负权回路的图。

Tarjan \texttt{Tarjan} Tarjan 算法

Trajan \texttt{Trajan} Trajan SCC \texttt{SCC} SCC

算法描述

  • Tarjan \texttt{Tarjan} Tarjan 算法用于在有向图中寻找强连通分量( SCC \texttt{SCC} SCC)。算法通过深度优先搜索( DFS \texttt{DFS} DFS)遍历图,并利用栈维护访问过的顶点,从而在回溯时能够识别并构成强连通分量。

代码解释

  • s.push(x), vis[x] = 1;:当前顶点 x 入栈,并标记为已访问。
  • dfn[x] = low[x] = ++tim;:为顶点 x 分配一个访问编号和最小可回溯编号。
  • 遍历 x 的每个邻接顶点 i
    • 如果 i 未被访问(!dfn[i]),递归调用 tarjan(i),并更新 xlow 值。
    • 如果 i 已在栈中(vis[i]),则更新 xlow 值。
  • 如果 dfn[x] == low[x],说明找到了一个强连通分量的根节点:
    • 通过循环将栈中的元素出栈,直到遇到 x,同时为出栈的顶点分配相同的强连通分量编号,并累加对应的值。

复杂度分析

  • 时间复杂度: O ( V + E ) O(V + E) O(V+E),其中 V V V 是顶点数, E E E 是边数。
  • 空间复杂度: O ( V ) O(V) O(V),主要是用于存储栈、访问标记、时间戳等信息。

通过这个函数实现, Tarjan \texttt{Tarjan} Tarjan 算法能有效地在有向图中识别所有的强连通分量,并能处理每个分量的累计值问题。希望这样的笔记能帮助您更好地理解和使用 Tarjan \texttt{Tarjan} Tarjan 算法。

代码

void tarjan(int x) {
	s.push(x), vis[x] = 1;
	dfn[x] = low[x] = ++tim;
	for (int i : Edge[x]) {
		if (!dfn[i]) {
			tarjan(i);
			low[x] = min(low[x], low[i]);
			low[x] = min(low[x], dfn[i]);
		} else if (vis[i]) {
			low[x] = min(low[x], dfn[i]);
			low[x] = min(low[x], low[i]);
		}
	}

	if (dfn[x] == low[x]) {
		++count_scc;
		while (s.top() != x) {
			color[s.top()] = count_scc;
			sum[count_scc] += val[s.top()];
			vis[s.top()] = false;
			s.pop();
		}
		color[s.top()] = count_scc;
		sum[count_scc] += val[s.top()];
		vis[s.top()] = false;
		s.pop();
	}
}

例 1: CF949C Data Center Maintenance

题意

题意 : n n n 个点,每个点有一个值 a i a_i ai m m m 条边,每个条边链接 2 2 2 个点 x , y x,y xy 使得 a x ≠ a y a_x \not =a_y ax=ay。选择最少的 k ( 1 ≤ k ≤ n ) k(1 \le k \le n) k(1kn) 个点,使 a i = ( a i + 1 ) m o d    h a_i = (a_i + 1) \mod h ai=(ai+1)modh m m m 个条件仍成立。

题解
  1. 对于每一条边,如果 x i = y i + 1 x_i = y_i + 1 xi=yi+1 则把 x i x_i xi y i y_i yi 连一条边
  2. 缩点
  3. DAG \texttt{DAG} DAG 上跑没有出度权值最小的点。
代码
#include <bits/stdc++.h>
#define int long long
#define debug(x) cerr << #x << " " << x << '\n';
#define multi false
using namespace std;
const int N = 1e5 + 10;
int t = 1, n, m, h, x, y, tim, scc_count, ansid;
int val[N], dfn[N], low[N], vis[N], color[N], siz[N];
stack <int> s;
vector <int> Edge[N];
vector <int> scc[N];
void tarjan (int x) {
	vis[x] = 1; s.push(x);
	dfn[x] = low[x] = ++tim;
	for (int i : Edge[x]) {
		if (!dfn[i]) {
			tarjan(i);
			low[x] = min(low[x], low[i]);
			low[x] = min(low[x], dfn[i]);
		} else if (vis[i]) {
			low[x] = min(low[x], low[i]);
			low[x] = min(low[x], dfn[i]);
		}
	}
	if (low[x] == dfn[x]) {
		scc_count++;
		while (s.top() != x) {
			color[s.top()] = scc_count;
			vis[s.top()] = 0;
			siz[scc_count]++; 
			s.pop();
		}
		color[s.top()] = scc_count;
		vis[s.top()] = 0;
		siz[scc_count]++; 
		s.pop();
	}
	return;
}
void solve() {
	cin >> n >> m >> h;
	for (int i = 1; i <= n; i++) cin >> val[i];
	for (int i = 1; i <= m; i++) {
		cin >> x >> y;
		if ((val[x] + 1) % h == val[y]) Edge[x].push_back(y);
		if (val[x] == (val[y] + 1) % h) Edge[y].push_back(x);
	}
	for (int i = 1; i <= n; i++) 
		if (!dfn[i]) tarjan(i);
	for (int i = 1; i <= n; i++)
		for (int j : Edge[i])
			if (color[i] != color[j])
				scc[color[i]].push_back(color[j]);
	for (int i = 1; i <= scc_count; i++) 
		if (scc[i].size() == 0 && (siz[i] < siz[ansid] || ansid == 0))
			ansid = i;
	cout << siz[ansid] << '\n';
	for (int i = 1; i <= n; i++)
		if (color[i] == ansid)
			cout << i << ' ';
    return;
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    if (multi) cin >> t;
    while (t--) solve();
	return 0;
}

Trajan \texttt{Trajan} Trajan 缩点

算法描述

  1. 求出所有的 SCC \texttt{SCC} SCC
  2. 对于每个 SCC \texttt{SCC} SCC,把所有的点缩成一个点。并求出其权值(这个是要根据题意来的,比如例题是求 SCC \texttt{SCC} SCC 的权值和)。
  3. 对于原图中的每一条边,如果这条边连接的两个点不在同一个 SCC \texttt{SCC} SCC 中,则把这条边连到两个 SCC \texttt{SCC} SCC 上。
  4. 对于缩点后的图,形成了一个 DAG \texttt{DAG} DAG

例1: P3387

题意

给定一个 n n n 个点 m m m 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。

允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

题解
  1. 求出所有的 SCC \texttt{SCC} SCC
  2. 对于每个 SCC \texttt{SCC} SCC,把所有的点缩成一个点,并求出其权值和。
  3. 对于原图中的每一条边,如果这条边连接的两个点不在同一个 SCC \texttt{SCC} SCC 中,则把这条边连到两个 SCC \texttt{SCC} SCC 上。
  4. 对于缩点后的图,形成了一个 DAG \texttt{DAG} DAG
  5. DAG \texttt{DAG} DAG 上跑 DP \texttt{DP} DP,求出路径经过的点权值之和的最大值。
代码
#include <bits/stdc++.h>
#define int long long
#define debug(x) cerr << #x << " " << x << '\n';
#define multi false
using namespace std;
const int N = 1e5 + 10;
const int M = 1e5 + 10;
int t = 1, n, m, tim, count_scc, ans;
int x[M], y[M], val[N], color[N], sum[N], f[N];
int vis[N], low[N], dfn[N];
vector <int> Edge[N];
vector <int> scc[N]; // scc edge
stack <int> s;
void tarjan(int x) {
	s.push(x), vis[x] = 1;
	dfn[x] = low[x] = ++tim;
	for (int i : Edge[x]) {
		if (!dfn[i]) {
			tarjan(i);
			low[x] = min(low[x], low[i]);
			low[x] = min(low[x], dfn[i]);
		} else if (vis[i]) {
			low[x] = min(low[x], dfn[i]);
			low[x] = min(low[x], low[i]);
		}
	}

	if (dfn[x] == low[x]) {
		++count_scc;
		while (s.top() != x) {
			color[s.top()] = count_scc;
			sum[count_scc] += val[s.top()];
			vis[s.top()] = false;
			s.pop();
		}
		color[s.top()] = count_scc;
		sum[count_scc] += val[s.top()];
		vis[s.top()] = false;
		s.pop();
	}
}
int dfs(int x) {
	if (f[x]) return f[x];
	f[x] = sum[x];
	for (int i : scc[x]) 
		f[x] = max(f[x], dfs(i) + sum[x]);
	return f[x];
}
void solve() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> val[i];
	for (int i = 1; i <= m; i++) {
		cin >> x[i] >> y[i];
		Edge[x[i]].push_back(y[i]);
	}
	for (int i = 1; i <= n; i++) 
		if (!dfn[i]) 
			tarjan(i);
	for (int i = 1; i <= m; i++) 
		if (color[x[i]] != color[y[i]])
			scc[color[x[i]]].push_back(color[y[i]]);
	for (int i = 1; i <= n; i++) 
		ans = max(ans, dfs(i));
	cout << ans << '\n';
    return;
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
#ifndef ONLINE_JUDGE
    freopen("in.txt", "r", stdin);
#endif
    if (multi) cin >> t;
    while (t--) solve();
	return 0;
}

参考资料

施工进度
  • 拓扑排序
  • Floyd \texttt{Floyd} Floyd 算法求最短路
  • Dijstra \texttt{Dijstra} Dijstra 算法求最短路
  • SPFA \texttt{SPFA} SPFA 算法求最短路
  • Tarjan \texttt{Tarjan} Tarjan 算法求强连通分量
  • 缩点
  • 最小生成树
  • Kruskal \texttt{Kruskal} Kruskal 算法
  • Prim \texttt{Prim} Prim 算法
  • 欧拉回路
  • 欧拉路径
  • 欧拉图
  • 二分图
  • 最大匹配
  • 最大流
  • 最小割
  • 最小费用最大流
  • 最短路径树
  • 最长路径树
  • 最长路
  • 27
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值