Today’s contest Div1 C—I didn’t know there was something called “biconnected components,” or that they could be found easily using bridges in O(n + m) (I should have studied that :( ). But I wrote this during the contest:
int n;
//LCA,DSU Templates
void solve() {
int m;
cin >> n >> m;
DSU dsu(n + 1); // DSU to merge nodes connected through cycles
DSU dsu1(n + 1); // DSU to build initial spanning tree
vector<pair<int, int>> edges, nonTreeEdges;
vector<int> dp(n + 1, 0);
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
edges.push_back({a, b});
}
// Build initial spanning tree and separate back edges
for (auto [u, v] : edges) {
// dsu1.join returns true if u and v are in different components
if (dsu1.join(u, v)) {
adj[u].push_back(v);
adj[v].push_back(u);
} else {
// back edges
nonTreeEdges.push_back({u, v});
}
}
build();
// Builds binary lifting structure for LCA queries
// The idea is just to connect all the nodes in the path from u to v on the tree
for (auto [u, v] : nonTreeEdges) {
int lca = LCA(u, v);
int du = depth[u] - depth[lca];
int dv = depth[v] - depth[lca];
dp[u] = max(dp[u], du);
dp[v] = max(dp[v], dv);
}
// DFS to propagate max distances and merge components for biconnected edges
function<void(int, int)> dfs1 = [&](int v, int p) {
for (auto u : adj[v]) {
if (u == p) continue;
dfs1(u, v);
// If child has a back edge affecting it, merge it with parent
if (dp[u]) dsu.join(u, v);
// Propagate maximum distance upwards
dp[v] = max(dp[v], dp[u] - 1);
}
};
dfs1(1, -1);
// After DFS, `dsu` connects all biconnected components correctly
// This approach can be modified to work dynamically for online queries if the initial graph is connected
}
I had no idea why it worked though (it was just a feeling~) :) I treated each cycle independently, and somehow it worked—yay!
It can actually be proved that this approach correctly identifies biconnected components using the high-level idea of bridges.
It may seem logical, but anything that works without a proof is just magic to me.
I’d love to know if there are other approaches, any cool properties of bridges, and, of course, the proofs related to them.








Bro is inventing algorithms during the contest.
Keep cooking.
Could you please explain your approach and logic in more detail in a comment/blog?
This seems like a much more intuitive approach as compared to Tarjan's. I tried finding a workaround for that online but there's not much I could get.
First, I build a spanning tree of the graph. After that, some edges remain unused—those are the edges that, if added, would form cycles.
Now, if you have a tree and you add only one extra edge between nodes u and v, exactly one cycle is created: the path between u and v in the tree, plus the new edge.
Continuing this way, I treat each unused edge as if it were the only one being added. Each edge connects u and v, forming a cycle consisting of the path between them in the tree. Of course, when you consider the overall picture, many of these cycles overlap (and eventually causes other cycles to be formed). If two cycles overlap, that means they share at least one vertex, which implies they’re connected. In our approach, that shared vertex serves as the connection point between both paths.
When I connect nodes from u to v, I do it by connecting u to their lowest common ancestor (LCA), and v to the LCA as well. Since the LCA is an ancestor, this structure allows a dynamic programming approach: let dp[u] represent the maximum length you can propagate upwards from node u.
If dp[u]=0, then u cannot connect to its parent.
If dp[u]>0, then u can connect to its parent, and we update the parent’s dp accordingly.
That’s the whole idea.
Thank you very much for the explanation :)
This is just the DFS tree in action, here's a great blog on it I found after the contest which gives you all the intuition you need, including how the standard algorithm works
It's super impressive you basically cooked this mid contest XD, I too didn't know about bridges beforehand, reached as far as finding their definition during the contest and then used an online implementation as a black box
Basically we could define dp[u] as the number of back edges that are going above the edge between node u and its parent(if it exists) in the dfs tree(as mentioned in your mentioned blog). if dp[u] > 0, then we join u and parent of u. This means we can implement the algorithm described above but in O(n + m) time complexity without having to rely on lca. Does that sound correct?
https://codeforces.me/blog/entry/68138
Interesting approach. I also thought about doing something with backward edges but couldn't figure it out during the contest. After the contest I studied jiangly's submission and his template for biconnected components.
I think that DSU time complexity is $$$O(n \alpha(n))$$$ (though, $$$\alpha(n)$$$ is the extremely slowly growing Ackermann function) if we use ranks and path compression, and LCA with binary lifting is $$$O(\log n)$$$. So the complexity of your approach is probably not $$$O(n+m)$$$.
Btw, when I revised my DSU theory understanding before writing this comment I discovered that Tarjan (1975) proved that in the comparison-based model $$$\alpha(n)$$$ is optimal in DSU. What a coincidence, Tarjan (1974) is also the author of the DFS-based $$$O(n+m)$$$ bridge-finding algorithm.
Yep, you are right, my approach complexity is $$$O((n+m)logn)$$$, I meant Tarjan's solves it in $$$O(n+m)$$$ ... + Wow that is actually interesting to know :)
can it be found by Tarjan , or did I misunderstand?
Yeah, It's standard using Tarjan in $$$O(n+m)$$$, my complexity is $$$O((n+m)log(n))$$$