vamaddur's blog

By vamaddur, history, 4 years ago, In English

We are given a tree of N nodes (with integer labels 1 to N). For each node in the tree, count the number of connected subgraphs in which that node has the maximum value label.

It is not too difficult to find a quadratic time solution by rerooting the tree N times and then performing an O(N) DFS with a dynamic programming step. Is it possible to solve this problem in O(N), O(N log N), or some sub-quadratic-time-complexity though?

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

»
4 years ago, # |
Rev. 4   Vote: I like it 0 Vote: I do not like it

You can solve this in $$$O(N \cdot \alpha(N))$$$ with a generalized dsu-forest (disjoint set union).

Consider processing the nodes in increasing order, storing for each node $$$n$$$ the connected component $$$c(n)$$$ containing it and (abstractly) the number of connected subgraphs $$$s(n)$$$ of that component containing that node. Then you can collect the answers as you go, as long as merging the components is made efficient enough.

Suppose for the sake of example you are processing node $$$7$$$ and its neighbors are nodes $$$2$$$ and $$$4$$$. (Larger merge operations are not very meaningfully harder.) Writing $$$s'(n)$$$ for what the abstract association $$$s(n)$$$ should be updated to, and $$$s(n)$$$ for its value before updating, it is clear that $$$s'(7) = s(2) * s(4)$$$, while for $$$n \in c(2)$$$, $$$s'(n) = s(n) * (1 + (1 + s(4)))$$$, and likewise for $$$n \in c(4)$$$, $$$s'(n) = s(n) * (1 + (1 + s(2)))$$$. Multiplying everything within each component by some constant is the tricky part, but can be done by using the dsu-forest itself to store factors of $$$s(n)$$$, which can be multiplied during the find operation when the value of $$$s(n)$$$ is needed. Care must be taken to ensure that the path-compression step preserves the path-products used for this calculation, and the merge step often has to allocate a new dsu-forest-node (rather than reuse one) for the same reason, but this should still be fairly easy to implement.

  • »
    »
    4 years ago, # ^ |
    Rev. 3   Vote: I like it 0 Vote: I do not like it

    In the worst case, doesn't merging still take O(N) under this idea because you have to update the entirety of the merged component? We also have to consider which nodes in the components are connected together because simply knowing what components are merged is not sufficient information for the calculation.

    • »
      »
      »
      4 years ago, # ^ |
        Vote: I like it 0 Vote: I do not like it

      My thought process was that since updating $$$s(n)$$$ for every node within an component means multiplying $$$s(n)$$$ by some integer value which depends only on what the component containing $$$n$$$ is being merged with (and does not on $$$n$$$ itself), it may be possible to perform this multiplication lazily by storing this integer value somewhere associated with that component being merged and thereby not actually performing $$$O(n)$$$ multiplications per merge. Then I realized that associating connected components with some object and merging later is exactly the type of problem that a dsu-forest is built to solve: If I store the number to multiply $$$s(n)$$$ by to account for each merge in the nodes of a dsu-forest, then performing the update on a whole component consists of one find operation and one multiplication, while calculating the value of $$$s(n)$$$ at any given time amounts to multiplying all of the stored numbers on the path from the leaf associated with $$$n$$$ to the root, which can also be done during the find operation.

      • »
        »
        »
        »
        4 years ago, # ^ |
          Vote: I like it 0 Vote: I do not like it

        The issue I see with the lazy propagation you described is that we would still need to move information in both directions, from a parent to a child and from a child to a parent. Could you perhaps provide an implementation or example of maintaining a DSU with your idea? It might make it clearer to me and the other people who seem to agree with my previous comment.

  • »
    »
    4 years ago, # ^ |
      Vote: I like it +3 Vote: I do not like it

    If I understood correctly, you are multiplying each node $$$v$$$ by $$$ (s(u) + 1)$$$, effectively saying: for each subtree of those of $$$v$$$ we can now vary the subtree of $$$u$$$. This is wrong. What you need to do is: for each subtree of $$$v$$$ containing the neighbor of $$$u$$$, vary the subtree of $$$u$$$, yielding a much more complicated transition. Am I correct?

    • »
      »
      »
      4 years ago, # ^ |
        Vote: I like it 0 Vote: I do not like it

      Oh, yes! That was a very silly oversight in my mental model of the process which unfortunately renders the above discussion of how to perform that (wrong) update efficiently irrelevant.

    • »
      »
      »
      4 years ago, # ^ |
        Vote: I like it 0 Vote: I do not like it

      bicsi would you happen to know of a modification to this method or another way of making this problem solvable in sub-quadratic time?

»
4 years ago, # |
  Vote: I like it +29 Vote: I do not like it

We still can do some dynamic programming. Let's choose a root, and let $$$T(v)$$$ be a subtree of node $$$v$$$ in a rooted tree.

For each node $$$x$$$ in $$$T(v)$$$ we would store a vector $$$dp_v(x)$$$ of size $$$3$$$:

  • number of connected subgraphs inside $$$T(v)$$$ in which the maximum label is $$$x$$$;

  • number of connected subgraphs inside $$$T(v)$$$ in which the maximum label is $$$x$$$ and $$$v$$$ belongs to subgraph;

  • constant value $$$1$$$

For a subtree we would store all $$$dp_v(x)$$$ in some balanced binary tree (e.g. Cartesian tree). For each label $$$v$$$, answer could be found in a root as $$$dp_{root}(v)[0]$$$. Now we have to maintain this values.

Consider node $$$v$$$ with a list of children $$$c_1, c_2 \dots c_k$$$. Assume we store dp's for each of $$$c_i$$$ in corresponding balanced trees. First of all we want to add $$$v$$$ to each of $$$T(c_i)$$$.

  • For $$$x < v$$$ we can't take $$$v$$$ into subgraph, so $$$dp_c'(x)[0] = dp_c(x)[0]$$$, $$$dp_c'(x)[1] = 0$$$.

  • For $$$x > v$$$ we can take $$$v$$$ into subgraph, so $$$dp_c'(x)[0] = dp_c(x)[0] + dp_c(x)[1]$$$, $$$dp_c'(x)[1] = dp_c(x)[1]$$$.

  • And $$$dp_v(v)[0] = dp_v(v)[1] = \sum_{x < v} dp_c(x)[1] + 1$$$.

After that we have to merge all $$$T(c_i)$$$. We would do it from small to large as usual. Consider we have two subtrees $$$|T(a)| > |T(b)|$$$.

First we will iterate over $$$x \in T(b)$$$ in increasing order, and we have

  • $$$dp_v(x)[0] = dp_b(x)[0] + dp_b(x)[1] \sum_{y < x; y \in T(a)} dp_a(y)[0]$$$;

  • $$$dp_v(x)[1] = dp_b(x)[1] \sum_{y < x; y \in T(a)} dp_a(y)[0]$$$

Now we have to symmetrically update values for labels from $$$T(a)$$$. For each segment of labels strictly between two consecutive $$$x_1, x_2 \in T(b)$$$ we have to set

  • $$$dp_v(y)[0] = dp_a(y)[0] + dp_a(y)[1] \sum_{x_i < y} dp_b(x_i)[0]$$$;

  • $$$dp_v(y)[1] = dp_a(y)[1] \sum_{x_i < y} dp_b(x_i)[0]$$$.

This range updates do not affect values for labels from $$$T(b)$$$.

What remains is to understand how we can perform all this range updates and range sums. In each node of the balanced tree we would store immediate vector $$$dp$$$, sum over all vectors in subtree $$$dpSum$$$, matrix of an update $$$dpUpdate$$$ that accumulates updates that should be performed on whole subtree. As far as matrix multiplication in associative and distributive we can accumulate updates and we can update $$$dpSum$$$.

  • »
    »
    4 years ago, # ^ |
    Rev. 3   Vote: I like it +5 Vote: I do not like it

    In the l both sets of bullet points, why do you have $$$dp_b(x_i)[0]$$$? Shouldn’t that be $$$dp_b(x_i)[1]$$$? That might simplify the formulas a bit, if I’m right. (you don’t need $$$dp[0]$$$ and the matrix reasoning anymore, just a lazy integer that would tell you how many times we should add $$$dp[1]$$$ to the answer)

    • »
      »
      »
      4 years ago, # ^ |
        Vote: I like it 0 Vote: I do not like it

      Yeah, I guess it might be done without $$$dp[1]$$$. In that case, as I see, you have to propagate the $$$multiplication\ value$$$, $$$addition\ value$$$ and this $$$how\ many\ times\ value$$$. That's much less than 3x3 matrix. Just a bit of invariants on order of propagation and it should work :)

      • »
        »
        »
        »
        4 years ago, # ^ |
          Vote: I like it 0 Vote: I do not like it

        What I meant is that the formulas in the first set of bullet points should have $$$dp_a(y)[1]$$$ instead of $$$dp_a(y)[0]$$$ (same with $$$dp_a(x_i)$$$ in the second set of bullet points). If that’s correct, you could omit calculating $$$dp_*(*)[0]$$$ entirely, and just add to the answer $$$dp_v(x)[1]$$$ lazily to every $$$x$$$ for each $$$v$$$.

  • »
    »
    4 years ago, # ^ |
    Rev. 2   Vote: I like it 0 Vote: I do not like it

    Another thing, mostly inrelated: do you know what happens if we merge the two subtrees in complexity $$$O(small \cdot log(big/small))$$$ instead of $$$O(small \cdot log(big))$$$ (see https://en.m.wikipedia.org/wiki/Treap at section ‘union’)? Is the worst case still $$$O(n log^2(n))$$$ or is it improved to $$$O(n log n)$$$? How would such a worst case look like?

    edit: never mind, I’m retty sure it’s $$$O(n log n)$$$.

    • »
      »
      »
      4 years ago, # ^ |
        Vote: I like it 0 Vote: I do not like it

      log(big / small) = log(big) — log(small)

      So for each element (vertex) if we look at the costs of logs summed it telescopes to O(log(N)), right?

      • »
        »
        »
        »
        4 years ago, # ^ |
          Vote: I like it 0 Vote: I do not like it

        Yes, my proof was similar. Pretty interesting, although I wonder how it performs in practice. Do you happen to know a problem where I could experiment with this implementation and do some benchmarks?

    • »
      »
      »
      4 years ago, # ^ |
      Rev. 2   Vote: I like it 0 Vote: I do not like it

      Ok pretty unrelated and might be a very silly doubt but I've had this doubt about treap union for months.

      $$$log(big/small)$$$ is 0 if $$$big == small$$$. What does this mean? Are there some extra terms in the time complexity that are omitted? Basically, what if during each union $$$big==small$$$, what will be the average time complexity?