6

Problem: Consider a complete k-ary tree with l levels, with nodes labelled by their rank in a breadth first traversal. Compute the list of labels in the order that they are traversed in the depth first traversal.

For example, for a binary tree with 3 levels, the required list is: [0 1 3 7 8 4 9 10 2 5 11 12 6 13 14]

One way to do this is to actually construct a tree structure and traverse it twice; The first traversal is breadth first, labelling the nodes 0,1,2,.. as you go. The second traversal is depth first, reporting the labels 0,1,3,7,.. as you go.

I'm interested in a method that avoids constructing a tree in memory. I realize that the size of such a tree is comparable to the size of the output, but I'm hoping that the solution will allow for a "streaming" output (ie one that needs not be stored entirely in memory).

I am also interested in the companion problem; start with a tree labelled according to a depth first traversal and generate the labels of a breadth first traversal. I imagine that the solution to this problem will be, in some sense, dual to the first problem.

sitiposit
  • 149
  • 1
  • 13

3 Answers3

3

You don't actually need to construct the tree. You can do the depth first traversal using just the BFS labels instead of pointers to actual nodes.

Using BFS position labels to represent the nodes in k-ary tree:

  • The root is 0
  • The first child of any node n is k*n + 1
  • The right sibling of a node n, if it has one, is n+1

in code it looks like this:

class Whatever
{
    static void addSubtree(List<Integer> list, int node, int k, int levelsleft)
    {
        if (levelsleft < 1)
        {
            return;
        }
        list.add(node);
        for (int i=0; i<k; i++)
        {
            addSubtree(list, node*k+i+1, k, levelsleft-1);
        }
    }
    public static void main (String[] args) throws java.lang.Exception
    {
        int K = 2, LEVELS = 4;
        ArrayList<Integer> list = new ArrayList<>();
        addSubtree(list, 0, K, LEVELS);
        System.out.println(list);
    }
}

This is actually used all the time to represent a binary heap in an array -- the nodes are the array elements in BFS order, and the tree is traversed by performing these operations on indexes.

Matt Timmermans
  • 53,709
  • 3
  • 46
  • 87
  • This looks like exactly what I want. Very elegant too. How would the "companion" function look like? – sitiposit Aug 22 '16 at 03:11
  • @sitiposit, I can figure out a way to go in the other direction, but it's not nearly as pretty (it has an inner loop), and not actually used in real life as far as I know. I think sticking something like that in the above answer would ruin it :) – Matt Timmermans Aug 22 '16 at 03:17
3

You can use the standard DFS and BFS algorithms, but instead of getting the child nodes of a particular node from a pre-built tree structure, you can compute them as needed.

For a BFS-numbered, complete K-ary tree of height H, the i-th child of a node N at depth D is:

K*N + 1 + i

A derivation of this formula when i = 0 (first child) is provided here.

For a DFS-numbered, complete K-ary tree of height H, the i-th child of a node N at depth D is given by a much uglier formula:

N + 1 + i*step where step = (K^(H - D) - 1) / (K - 1)

Here is a rough explanation of this formula:

For a node N at depth D in a DFS-numbered K-ary tree of height H, its first child is simply N+1 because it is the next node to be visited in a depth-first traversal. The second child of N will be visited directly after visiting the entire sub-tree rooted at the first child (N+1), which is itself a complete K-ary tree of height H - (D + 1). The size of any complete, K-ary tree is given by the sum of a finite geometric series as explained here. The size of said sub-tree is the distance between the first and second children, and, in fact, it is the same distance between all siblings since each of their sub-trees are the same size. If we call this distance step, then:

1st child is N + 1 2nd child is N + 1 + step 3rd child is N + 1 + step + step ...and so on.

Below is a Python implementation (note: the dfs function uses the BFS formula, because it is converting from DFS to BFS, and vice-versa for the bfs function.):

def dfs(K, H):
    stack = list()
    push, pop = list.append, list.pop

    push(stack, (0, 0))

    while stack:
        label, depth = pop(stack)
        yield label

        if depth + 1 > H: # leaf node
            continue

        for i in reversed(range(K)):
            push(stack, (K*label + 1 + i, depth + 1))

def bfs(K, H):
    from collections import deque

    queue = deque()
    push, pop = deque.append, deque.popleft

    push(queue, (0, 0))

    while queue:
        label, depth = pop(queue)
        yield label

        if depth + 1 > H: # leaf node
            continue

        step = (K**(H - depth) - 1) // (K - 1)

        for i in range(K):
            push(queue, (label + 1 + i*step, depth + 1))

print(list(dfs(2, 3)))
print(list(bfs(2, 3)))
print(list(dfs(3, 2)))
print(list(bfs(3, 2)))

The above will print:

[0, 1, 3, 7, 8, 4, 9, 10, 2, 5, 11, 12, 6, 13, 14]
[0, 1, 8, 2, 5, 9, 12, 3, 4, 6, 7, 10, 11, 13, 14]
[0, 1, 4, 5, 6, 2, 7, 8, 9, 3, 10, 11, 12]
[0, 1, 5, 9, 2, 3, 4, 6, 7, 8, 10, 11, 12]
Community
  • 1
  • 1
  • This was exactly the kind of solution I was looking for. Nicely illustrates the duality. I certainly believe the fomulae on lines 4 and 7, but I'm finding it hard to understand why they work. Can you offer any justification or point me to a relevant reference. – sitiposit Aug 22 '16 at 22:09
  • Honestly, I just sketched out the depth-first and breadth-first examples for (K, D) = (2, 3), (3, 2), (3, 3) by hand and extrapolated my results into the general formulas. I'm not sure I can come up an good intuitive or rigorous explanation. I'll open a separate question for it. –  Aug 22 '16 at 23:37
  • The explanation you included in pink above was helpful. I'm much more confident about the formulae now. – sitiposit Aug 23 '16 at 23:38
1

Here's some javascript that seems to solve the problem.

var arity = 2;
var depth = 3;

function look(rowstart, pos, dep) {
  var number = rowstart + pos;  
  console.log(number);  
  if (dep < depth-1) {
    var rowlen = Math.pow(arity, dep);
    var newRowstart = rowstart + rowlen;
    for (var i = 0; i < arity; i++) {
      look(newRowstart, pos*arity + i, dep+1);
    }    
  }  
}

look(0, 0, 0);

It's a depth-first search that calculates the BFS label of each node on the way down.

It calculates the label of a node using the current depth dep, the horizontal position in the current row (pos) and the label of the first node in the row (rowstart).

Blorgbeard
  • 101,031
  • 48
  • 228
  • 272