Tarjan算法
2022/3/5 20:45:10
本文主要是介绍Tarjan算法,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Tarjan 算法简介
Tarjan 算法一种由Robert Tarjan提出的求解有向图强连通分量的算法,它能做到线性时间的复杂度。
我们定义:
如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
例如:在上图中,{1 , 2 , 3 , 4 } , { 5 } , { 6 }
三个区域可以相互连通,称为这个图的强连通分量。
Tarjan 算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
再 Tarjan 算法中,有如下定义。
DFN[ i ]
: 在DFS中该节点被搜索的次序(时间戳)
LOW[ i ]
: 为i或i的子树能够追溯到的最早的栈中节点的次序号
当 DFN[ i ]==LOW[ i ]
时,为i或i的子树可以构成一个强连通分量。
算法图示:
以1为Tarjan 算法的起始点,如图
顺次DFS搜到节点6
回溯时发现 LOW[ 5 ]==DFN[ 5 ] , LOW[ 6 ]==DFN[ 6 ] ,
则 { 5 } , { 6 }
为两个强连通分量。回溯至 3 节点,拓展节点 4.
拓展节点1 , 发现1再栈中更新LOW[ 4 ],LOW[ 3 ] 的值为1
回溯节点1,拓展节点2
自此,Tarjan Algorithm 结束,{1 , 2 , 3 , 4 } , { 5 } , { 6 }
为图中的三个强连通分量。
时空复杂度 \(O(E+V)\)
//---------------------------------------------------------------------------
Tarjan 解 有向图强连通分量问题
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
直接根据定义,用双向遍历取交集的方法求强连通分量,时间复杂度为O(N^2+M)。更好的方法是Kosaraju算法或Tarjan算法,两者的时间复杂度都是O(N+M)。本文介绍的是Tarjan算法。
模板代码
void tarjan(int i) { int j; DFN[i]=LOW[i]=++Dindex; instack[i]=true; Stap[++Stop]=i; for (edge *e=V[i];e;e=e->next) { j=e->t; if (!DFN[j]) { tarjan(j); if (LOW[j]<LOW[i]) LOW[i]=LOW[j]; } else if (instack[j] && DFN[j]<LOW[i]) LOW[i]=DFN[j]; } if (DFN[i]==LOW[i]) { Bcnt++; do { j=Stap[Stop--]; instack[j]=false; Belong[j]=Bcnt; } while (j!=i); } } void solve() { int i; Stop=Bcnt=Dindex=0; memset(DFN,0,sizeof(DFN)); for (i=1;i<=N;i++) if (!DFN[i]) tarjan(i); }
Tarjan 解 无向图的双连通分量问题
对于一个连通图,如果任意两点至少存在两条“点不重复”的路径,则说图是点双连通的(即任意两条边都在一个简单环中),点双连通的极大子图称为点_双连通分量。 通常来说,如果要求任意两条边在同一个简单环中,那么就是求点-双连通
例题理解
有向图强连通分量问题例题
每一头牛的愿望就是变成一头最受欢迎的牛。 现在有 N 头牛,编号从 1 到 N,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。 这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。 你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。 输入格式 第一行两个数 N,M; 接下来 M 行,每行两个数 A,B,意思是 A 认为 B 是受欢迎的(给出的信息有可能重复,即有可能出现多个 A,B)。 输出格式 输出被除自己之外的所有牛认为是受欢迎的牛的数量。 数据范围 1≤N≤104, 1≤M≤5×104 输入样例: 3 3 1 2 2 1 2 3 输出样例: 1 样例解释 只有第三头牛被除自己之外的所有牛认为是受欢迎的。 #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 10010, M = 50010; int n, m; int h[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, Size[N]; int dout[N]; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u, in_stk[u] = true; for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if (in_stk[j]) low[u] = min(low[u], dfn[j]); } if (dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top -- ]; in_stk[y] = false; id[y] = scc_cnt; Size[scc_cnt] ++ ; } while (y != u); } } int main() { scanf("%d%d", &n, &m); memset(h, -1, sizeof h); while (m -- ) { int a, b; scanf("%d%d", &a, &b); add(a, b); } for (int i = 1; i <= n; i ++ ) if (!dfn[i]) tarjan(i); for (int i = 1; i <= n; i ++ ) for (int j = h[i]; ~j; j = ne[j]) { int k = e[j]; int a = id[i], b = id[k]; if (a != b) dout[a] ++ ; } int zeros = 0, sum = 0; for (int i = 1; i <= scc_cnt; i ++ ) if (!dout[i]) { zeros ++ ; sum += Size[i]; if (zeros > 1) { sum = 0; break; } } printf("%d\n", sum); return 0; } 一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 A 支援学校 B,并不表示学校 B 一定要支援学校 A)。 当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。 因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。 现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校? 最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件? 输入格式 第 1 行包含整数 N,表示学校数量。 第 2..N+1 行,每行包含一个或多个整数,第 i+1 行表示学校 i 应该支援的学校名单,每行最后都有一个 0 表示名单结束(只有一个 0 即表示该学校没有需要支援的学校)。 输出格式 输出两个问题的结果,每个结果占一行。 数据范围 2≤N≤100 输入样例: 5 2 4 3 0 4 5 0 0 0 1 0 输出样例: 1 2 #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 110, M = 10010; int n; int h[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt; int din[N], dout[N]; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u, in_stk[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if (in_stk[j]) low[u] = min(low[u], dfn[j]); } if (dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top -- ]; in_stk[y] = false; id[y] = scc_cnt; } while (y != u); } } int main() { cin >> n; memset(h, -1, sizeof h); for (int i = 1; i <= n; i ++ ) { int t; while (cin >> t, t) add(i, t); } for (int i = 1; i <= n; i ++ ) if (!dfn[i]) tarjan(i); for (int i = 1; i <= n; i ++ ) for (int j = h[i]; j != -1; j = ne[j]) { int k = e[j]; int a = id[i], b = id[k]; if (a != b) { dout[a] ++ ; din[b] ++ ; } } int a = 0, b = 0; for (int i = 1; i <= scc_cnt; i ++ ) { if (!din[i]) a ++ ; if (!dout[i]) b ++ ; } printf("%d\n", a); if (scc_cnt == 1) puts("0"); else printf("%d\n", max(a, b)); return 0; } 一个有向图 G=(V,E) 称为半连通的 (Semi-Connected),如果满足:∀u,v∈V,满足 u→v 或 v→u,即对于图中任意两点 u,v,存在一条 u 到 v 的有向路径或者从 v 到 u 的有向路径。 若 G′=(V′,E′) 满足,E′ 是 E 中所有和 V′ 有关的边,则称 G′ 是 G 的一个导出子图。 若 G′ 是 G 的导出子图,且 G′ 半连通,则称 G′ 为 G 的半连通子图。 若 G′ 是 G 所有半连通子图中包含节点数最多的,则称 G′ 是 G 的最大半连通子图。 给定一个有向图 G,请求出 G 的最大半连通子图拥有的节点数 K,以及不同的最大半连通子图的数目 C。 由于 C 可能比较大,仅要求输出 C 对 X 的余数。 输入格式 第一行包含三个整数 N,M,X。N,M 分别表示图 G 的点数与边数,X 的意义如上文所述; 接下来 M 行,每行两个正整数 a,b,表示一条有向边 (a,b)。 图中的每个点将编号为 1 到 N,保证输入中同一个 (a,b) 不会出现两次。 输出格式 应包含两行。 第一行包含一个整数 K,第二行包含整数 C mod X。 数据范围 1≤N≤105, 1≤M≤106, 1≤X≤108 输入样例: 6 6 20070603 1 2 2 1 1 3 2 4 5 6 6 4 输出样例: 3 3 #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <unordered_set> using namespace std; typedef long long LL; const int N = 100010, M = 2000010; int n, m, mod; int h[N], hs[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, scc_size[N]; int f[N], g[N]; void add(int h[], int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u, in_stk[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if (in_stk[j]) low[u] = min(low[u], dfn[j]); } if (dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top -- ]; in_stk[y] = false; id[y] = scc_cnt; scc_size[scc_cnt] ++ ; } while (y != u); } } int main() { memset(h, -1, sizeof h); memset(hs, -1, sizeof hs); scanf("%d%d%d", &n, &m, &mod); while (m -- ) { int a, b; scanf("%d%d", &a, &b); add(h, a, b); } for (int i = 1; i <= n; i ++ ) if (!dfn[i]) tarjan(i); unordered_set<LL> S; // (u, v) -> u * 1000000 + v for (int i = 1; i <= n; i ++ ) for (int j = h[i]; ~j; j = ne[j]) { int k = e[j]; int a = id[i], b = id[k]; LL hash = a * 1000000ll + b; if (a != b && !S.count(hash)) { add(hs, a, b); S.insert(hash); } } for (int i = scc_cnt; i; i -- ) { if (!f[i]) { f[i] = scc_size[i]; g[i] = 1; } for (int j = hs[i]; ~j; j = ne[j]) { int k = e[j]; if (f[k] < f[i] + scc_size[k]) { f[k] = f[i] + scc_size[k]; g[k] = g[i]; } else if (f[k] == f[i] + scc_size[k]) g[k] = (g[k] + g[i]) % mod; } } int maxf = 0, sum = 0; for (int i = 1; i <= scc_cnt; i ++ ) if (f[i] > maxf) { maxf = f[i]; sum = g[i]; } else if (f[i] == maxf) sum = (sum + g[i]) % mod; printf("%d\n", maxf); printf("%d\n", sum); return 0; } 银河中的恒星浩如烟海,但是我们只关注那些最亮的恒星。 我们用一个正整数来表示恒星的亮度,数值越大则恒星就越亮,恒星的亮度最暗是 1。 现在对于 N 颗我们关注的恒星,有 M 对亮度之间的相对关系已经判明。 你的任务就是求出这 N 颗恒星的亮度值总和至少有多大。 输入格式 第一行给出两个整数 N 和 M。 之后 M 行,每行三个整数 T,A,B,表示一对恒星 (A,B) 之间的亮度关系。恒星的编号从 1 开始。 如果 T=1,说明 A 和 B 亮度相等。 如果 T=2,说明 A 的亮度小于 B 的亮度。 如果 T=3,说明 A 的亮度不小于 B 的亮度。 如果 T=4,说明 A 的亮度大于 B 的亮度。 如果 T=5,说明 A 的亮度不大于 B 的亮度。 输出格式 输出一个整数表示结果。 若无解,则输出 −1。 数据范围 N≤100000,M≤100000 输入样例: 5 7 1 1 2 2 3 2 4 4 1 3 4 5 5 4 5 2 3 5 4 5 1 输出样例: 11 #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; typedef long long LL; const int N = 100010, M = 600010; int n, m; int h[N], hs[N], e[M], ne[M], w[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, sz[N]; int dist[N]; void add(int h[], int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u, in_stk[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if (in_stk[j]) low[u] = min(low[u], dfn[j]); } if (dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top -- ]; in_stk[y] = false; id[y] = scc_cnt; sz[scc_cnt] ++ ; } while (y != u); } } int main() { scanf("%d%d", &n, &m); memset(h, -1, sizeof h); memset(hs, -1, sizeof hs); for (int i = 1; i <= n; i ++ ) add(h, 0, i, 1); while (m -- ) { int t, a, b; scanf("%d%d%d", &t, &a, &b); if (t == 1) add(h, b, a, 0), add(h, a, b, 0); else if (t == 2) add(h, a, b, 1); else if (t == 3) add(h, b, a, 0); else if (t == 4) add(h, b, a, 1); else add(h, a, b, 0); } tarjan(0); bool success = true; for (int i = 0; i <= n; i ++ ) { for (int j = h[i]; ~j; j = ne[j]) { int k = e[j]; int a = id[i], b = id[k]; if (a == b) { if (w[j] > 0) { success = false; break; } } else add(hs, a, b, w[j]); } if (!success) break; } if (!success) puts("-1"); else { for (int i = scc_cnt; i; i -- ) { for (int j = hs[i]; ~j; j = ne[j]) { int k = e[j]; dist[k] = max(dist[k], dist[i] + w[j]); } } LL res = 0; for (int i = 1; i <= scc_cnt; i ++ ) res += (LL)dist[i] * sz[i]; printf("%lld\n", res); } return 0; }
无向图双连通分量问题例题
为了从 F 个草场中的一个走到另一个,奶牛们有时不得不路过一些她们讨厌的可怕的树。 奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择。 每对草场之间已经有至少一条路径。 给出所有 R 条双向路的描述,每条路连接了两个不同的草场,请计算最少的新建道路的数量,路径由若干道路首尾相连而成。 两条路径相互分离,是指两条路径没有一条重合的道路。 但是,两条分离的路径上可以有一些相同的草场。 对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路。 输入格式 第 1 行输入 F 和 R。 接下来 R 行,每行输入两个整数,表示两个草场,它们之间有一条道路。 输出格式 输出一个整数,表示最少的需要新建的道路数。 数据范围 1≤F≤5000, F−1≤R≤10000 输入样例: 7 7 1 2 2 3 3 4 2 5 4 5 5 6 5 7 输出样例: 2 #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 5010, M = 20010; int n, m; int h[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; int id[N], dcc_cnt; bool is_bridge[M]; int d[N]; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } void tarjan(int u, int from) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan(j, i); low[u] = min(low[u], low[j]); if (dfn[u] < low[j]) is_bridge[i] = is_bridge[i ^ 1] = true; } else if (i != (from ^ 1)) low[u] = min(low[u], dfn[j]); } if (dfn[u] == low[u]) { ++ dcc_cnt; int y; do { y = stk[top -- ]; id[y] = dcc_cnt; } while (y != u); } } int main() { cin >> n >> m; memset(h, -1, sizeof h); while (m -- ) { int a, b; cin >> a >> b; add(a, b), add(b, a); } tarjan(1, -1); for (int i = 0; i < idx; i ++ ) if (is_bridge[i]) d[id[e[i]]] ++ ; int cnt = 0; for (int i = 1; i <= dcc_cnt; i ++ ) if (d[i] == 1) cnt ++ ; printf("%d\n", (cnt + 1) / 2); return 0; } 给定一个由 n 个点 m 条边构成的无向图,请你求出该图删除一个点之后,连通块最多有多少。 输入格式 输入包含多组数据。 每组数据第一行包含两个整数 n,m。 接下来 m 行,每行包含两个整数 a,b,表示 a,b 两点之间有边连接。 数据保证无重边。 点的编号从 0 到 n−1。 读入以一行 0 0 结束。 输出格式 每组数据输出一个结果,占一行,表示连通块的最大数量。 数据范围 1≤n≤10000, 0≤m≤15000, 0≤a,b<n 输入样例: 3 3 0 1 0 2 2 1 4 2 0 1 2 3 3 1 1 0 0 0 输出样例: 1 2 2 #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 10010, M = 30010; int n, m; int h[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int root, ans; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; int cnt = 0; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); if (low[j] >= dfn[u]) cnt ++ ; } else low[u] = min(low[u], dfn[j]); } if (u != root) cnt ++ ; ans = max(ans, cnt); } int main() { while (scanf("%d%d", &n, &m), n || m) { memset(dfn, 0, sizeof dfn); memset(h, -1, sizeof h); idx = timestamp = 0; while (m -- ) { int a, b; scanf("%d%d", &a, &b); add(a, b), add(b, a); } ans = 0; int cnt = 0; for (root = 0; root < n; root ++ ) if (!dfn[root]) { cnt ++ ; tarjan(root); } printf("%d\n", ans + cnt - 1); } return 0; } 煤矿工地可以看成是由隧道连接挖煤点组成的无向图。 为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。 于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。 请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。 输入格式 输入文件有若干组数据,每组数据的第一行是一个正整数 N,表示工地的隧道数。 接下来的 N 行每行是用空格隔开的两个整数 S 和 T,表示挖煤点 S 与挖煤点 T 由隧道直接连接。 注意,每组数据的挖煤点的编号为 1∼Max,其中 Max 表示由隧道连接的挖煤点中,编号最大的挖煤点的编号,可能存在没有被隧道连接的挖煤点。 输入数据以 0 结尾。 输出格式 每组数据输出结果占一行。 其中第 i 行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i 与 : 之间无空格,: 之后有空格)。 其后是用空格隔开的两个正整数,第一个正整数表示对于第 i 组输入数据至少需要设置几个救援出口,第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总数。 输入数据保证答案小于 264,输出格式参照以下输入输出样例。 数据范围 1≤N≤500, 1≤Max≤1000 输入样例: 9 1 3 4 1 3 5 1 2 2 6 1 5 6 3 1 6 3 2 6 1 2 1 3 2 4 2 5 3 6 3 7 0 输出样例: Case 1: 2 4 Case 2: 4 1 #include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std; typedef unsigned long long ULL; const int N = 1010, M = 1010; int n, m; int h[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; int dcc_cnt; vector<int> dcc[N]; bool cut[N]; int root; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[ ++ top] = u; if (u == root && h[u] == -1) { dcc_cnt ++ ; dcc[dcc_cnt].push_back(u); return; } int cnt = 0; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); if (dfn[u] <= low[j]) { cnt ++ ; if (u != root || cnt > 1) cut[u] = true; ++ dcc_cnt; int y; do { y = stk[top -- ]; dcc[dcc_cnt].push_back(y); } while (y != j); dcc[dcc_cnt].push_back(u); } } else low[u] = min(low[u], dfn[j]); } } int main() { int T = 1; while (cin >> m, m) { for (int i = 1; i <= dcc_cnt; i ++ ) dcc[i].clear(); idx = n = timestamp = top = dcc_cnt = 0; memset(h, -1, sizeof h); memset(dfn, 0, sizeof dfn); memset(cut, 0, sizeof cut); while (m -- ) { int a, b; cin >> a >> b; n = max(n, a), n = max(n, b); add(a, b), add(b, a); } for (root = 1; root <= n; root ++ ) if (!dfn[root]) tarjan(root); int res = 0; ULL num = 1; for (int i = 1; i <= dcc_cnt; i ++ ) { int cnt = 0; for (int j = 0; j < dcc[i].size(); j ++ ) if (cut[dcc[i][j]]) cnt ++ ; if (cnt == 0) { if (dcc[i].size() > 1) res += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2; else res ++ ; } else if (cnt == 1) res ++, num *= dcc[i].size() - 1; } printf("Case %d: %d %llu\n", T ++, res, num); } return 0; }
这篇关于Tarjan算法的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南