Baskot's blog

By Baskot, history, 13 months ago, In English

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.

  • Vote: I like it
  • +70
  • Vote: I do not like it

| Write comment?
»
13 months ago, hide # |
 
Vote: I like it +97 Vote: I do not like it

Bro is inventing algorithms during the contest.

Keep cooking.

»
13 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

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.

  • »
    »
    13 months ago, hide # ^ |
     
    Vote: I like it +2 Vote: I do not like it

    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.

    • »
      »
      »
      13 months ago, hide # ^ |
       
      Vote: I like it +8 Vote: I do not like it

      Thank you very much for the explanation :)

    • »
      »
      »
      13 months ago, hide # ^ |
       
      Vote: I like it +6 Vote: I do not like it

      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

      • »
        »
        »
        »
        13 months ago, hide # ^ |
        ← Rev. 2  
        Vote: I like it 0 Vote: I do not like it

        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?

»
13 months ago, hide # |
 
Vote: I like it +9 Vote: I do not like it

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.

  • »
    »
    13 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    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 :)

»
13 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

can it be found by Tarjan , or did I misunderstand?

  • »
    »
    13 months ago, hide # ^ |
     
    Vote: I like it +5 Vote: I do not like it

    Yeah, It's standard using Tarjan in $$$O(n+m)$$$, my complexity is $$$O((n+m)log(n))$$$