From 564f3bb250280b0969b744863be2e82df18c5caa Mon Sep 17 00:00:00 2001 From: ei1333 Date: Tue, 11 Aug 2026 11:57:15 +0000 Subject: [PATCH 1/4] Make strongly connected components iterative --- docs/strongly-connected-components.md | 5 +- .../strongly-connected-components.hpp | 47 ++++++++-- .../strongly-connected-components.test.cpp | 87 +++++++++++++++++++ 3 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 test/unittest/strongly-connected-components.test.cpp diff --git a/docs/strongly-connected-components.md b/docs/strongly-connected-components.md index 6dcc3782..2baa1f81 100644 --- a/docs/strongly-connected-components.md +++ b/docs/strongly-connected-components.md @@ -7,7 +7,7 @@ documentation_of: //graph/connected-components/strongly-connected-components.hpp グラフの任意の $2$ 頂点間に有向路が存在するとき、有向グラフが強連結であるとよぶ。強連結成分は、極大で強連結な部分グラフである。 -適当な頂点から DFS をして、帰りがけ順に頂点を列挙することを、未訪問の頂点がある間繰り返す。次に辺をすべて逆向きにしたグラフについて、列挙した頂点の逆順に DFS する。$1$ 回の DFS で到達できた頂点が $1$ つの強連結成分となる。 +適当な頂点から DFS をして、帰りがけ順に頂点を列挙することを、未訪問の頂点がある間繰り返す。次に辺をすべて逆向きにしたグラフについて、列挙した頂点の逆順に DFS する。$1$ 回の DFS で到達できた頂点が $1$ つの強連結成分となる。DFS は明示的なスタックを用いて非再帰で実行するため、グラフが深い場合もコールスタックを消費しない。 強連結成分を縮約後の頂点とそれらを結ぶ辺からなるグラフは DAG になっている。 @@ -29,11 +29,12 @@ explicit StronglyConnectedComponents(int n) void build() ``` -強連結成分分解する。`dag` には縮約後の頂点と辺からなる DAG が格納される。`comp` には各頂点が属する強連結成分の頂点番号が格納される。`group` には各強連結成分について、それに属する頂点が格納される。 +強連結成分分解する。`dag` には縮約後の頂点と辺からなる DAG が格納される。`comp` には各頂点が属する強連結成分の頂点番号がトポロジカル順で格納される。`group` には各強連結成分について、それに属する頂点が格納される。辺を追加した後に再度呼び出すこともできる。 ## 計算量 - $O(E + V)$ +- 空間計算量: $O(E + V)$ # operator[] diff --git a/graph/connected-components/strongly-connected-components.hpp b/graph/connected-components/strongly-connected-components.hpp index 83b9bd9c..384db33a 100644 --- a/graph/connected-components/strongly-connected-components.hpp +++ b/graph/connected-components/strongly-connected-components.hpp @@ -15,9 +15,15 @@ struct StronglyConnectedComponents : Graph { using Graph::g; std::vector comp; Graph dag; - std::vector > group; + std::vector> group; void build() { + order.clear(); + order.reserve(g.size()); + dfs_stack.clear(); + dfs_stack.reserve(g.size()); + rdfs_stack.clear(); + rdfs_stack.reserve(g.size()); rg = Graph(g.size()); for (std::size_t i = 0; i < g.size(); i++) { for (auto& e : g[i]) { @@ -26,7 +32,7 @@ struct StronglyConnectedComponents : Graph { } comp.assign(g.size(), -1); used.assign(g.size(), 0); - for (std::size_t i = 0; i < g.size(); i++) dfs(i); + for (std::size_t i = 0; i < g.size(); i++) dfs(static_cast(i)); std::reverse(std::begin(order), std::end(order)); int ptr = 0; for (int i : order) @@ -39,7 +45,7 @@ struct StronglyConnectedComponents : Graph { dag.add_directed_edge(x, y, e.cost); } } - group.resize(ptr); + group.assign(ptr, {}); for (std::size_t i = 0; i < g.size(); i++) { group[comp[i]].emplace_back(i); } @@ -49,17 +55,44 @@ struct StronglyConnectedComponents : Graph { private: std::vector order, used; + std::vector> dfs_stack; + std::vector rdfs_stack; Graph rg; void dfs(int idx) { - if (std::exchange(used[idx], true)) return; - for (auto& to : g[idx]) dfs(to); - order.push_back(idx); + if (used[idx]) return; + used[idx] = true; + dfs_stack.clear(); + dfs_stack.emplace_back(idx, 0); + while (!dfs_stack.empty()) { + auto& [v, edge_index] = dfs_stack.back(); + if (edge_index == g[v].size()) { + order.push_back(v); + dfs_stack.pop_back(); + continue; + } + int to = g[v][edge_index++].to; + if (!used[to]) { + used[to] = true; + dfs_stack.emplace_back(to, 0); + } + } } void rdfs(int idx, int cnt) { if (comp[idx] != -1) return; comp[idx] = cnt; - for (auto& to : rg.g[idx]) rdfs(to, cnt); + rdfs_stack.clear(); + rdfs_stack.push_back(idx); + while (!rdfs_stack.empty()) { + int v = rdfs_stack.back(); + rdfs_stack.pop_back(); + for (const auto& edge : rg.g[v]) { + if (comp[edge.to] == -1) { + comp[edge.to] = cnt; + rdfs_stack.push_back(edge.to); + } + } + } } }; diff --git a/test/unittest/strongly-connected-components.test.cpp b/test/unittest/strongly-connected-components.test.cpp new file mode 100644 index 00000000..cb048481 --- /dev/null +++ b/test/unittest/strongly-connected-components.test.cpp @@ -0,0 +1,87 @@ +// competitive-verifier: STANDALONE + +#include "../../graph/connected-components/strongly-connected-components.hpp" + +#include +#include +#include + +int main() { + { + StronglyConnectedComponents<> graph(8); + graph.add_directed_edge(0, 1); + graph.add_directed_edge(1, 2); + graph.add_directed_edge(2, 0); + graph.add_directed_edge(2, 3); + graph.add_directed_edge(3, 4); + graph.add_directed_edge(4, 3); + graph.add_directed_edge(4, 5); + graph.add_directed_edge(6, 7); + graph.build(); + + assert(graph[0] == graph[1] && graph[1] == graph[2]); + assert(graph[3] == graph[4]); + assert(graph[2] != graph[3]); + assert(graph[3] != graph[5]); + assert(graph[6] != graph[7]); + assert(graph[2] < graph[3] && graph[3] < graph[5]); + assert(graph[6] < graph[7]); + + graph.add_directed_edge(5, 0); + graph.build(); + for (int v = 1; v <= 5; v++) assert(graph[v] == graph[0]); + int grouped_vertices = 0; + for (const auto& component : graph.group) { + grouped_vertices += static_cast(component.size()); + } + assert(grouped_vertices == 8); + } + + std::mt19937 rng(123456789); + for (int n = 1; n <= 30; n++) { + for (int iteration = 0; iteration < 30; iteration++) { + StronglyConnectedComponents<> graph(n); + std::vector> reachable(n, std::vector(n, false)); + for (int v = 0; v < n; v++) reachable[v][v] = true; + for (int from = 0; from < n; from++) { + for (int to = 0; to < n; to++) { + if (rng() % 5 == 0) { + graph.add_directed_edge(from, to); + reachable[from][to] = true; + } + } + } + for (int k = 0; k < n; k++) { + for (int from = 0; from < n; from++) { + for (int to = 0; to < n; to++) { + reachable[from][to] = + reachable[from][to] || (reachable[from][k] && reachable[k][to]); + } + } + } + + graph.build(); + for (int from = 0; from < n; from++) { + for (int to = 0; to < n; to++) { + bool same_component = reachable[from][to] && reachable[to][from]; + assert((graph[from] == graph[to]) == same_component); + if (reachable[from][to] && graph[from] != graph[to]) { + assert(graph[from] < graph[to]); + } + } + } + } + } + + { + constexpr int kVertices = 300000; + StronglyConnectedComponents<> graph(kVertices); + for (int v = 1; v < kVertices; v++) { + graph.add_directed_edge(v - 1, v); + } + graph.build(); + assert(graph.group.size() == kVertices); + assert(graph[0] == 0); + assert(graph[kVertices - 1] == kVertices - 1); + } +} From ff410dcc1cb77e0c271a61483e55022c9ab8fd2f Mon Sep 17 00:00:00 2001 From: ei1333 Date: Tue, 11 Aug 2026 12:01:19 +0000 Subject: [PATCH 2/4] Use iterative lowlink for SCC --- docs/strongly-connected-components.md | 4 +- .../strongly-connected-components.hpp | 87 +++++++++---------- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/docs/strongly-connected-components.md b/docs/strongly-connected-components.md index 2baa1f81..d3c4f621 100644 --- a/docs/strongly-connected-components.md +++ b/docs/strongly-connected-components.md @@ -7,7 +7,7 @@ documentation_of: //graph/connected-components/strongly-connected-components.hpp グラフの任意の $2$ 頂点間に有向路が存在するとき、有向グラフが強連結であるとよぶ。強連結成分は、極大で強連結な部分グラフである。 -適当な頂点から DFS をして、帰りがけ順に頂点を列挙することを、未訪問の頂点がある間繰り返す。次に辺をすべて逆向きにしたグラフについて、列挙した頂点の逆順に DFS する。$1$ 回の DFS で到達できた頂点が $1$ つの強連結成分となる。DFS は明示的なスタックを用いて非再帰で実行するため、グラフが深い場合もコールスタックを消費しない。 +各頂点の DFS における訪問順 `ord` と、DFS 木の部分木から到達できる訪問中の頂点の最小訪問順 `low` を求める。`ord` と `low` が等しい頂点を根とする部分が $1$ つの強連結成分となる。DFS は明示的なスタックを用いて非再帰で実行するため、グラフが深い場合もコールスタックを消費しない。また、逆辺グラフは構築しない。 強連結成分を縮約後の頂点とそれらを結ぶ辺からなるグラフは DAG になっている。 @@ -34,7 +34,7 @@ void build() ## 計算量 - $O(E + V)$ -- 空間計算量: $O(E + V)$ +- 補助空間計算量: $O(V)$ # operator[] diff --git a/graph/connected-components/strongly-connected-components.hpp b/graph/connected-components/strongly-connected-components.hpp index 384db33a..6d948194 100644 --- a/graph/connected-components/strongly-connected-components.hpp +++ b/graph/connected-components/strongly-connected-components.hpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -18,25 +17,20 @@ struct StronglyConnectedComponents : Graph { std::vector> group; void build() { - order.clear(); - order.reserve(g.size()); + comp.assign(g.size(), -1); + order.assign(g.size(), -1); + low.resize(g.size()); + in_stack.assign(g.size(), false); dfs_stack.clear(); dfs_stack.reserve(g.size()); - rdfs_stack.clear(); - rdfs_stack.reserve(g.size()); - rg = Graph(g.size()); + vertex_stack.clear(); + vertex_stack.reserve(g.size()); + int now = 0, ptr = 0; for (std::size_t i = 0; i < g.size(); i++) { - for (auto& e : g[i]) { - rg.add_directed_edge(e.to, e.from, e.cost); - } + if (order[i] == -1) dfs(static_cast(i), now, ptr); } - comp.assign(g.size(), -1); - used.assign(g.size(), 0); - for (std::size_t i = 0; i < g.size(); i++) dfs(static_cast(i)); - std::reverse(std::begin(order), std::end(order)); - int ptr = 0; - for (int i : order) - if (comp[i] == -1) rdfs(i, ptr), ptr++; + for (int& component : comp) component = ptr - 1 - component; + dag = Graph(ptr); for (std::size_t i = 0; i < g.size(); i++) { for (auto& e : g[i]) { @@ -54,45 +48,46 @@ struct StronglyConnectedComponents : Graph { int operator[](int k) const { return comp[k]; } private: - std::vector order, used; + std::vector order, low; + std::vector in_stack; std::vector> dfs_stack; - std::vector rdfs_stack; - Graph rg; + std::vector vertex_stack; - void dfs(int idx) { - if (used[idx]) return; - used[idx] = true; + void dfs(int root, int& now, int& component_count) { dfs_stack.clear(); - dfs_stack.emplace_back(idx, 0); + order[root] = low[root] = now++; + in_stack[root] = true; + vertex_stack.push_back(root); + dfs_stack.emplace_back(root, 0); while (!dfs_stack.empty()) { auto& [v, edge_index] = dfs_stack.back(); - if (edge_index == g[v].size()) { - order.push_back(v); - dfs_stack.pop_back(); + if (edge_index < g[v].size()) { + int to = g[v][edge_index++].to; + if (order[to] == -1) { + order[to] = low[to] = now++; + in_stack[to] = true; + vertex_stack.push_back(to); + dfs_stack.emplace_back(to, 0); + } else if (in_stack[to]) { + low[v] = std::min(low[v], order[to]); + } continue; } - int to = g[v][edge_index++].to; - if (!used[to]) { - used[to] = true; - dfs_stack.emplace_back(to, 0); - } - } - } - void rdfs(int idx, int cnt) { - if (comp[idx] != -1) return; - comp[idx] = cnt; - rdfs_stack.clear(); - rdfs_stack.push_back(idx); - while (!rdfs_stack.empty()) { - int v = rdfs_stack.back(); - rdfs_stack.pop_back(); - for (const auto& edge : rg.g[v]) { - if (comp[edge.to] == -1) { - comp[edge.to] = cnt; - rdfs_stack.push_back(edge.to); - } + dfs_stack.pop_back(); + if (!dfs_stack.empty()) { + int parent = dfs_stack.back().first; + low[parent] = std::min(low[parent], low[v]); + } + if (low[v] != order[v]) continue; + while (true) { + int u = vertex_stack.back(); + vertex_stack.pop_back(); + in_stack[u] = false; + comp[u] = component_count; + if (u == v) break; } + component_count++; } } }; From b04c341bdc964518b0c29eb44527f4dee608e49f Mon Sep 17 00:00:00 2001 From: ei1333 Date: Tue, 11 Aug 2026 12:05:52 +0000 Subject: [PATCH 3/4] fix --- docs/strongly-connected-components.md | 3 +-- graph/connected-components/strongly-connected-components.hpp | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/strongly-connected-components.md b/docs/strongly-connected-components.md index d3c4f621..abedb0a7 100644 --- a/docs/strongly-connected-components.md +++ b/docs/strongly-connected-components.md @@ -7,7 +7,7 @@ documentation_of: //graph/connected-components/strongly-connected-components.hpp グラフの任意の $2$ 頂点間に有向路が存在するとき、有向グラフが強連結であるとよぶ。強連結成分は、極大で強連結な部分グラフである。 -各頂点の DFS における訪問順 `ord` と、DFS 木の部分木から到達できる訪問中の頂点の最小訪問順 `low` を求める。`ord` と `low` が等しい頂点を根とする部分が $1$ つの強連結成分となる。DFS は明示的なスタックを用いて非再帰で実行するため、グラフが深い場合もコールスタックを消費しない。また、逆辺グラフは構築しない。 +各頂点の DFS における訪問順 `ord` と、DFS 木の部分木から到達できる訪問中の頂点の最小訪問順 `low` を求める。`ord` と `low` が等しい頂点を根とする部分が $1$ つの強連結成分となる。 強連結成分を縮約後の頂点とそれらを結ぶ辺からなるグラフは DAG になっている。 @@ -34,7 +34,6 @@ void build() ## 計算量 - $O(E + V)$ -- 補助空間計算量: $O(V)$ # operator[] diff --git a/graph/connected-components/strongly-connected-components.hpp b/graph/connected-components/strongly-connected-components.hpp index 6d948194..ae6a251a 100644 --- a/graph/connected-components/strongly-connected-components.hpp +++ b/graph/connected-components/strongly-connected-components.hpp @@ -9,7 +9,6 @@ template struct StronglyConnectedComponents : Graph { - public: using Graph::Graph; using Graph::g; std::vector comp; @@ -48,10 +47,8 @@ struct StronglyConnectedComponents : Graph { int operator[](int k) const { return comp[k]; } private: - std::vector order, low; - std::vector in_stack; + std::vector order, low, in_stack, vertex_stack; std::vector> dfs_stack; - std::vector vertex_stack; void dfs(int root, int& now, int& component_count) { dfs_stack.clear(); From 7b7c161df2b85a4027a8d79c17b6a1a229e260d9 Mon Sep 17 00:00:00 2001 From: ei1333 Date: Tue, 11 Aug 2026 12:21:48 +0000 Subject: [PATCH 4/4] fix --- test/verify/aoj-2450.test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/verify/aoj-2450.test.cpp b/test/verify/aoj-2450.test.cpp index e029f678..1e3cc88f 100644 --- a/test/verify/aoj-2450.test.cpp +++ b/test/verify/aoj-2450.test.cpp @@ -14,7 +14,7 @@ using namespace std; -constexpr int64_t kInfinity = numeric_limits::max(); +constexpr int64_t kInfinity = (1LL << 62) - 1; int main() { int N, Q;