3
$\begingroup$

Let $T$ be a rooted, finitely branching tree, with values from $A$ in its nodes; $T(j)$ is the value of a node called $j$. $A$ is a commutative monoid. The "bottom-up scan tree" of $T$ is a rooted tree $T^\ast$ isomorphic to $T$, such that if $j$ is a leaf of $T$, $T(j)=T^\ast(j)$, and otherwise $T^\ast(j) = \sum_{i\leq j}T(i)$, where $i\leq j$ indicates that $i$ is a descendant of $j$.

  1. What algorithms exist to compute $T^\ast$ quickly on a parallel random access machine?

  2. Is there an algorithm which performs in time strictly less than $O(N)$, even in the pathological case where $T$ is a linked list?

  3. In the real world, how should $T^\ast$ and $T$ be implemented in order to get good performance on a GPU?

My motivation comes from studying the description of the network simplex algorithm in the book "Network flows" by Ahuja and Magnanti, where this problem appears as "compute-flows" in section 11.4. I am trying to solve optimal transport problems.

I think that it should be relatively straightforward to solve this problem in a loop indexed by the height of tree, where the time of each loop iteration is at most $\log(N)$. If the tree is balanced, then this algorithm suffices, and there is no need for sophisticated algorithms. However, I can see no reason why the trees in the network simplex algorithm for minimum flow cost should be reasonably balanced. (If there are adaptations of the network simplex algorithm where the tree is always balanced, I would be interested in hearing about this.)

$\endgroup$
6
  • $\begingroup$ Is the shape of $T$ known at compile time or only at runtime? (i.e., does it change each time you want to run the algorithm?) $\endgroup$
    – D.W.
    Commented Feb 20 at 18:21
  • $\begingroup$ We have a fixed graph $G$ and $T$ is a spanning tree for $G$. The algorithm operates in stages, at each stage $T$ is replaced with a better solution $T'$. We know the number of nodes and edges of $T$ at compile time but the topology of $T$ changes every iteration of the loop. $T'$ differs from $T$ only by severing one edge and joining another, so if a representation of $T$ carries additional information about its topology such as the depth of each node or a given traversal it may be possible to compute that of $T'$ efficiently from this. $\endgroup$ Commented Feb 20 at 19:03
  • 1
    $\begingroup$ Can this be formulated as a job scheduling problem, i.e., $P|p_i=1,\text{tree}|C_\max$? See e.g. Hu's algorithm. This doesn't take into account the memory hierarchy or locality, though, so might be unhelpful in practice. I don't know much about this topic so I apologize if this direction is not useful or is missing the point. $\endgroup$
    – D.W.
    Commented Feb 20 at 22:22
  • $\begingroup$ I don't know. Thank you for the suggestion, I will look into it, heeding your caveat that it may not be applicable after all. $\endgroup$ Commented Feb 20 at 22:49
  • $\begingroup$ What shape does $T^*$ have? Isn't it the same structure as $T$ just with other node values? It seems to me that you "just" want the prefix sum of the bottom-up traversal of the tree. $\endgroup$ Commented Feb 21 at 15:42

1 Answer 1

1
$\begingroup$

Here is one approach that ought to run fast on real hardware. Store the structure of your tree in an array $P$ so that the parent of node $i$ is found at $P[i]$. For example, with $$ P = [-1, 0, 0, 2, 3], $$ $P[3] = 2$ so the parent of node 3 is node 2. -1 is a dummy value for the parent-less root node. Store all the intermediate sum of the nodes in an equally long array called $V$. Create another array $C$ with all elements set to 0 where you compute how many children each node has:

N = len(P)
C = [0] * N
for i in range(N):
    C[P[i]] += 1

Now process the tree in bottom-up order. Leaf nodes are childless, so we begin with those:

changed = False
parfor i in range(N):
    if C[i] == 0:
        C[i] -= 1
        atomic C[P[i]] -= 1
        atomic V[P[i]] += V[i]
        changed = True

So for every leaf node, we set its child count to -1 to indicate that it has been processed. Then we decrement the parent's child count.Effectively, this generates new leaf nodes and we can iterate the process until no work remain:

while True:
    changed = False
    parfor i in range(N):
        if C[i] == 0:
            C[i] -= 1
            atomic C[P[i]] -= 1
            atomic V[P[i]] += V[i]
            changed = True
    if not changed:
        break

I've invented the keyword "parfor" to denote loops whose iterations can run in parallel and the keyword "atomic" for operations that must run atomically as multiple threads may write to the same memory. There are many standard approaches for handling this effectively. I've also glossed over handling of the parent-less root note. But it's easy to add checks for. If memory is the bottleneck you could rearrange the tree so that nodes and their children are close. I.e, the $|i - P[i]|$ should preferably be small.

$\endgroup$
1
  • $\begingroup$ Thank you very much for your answer! Upon reflection, for my purposes I am interested in an algorithm which behaves well even on unbalanced trees, and will perform in sublinear time even when the tree is simply a linked list. I have edited the question to add this as a requirement, because I am interested in algorithms that behave well on arbitrary input and are always able to make some use of parallelization. $\endgroup$ Commented Feb 22 at 19:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.