4

I'm developing a structure that is like a binary tree but generalized across dimensions so you can set whether it is a binary tree, quadtree, octree, etc by setting the dimension parameter during initialization.

Here is the definition of it:

template <uint Dimension, typename StateType>
class NDTree {
public:
    std::array<NDTree*, cexp::pow(2, Dimension)> * nodes;
    NDTree * parent;
    StateType state;
    char position; //position in parents node list
    bool leaf;

    NDTree const &operator[](const int i) const
    {
        return (*(*nodes)[i]);
    }

    NDTree &operator[](const int i)
    {
        return (*(*nodes)[i]);
    }
}

So, to initialize it- I set a dimension and then subdivide. I am going for a quadtree of depth 2 for illustration here:

const uint Dimension = 2;
NDTree<Dimension, char> tree;
tree.subdivide();

for(int i=0; i<tree.size(); i++)
    tree[i].subdivide();

for(int y=0; y<cexp::pow(2, Dimension); y++) {
    for(int x=0; x<cexp::pow(2, Dimension); x++) {
        tree[y][x].state = ((y)*10)+(x);
    }
}
std::cout << tree << std::endl;

This will result in a quadtree, the state of each of the values are initialized to [0-4][0-4].

([{0}{1}{2}{3}][{10}{11}{12}{13}][{20}{21}{22}{23}][{30}{31}{32}{33}])

I am having trouble finding adjacent nodes from any piece. What it needs to do is take a direction and then (if necessary) traverse up the tree if the direction goes off of the edge of the nodes parent (e.g. if we were on the bottom right of the quadtree square and we needed to get the piece to the right of it). My algorithm returns bogus values.

Here is how the arrays are laid out:

enter image description here

And here are the structures necessary to know for it:

This just holds the direction for items.

enum orientation : signed int {LEFT = -1, CENTER = 0, RIGHT = 1};

This holds a direction and whether or not to go deeper.

template <uint Dimension>
struct TraversalHelper {
    std::array<orientation, Dimension> way;
    bool deeper;
};

node_orientation_table holds the orientations in the structure. So in 2d, 0 0 refers to the top left square (or left left square). [[LEFT, LEFT], [RIGHT, LEFT], [LEFT, RIGHT], [RIGHT, RIGHT]]

And the function getPositionFromOrientation would take LEFT, LEFT and return 0. It is just basically the opposite of the node_orientation_table above.

TraversalHelper<Dimension> traverse(const std::array<orientation, Dimension> dir, const std::array<orientation, Dimension> cmp) const
{
    TraversalHelper<Dimension> depth;

    for(uint d=0; d < Dimension; ++d) {
        switch(dir[d]) {
            case CENTER:
                depth.way[d] = CENTER;
                goto cont;

            case LEFT:
                if(cmp[d] == RIGHT) {
                    depth.way[d] = LEFT;
                } else {
                    depth.way[d] = RIGHT;
                    depth.deeper = true;
                }
                break;

            case RIGHT:
                if(cmp[d] == LEFT) {
                    depth.way[d] = RIGHT;
                } else {
                    depth.way[d] = LEFT;
                    depth.deeper = true;
                }
                break;
        }

        cont:
            continue;
    }

    return depth;
}

std::array<orientation, Dimension> uncenter(const std::array<orientation, Dimension> dir, const std::array<orientation, Dimension> cmp) const
{
    std::array<orientation, Dimension> way;

    for(uint d=0; d < Dimension; ++d)
        way[d] = (dir[d] == CENTER) ? cmp[d] : dir[d];

    return way;
}

NDTree * getAdjacentNode(const std::array<orientation, Dimension> direction) const
{
    //our first traversal pass
    TraversalHelper<Dimension> pass = traverse(direction, node_orientation_table[position]);

    //if we are lucky the direction results in one of our siblings
    if(!pass.deeper)
        return (*(*parent).nodes)[getPositionFromOrientation<Dimension>(pass.way)];


    std::vector<std::array<orientation, Dimension>> up;   //holds our directions for going up the tree
    std::vector<std::array<orientation, Dimension>> down; //holds our directions for going down
    NDTree<Dimension, StateType> * tp = parent;           //tp is our tree pointer
    up.push_back(pass.way); //initialize with our first pass we did above

    while(true) {
        //continue going up as long as it takes, baby
        pass = traverse(up.back(), node_orientation_table[tp->position]);
        std::cout << pass.way << " :: " << uncenter(pass.way, node_orientation_table[tp->position]) << std::endl;

        if(!pass.deeper) //we've reached necessary top
            break;
        up.push_back(pass.way);

        //if we don't have any parent we must explode upwards
        if(tp->parent == nullptr)
            tp->reverseBirth(tp->position);

        tp = tp->parent;
    }

    //line break ups and downs
    std::cout << std::endl;

    //traverse upwards combining the matrices to get our actual position in cube
    tp = const_cast<NDTree *>(this);
    for(int i=1; i<up.size(); i++) {
        std::cout << up[i] << " :: " << uncenter(up[i], node_orientation_table[tp->position]) << std::endl;
        down.push_back(uncenter(up[i], node_orientation_table[tp->parent->position]));
        tp = tp->parent;
    }

    //make our way back down (tp is still set to upmost parent from above)
    for(const auto & i : down) {
        int pos = 0; //we need to get the position from an orientation list

        for(int d=0; d<i.size(); d++)
            if(i[d] == RIGHT)
                pos += cexp::pow(2, d); //consider left as 0 and right as 1 << dimension

        //grab the child of treepointer via position we just calculated
        tp = (*(*tp).nodes)[pos];
    }

    return tp;
}

For an example of this:

std::array<orientation, Dimension> direction;
direction[0] = LEFT; //x
direction[1] = CENTER; //y

NDTree<Dimension> * result = tree[3][0]->getAdjacentNode(direction);

enter image description here

This should grab the top right square within bottom left square, e.g. tree[2][1] which would have a value of 21 if we read its state. Which works since my last edit (algorithm is modified). Still, however, many queries do not return correct results.

//Should return tree[3][1], instead it gives back tree[2][3]
NDTree<Dimension, char> * result = tree[1][2].getAdjacentNode({ RIGHT, RIGHT });

//Should return tree[1][3], instead it gives back tree[0][3]
NDTree<Dimension, char> * result = tree[3][0].getAdjacentNode({ RIGHT, LEFT });

There are more examples of incorrect behavior such as tree[0][0](LEFT, LEFT), but many others work correctly.

Here is the folder of the git repo I am working from with this. Just run g++ -std=c++11 main.cpp from that directory if it is necessary.

Zword
  • 6,605
  • 3
  • 27
  • 52
jett
  • 1,276
  • 2
  • 14
  • 34
  • I can't compile. It's looking for `gmp.h`. Can you say all libraries you're using? – Gene Feb 07 '14 at 23:38
  • Go into the test/treet folder and compile from there. The root folder uses different code outside of the question. – jett Feb 07 '14 at 23:40
  • Thanks. It compiles here just fine. This is a rather baroque implementation. Lots of redundant information. Are you sure you don't want to be more space conservative? Space is usually an issue in realistic 2^n-tree applications. – Gene Feb 08 '14 at 00:13
  • My plan was to remove the array of pointers and just point to the first node and access from there, it is just laid out how it is now for ease of reading/writing. Will be able to likely pack the state, position, and leaf into a single int in most situations as well. So, it can be reduced pretty well? – jett Feb 08 '14 at 01:00
  • Can you show another example for binary tree or octree about adjacent node? I can't decide which one is the adjacent node for binary tree or octree. – Alex Chi Feb 08 '14 at 16:34
  • What should be considered the "adjacent" node in some direction if there is no same-depth node in that direction, or if there is one and it has even deeper children? – j_random_hacker Feb 09 '14 at 16:25
  • @AlexChi I updated the example to make it a little more clear- hope it helps. Thanks. – jett Feb 09 '14 at 21:51
  • @j_random_hacker When it is finished I'd like it to be same level as requesting node if adjacent node goes deeper, If adjacent node doesn't go as deep as requesting node then just as far down as possible without subdividing (e.g. no subdivide to get to same level). Does that make sense? At this point I'm not concerned about it like that as I am just trying to get the basic logic down. – jett Feb 09 '14 at 21:54
  • Sorry, I still can't understand that. How did the arrays laid out if it is a binary tree? – Alex Chi Feb 10 '14 at 03:31
  • @AlexChi The nodes array would just have 2 children instead of 4. So access would be [1][0] to get the left leaf of the right branch. Likewise, an octree would have 8 children. Left always refers to lower numbers, than right on the dimension. Look at the last part where position is generated by looking at the orientation, left per dimension = 0, right = pow(2, dimension). So, exactly how a C array would be laid out. – jett Feb 10 '14 at 03:35
  • 1
    I think your example has an error. You said `tree[3][0].getAdjacentNode({ RIGHT, LEFT })` should return `tree[3][1]`. It should return `tree[1][3]`. Is it right? – Alex Chi Feb 10 '14 at 07:13
  • @AlexChi Yes thank you, Got them mixed up when writing. – jett Feb 10 '14 at 13:02
  • Can you clearly state me what is adjacent over here? I can't understand you properly. Do you want all the child and parent of a node form a group (Logically Physical Connection) or you want parent, sibling and child of a node to form a group ?? – Karthik Surianarayanan Feb 11 '14 at 03:45
  • @KarthikSurianarayanan Adjacent just means a node in a direction compared to another node. So, in 1d if we had 0,1,2,3,4 and I wanted the node to the right of 2 I would receive the node containing 3. – jett Feb 11 '14 at 06:54
  • It looks like the complicated part of what you are trying to do is make a single up/down traversal of the tree while stepping in two dimensions simultaneously. You can vastly simplify your algorithm by worrying only about the 4 neighors E, W, S, N and taking two steps to get to NE, SE, SW, NW. – Gene Feb 11 '14 at 22:40
  • @Gene That is correct. I don't think I understand why that would simplify it? – jett Feb 12 '14 at 00:50
  • 1
    @jett E.g. for successor you must find the first ancestor A of which the start node is a descendent of its left child, then find the leftmost descendent of the right child of A. The complexity is that A is unique _for each dimension_. I believe your code is getting the right answer when A happens to be the same for all dimensions and it's wrong when the respective A's are different. Stepping on only one dimension at a time will fix this problem easily. – Gene Feb 12 '14 at 01:49
  • @Gene Thank you, I think I see what you mean. – jett Feb 12 '14 at 05:20

3 Answers3

2

here is one property you can try to exploit: consider just the 4 nodes:

 00  01
 10  11

Any node can have up to 4 neighbor nodes; two will exist in the same structure (larger square) and you have to look for the other two in neighboring structures. Let's focus on identifying the neighbors which are in the same structure: the neighbors for 00 are 01 and 10; the neighbors for 11 are 01 and 10. Notice that only one bit differs between neighbor nodes and that neighbors can be classified in horizontal and vertical. SO

     00 - 01    00 - 01    //horizontal neighbors
     |               |
     10              11    //vertical neighbors

Notice how flipping the MSB gets the vertical neighbor and flipping the LSB gets the horizontal node? Let's have a close look:

   MSB:  0 -> 1 gets the node directly below
         1 -> 0 sets the node directly above

   LSB:  0 -> 1 gets the node to the right
         1 -> 0 gets the node to the left

So now we can determine the node's in each direction assuming they exist in the same substructure. What about the node to the left of 00 or above 10?? According to the logic so far if you want a horizontal neighbor you should flip the LSB; but flipping it would retrieve 10 ( the node to the right). So let's add a new rule, for impossible operations:

   you can't go left for  x0 , 
   you can't go right for x1,
   you can't go up for    0x,
   you can't go down for  1x

*impossible operations refers to operations within the same structure. Let's look at the bigger picture which are the up and left neighbors for 00? if we go left for 00 of strucutre 0 (S0), we should end up with 01 of(S1), and if we go up we end up with node 10 of S(2). Notice that they are basically the same horizontal/ veritical neighbor values form S(0) only they are in different structures. So basically if we figure out how to jump from one structure to another we have an algorithm. Let's go back to our example: going up from node 00 (S0). We should end up in S2; so again 00->10 flipping the MSB. So if we apply the same algorithm we use within the structure we should be fine.

Bottom line: valid transition within a strucutres

MSB 0, go down
  1, go up
LSB 0, go right
  1, go left

for invalid transitions (like MSB 0, go up)
determine the neighbor structure by flipping the MSB for vertical and LSB for vertical
and get the neighbor you are looking for by transforming a illegal move in structure A
into a legal one in strucutre B-> S0: MSB 0 up, becomes S2:MSB 0 down.   

I hope this idea is explicit enough

Pandrei
  • 4,843
  • 3
  • 27
  • 44
  • Yes, this is what I am trying to do inside of walk. It does help to see it written out by someone else. – jett Feb 07 '14 at 10:14
  • so I rewrote my algorithm slightly differently and I am getting further with it, it has solved some of the problems of my old one (but not all). – jett Feb 09 '14 at 21:55
  • It seems to happen when you would have to move up and to the right. So, [3][0]{LEFT, LEFT} correctly gives [0][3] but [0][3]{RIGHT, RIGHT} gives [0][3] whereas it should be [3][0]. Seems to happen when moving right in a higher section. Having the parent "explode" upwards isn't working as well but that is fine for now I guess. – jett Feb 10 '14 at 13:10
  • I'm having a bit of difficulty following; what is [3][0]{LEFT,LEFT} and what is [0][3] {RIGHT,RIGHT} – Pandrei Feb 10 '14 at 13:39
  • Sorry, the picture I have above should show it. But [3][0] would be top left square of bottom right square, [0][3] would be bottom right square of top left square, and the left/right is direction so {X, Y}, so going RIGHT RIGHT from [0][3] should bring you to [3][0] – jett Feb 10 '14 at 14:30
  • Ok, I got the [x][y] notation but still don't get the directions: [0][3] RIGHT get's you to [1][2], And RIGHT again get's you to [1][3]. So [0][3] RIGHT RIGHT => [1][3]. Why should it be [3][0]?? that should be [0][3] RIGHT DOWN... – Pandrei Feb 10 '14 at 15:04
  • If you were moving both on the X and Y axis. [1][2] would be only to the right along the X axis. Right down is correct, everything is just modeled in left/center/right. – jett Feb 11 '14 at 00:18
  • ok, so when you say {RIGHT, RIGHT} you mean RIGHT on the X axis and down is RIGHT on the Y axis. – Pandrei Feb 11 '14 at 09:48
  • since [0][3] {RIGHT, RIGHT} gives [0][3] it sounds like you are not moving :) can you point to the place in the code where this functionality is implemented... – Pandrei Feb 11 '14 at 09:49
  • That is correct, and yes :) In the traverse function it ends up setting deeper to false too early. The issue has to be with combining the directions but giant brainfart as to why that is the case. – jett Feb 11 '14 at 13:02
  • I can try an have a look if you point me to the right place; where can I find your code? or at leas the function which handles this part? – Pandrei Feb 11 '14 at 14:35
  • Excuse me, it is in the `traverse` function. – jett Feb 12 '14 at 00:43
1

Check out this answer for neighbor search in octrees: https://stackoverflow.com/a/21513573/3146587. Basically, you need to record in the nodes the traversal from the root to the node and manipulate this information to generate the required traversal to reach the adjacent nodes.

Community
  • 1
  • 1
user3146587
  • 4,250
  • 1
  • 16
  • 25
  • Thank you for the link; with this structure there is not a static root (and updating all nodes for a new root is prohibitively expensive). I must do a local approach going upwards from the current node rather than top down. – jett Feb 04 '14 at 22:32
  • Then you can generate what's called "nD index" in the answer I referenced (the record of the traversal from root to the node) by walking up until the root is found. And the computation of the "nD index" of the desired neighbor still applies, as well as the corresponding traversal to find this neighbor. – user3146587 Feb 04 '14 at 22:36
  • I don't like the worst case implications of going to root each time; I know that it is possible to not have to go all of the way up if you have the position in your node's parent's list of nodes available e.g. parent, position, and children. There is about 1/2^n chance of having to actually go higher each time, so going the full route by default is a lot of extra work. – jett Feb 04 '14 at 22:45
  • You may not have to go all the the way up to the root with the "nD index" solution. When the "nD index" of the query node is fully known, this index is simply incremented/decremented to get the "nD index" of the node neighbor. Now as you walk up the tree from the query node, you are progressively recovering this "nD index". As soon as an increment/decrement of this partial "nD index" does not carry/borrow (in base 2) anymore, then you can stop moving up and start moving down. – user3146587 Feb 04 '14 at 23:07
0

The simplest answer I can think of is to get back your node from the root of your tree.

Each cell can be assigned a coordinate mapping to the deepest nodes of your tree. In your example, the (x,y) coordinates would range from 0 to 2dimension-1 i.e. 0 to 3.

First, compute the coordinate of the neighbour with whatever algorithm you like (for instance, decide if a right move off the edge should wrap to the 1st cell of the same row, go down to the next row or stay in place).

Then, feed the new coordinates to your regular search function. It will return the neighbour cell in dimension steps.

You can optimize that by looking at the binary value of the coordinates. Basically, the rank of the most significant bit of difference tells you how many levels up you should go.

For instance, let's take a quadtree of depth 4. Coordinates range from 0 to 15.

Assume we go left from cell number 5 (0101b). The new coordinate is 4 (0100b). The most significant bit changed is bit 0, which means you can find the neighbour in the current block.

Now if you go right, the new coordinate is 6 (0110b), so the change is affecting bit 1, which means you have to go up one level to access your cell.

All this being said, the computation time and volume of code needed to use such tricks seems hardly worth the effort to me.

kuroi neko
  • 8,479
  • 1
  • 19
  • 43