同步发表于 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 e∈E 没有方向,则称 G G G 为 无向图。无向图的边记作 e = ( u , v ) e = (u, v) e=(u,v), u , v u, v u,v 之间无序。
- 有向图:若 e ∈ E e \in E e∈E 有方向,则称 G G G 为 有向图。有向图的边记作 e = u → v e = u \to v e=u→v 或 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 u→v 和 v → u v \to u v→u。
- 重边:端点和方向(有向图)完全相同的边称为 重边。
- 自环:连接相同点的边称为 自环。
相邻相关
- 相邻:在无向图中,称 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 u→v 称为 u u u 的 出边,到达 u u u 的边 v → u v \to u v→u 称为 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)=∑e∈E([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 v0⋯vk 和边序列 e 1 ⋯ e k e_1 \cdots e_k e1⋯ek 描述,其中 e i = ( v i − 1 , v i ) e_i = (v_{i-1}, v_i) ei=(vi−1,vi)。通常写为 v 0 → v 1 → ⋯ → v k v_0 \to v_1 \to \cdots \to v_k v0→v1→⋯→vk。
- 迹:不经过重复边的途径称为 迹。
- 回路: 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 u⇒v。
- 关于点双连通 / 边双连通 / 强连通,见对应章节。
特殊图
- 简单图:不含重边和自环的图称为 简单图。
- 基图:将有向图的所有有向边替换为无向边得到的图称为该有向图的 基图。
- 有向无环图:不含环的有向图称为 有向无环图,简称 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 ∣V∣2 的图称为 稀疏图, ∣ E ∣ |E| ∣E∣ 接近 ∣ V ∣ 2 |V|^2 ∣V∣2 的图称为 稠密图。这两个概念没有严格定义,用于讨论时间复杂度为 O ( ∣ E ∣ ) O(|E|) O(∣E∣) 和 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2) 的算法。
子图相关
- 子图:满足 V ′ ⊆ V V' \subseteq V V′⊆V 且 E ′ ⊆ E E' \subseteq E E′⊆E 的图 G ′ = ( V ′ , E ′ ) G' = (V', E') G′=(V′,E′) 称为 G = ( V , E ) G = (V, E) G=(V,E) 的 子图,记作 G ′ ⊆ G G' \subseteq G G′⊆G。
- 导出子图:选择若干个点以及两端都在该点集的所有边构成的子图称为该图的 导出子图。导出子图的形态仅由选择的点集 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 G′⊂G′′⊆G。称 G ′ G' G′ 为满足该性质的 分量,如连通分量,点双连通分量。极大子图不能再扩张。例如,极大的连通的子图称为原图的连通分量,也就是我们熟知的连通块。
约定
- 一般记 n n n 表示点集大小 ∣ V ∣ |V| ∣V∣, m m m 表示边集大小 ∣ E ∣ |E| ∣E∣。
拓扑排序
计算方法
常用的拓扑排序算法包括基于深度优先搜索( DFS \texttt{DFS} DFS)的方法和基于入度表( Kahn \texttt{Kahn} Kahn 算法)的方法。这里,我将描述基于入度表的方法,这种方法利用队列来实现:
- 初始化入度表:遍历图中所有的边,统计每个顶点的入度(即指向该顶点的边的数量)。
- 将入度为 0 0 0 的顶点入队:所有在图中入度为 0 0 0 的顶点,都可以作为拓扑排序的起点,将它们加入到一个队列中。
- 循环执行以下步骤,直到队列为空:
- 从队列中取出一个顶点 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 1≤N≤100),表示家族的人数。接下来 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 算法是一种计算图中所有顶点对之间最短路径的算法。
算法流程
- 初始化距离矩阵,对角线为0,其他为两点之间的边权重,若无直接边则为无穷大。
- 对每个顶点 $k $,更新所有顶点对 $ (i, j) $ 的距离:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
。 - 重复步骤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 算法用于在加权图中找到一个顶点到其他所有顶点的最短路径。
算法流程
- 初始化距离数组,源点距离为 0 0 0,其余为无穷大。
- 使用优先队列(或堆)来存储所有节点,优先级为节点的当前距离。
- 从队列中取出距离最小的节点,更新其相邻节点的距离。
- 重复步骤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 算法的一种改进,用于求解单源最短路径问题。它通过使用队列优化了算法的效率。
算法流程:
- 初始化距离数组,源点距离为0,其余为无穷大。
- 将源点入队。
- 当队列非空时,取出队首元素,遍历其所有出边。
- 如果通过当前点可以使得到达某个点的距离更短,则更新距离并将该点入队(如果它当前不在队列中)。
- 重复步骤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)
,并更新x
的low
值。 - 如果
i
已在栈中(vis[i]
),则更新x
的low
值。
- 如果
- 如果
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 x,y 使得 a x ≠ a y a_x \not =a_y ax=ay。选择最少的 k ( 1 ≤ k ≤ n ) k(1 \le k \le n) k(1≤k≤n) 个点,使 a i = ( a i + 1 ) m o d h a_i = (a_i + 1) \mod h ai=(ai+1)modh, m m m 个条件仍成立。
题解
- 对于每一条边,如果 x i = y i + 1 x_i = y_i + 1 xi=yi+1 则把 x i x_i xi 向 y i y_i yi 连一条边
- 缩点
- 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 缩点
算法描述
- 求出所有的 SCC \texttt{SCC} SCC。
- 对于每个 SCC \texttt{SCC} SCC,把所有的点缩成一个点。并求出其权值(这个是要根据题意来的,比如例题是求 SCC \texttt{SCC} SCC 的权值和)。
- 对于原图中的每一条边,如果这条边连接的两个点不在同一个 SCC \texttt{SCC} SCC 中,则把这条边连到两个 SCC \texttt{SCC} SCC 上。
- 对于缩点后的图,形成了一个 DAG \texttt{DAG} DAG。
例1: P3387
题意
给定一个 n n n 个点 m m m 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
题解
- 求出所有的 SCC \texttt{SCC} SCC。
- 对于每个 SCC \texttt{SCC} SCC,把所有的点缩成一个点,并求出其权值和。
- 对于原图中的每一条边,如果这条边连接的两个点不在同一个 SCC \texttt{SCC} SCC 中,则把这条边连到两个 SCC \texttt{SCC} SCC 上。
- 对于缩点后的图,形成了一个 DAG \texttt{DAG} DAG。
- 在 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 算法
- 欧拉回路
- 欧拉路径
- 欧拉图
- 二分图
- 最大匹配
- 最大流
- 最小割
- 最小费用最大流
- 最短路径树
- 最长路径树
- 最长路