I need to represent a directed rooted tree in memory. The caveat is, that nodes have properties.
And those properties are inherited (but not 100%) by the child nodes, recursively.
What would be a good data structure and algorithms for storing this in memory?
For the purposes of notation:
- D(V) - depth of the vertex (distance from the root).
- A(V) - arity of the vertex (how many children it has).
- V - Size of the tree (how many total vertices)
- P - average # of properties inherited by a vertex.
The operations that need to be supported (efficiently) are:
Modifying the tree (add node/subtree; remove node/subtree, move node/subtree)
Tree modifications occur not too frequently, perhaps 1-3% of the tree changes throughout the day.
Change (add/delete/modify) a property on a node, and have all its descendents inherit that property according to inheritance rules:
a property is inherited by default.
A property itself has 2 attributes: "propagate" and "turned off".
A property is inherited from parent to child if (and only if): Parent node's property has "propagate" flag on AND the child node does NOT have the same property set with "turned off" attribute.
Query out properties of a given node (this is a very frequent query, massively dominating other access patterns by frequency)
Efficiently query out properties of a set of nodes
This means that, if we query out properties of N nodes sharing same parent, at depth D, the complexity would be O((N+D)P) instead of O(DN*P)
This query is frequent but significantly less so than the single-node one. BUT, the problem is that it would frequently be run on the entire tree or a very large portion of it, and the bulk speedup matters due to V being very large.
Efficiently query out all nodes (in whole tree or given subtree) that have a specific property (either set directly or inherited) that is not turned off.
This means that it should be much faster than O(V*P).
Access pattern: less frequent than the other two, BUT needs to be very fast because it's called by user-facing UI and thus for user experience purposes needs to be responsive.
I have tried implementing 3 approaches, but all of them suffer from different issues due to the seemingly irreconcilable trade-offs:
Approach 1:
Store the tree as a regular tree (possibly with enhancements to speed things up, such as doubly-linked edges to facilitate walking up the tree, and a hashtable to be able to find a given node in the tree in O(1).
Store properties with the nodes they are set on
Compute the properties inherited by nodes, by walking UP the tree from the node to find the path from root to the node, then walking back down that path and seeing which properties would propagate from root to child, then to grandchild... and so on till the node.
PROBLEM: Computing properties on a set of nodes is slow. Dumb implementation results in O(PDN) although I have a feeling this can be improved.
PROBLEM: Finding out which nodes inherit a specific property in the whole tree is VERY slow and I don't see a good way to optimize it.
This implementation results in O(V*P) complexity, which is unacceptable.
Approach 2:
Store the tree as a regular tree (possibly with enhancements to speed things up, such as doubly-linked edges to facilitate walking up the tree, and a hashtable to be able to find a given node in the tree in O(1).
Store properties with the nodes they are set on, AND separately store (cache) the inherited properties of each node.
This solution makes finding properties inherited by a node O(P) and a set of nodes O(P*N) very efficient.
PROBLEM: Tree manipulation is slow. Basically, you need to re-compute O(PD + PN) (where D is a depth of a node where change occurred and N is amount of nodes being added/moved)
PROBLEM: Finding out which nodes inherit a specific property in the whole tree is VERY slow and I don't see a good way to optimize it.
This implementation results in O(V*P) complexity, where P is average # of properties inherited on a node and V is # of vertices on the whole tree.
Approach 3:
Store the tree as a regular tree (possibly with enhancements to speed things up, such as doubly-linked edges to facilitate walking up the tree, and a hashtable to be able to find a given node in the tree in O(1).
Store properties with the nodes they are set on, AND separately store (cache) the inherited properties of each node.
Also, have a separate cache linking properties to a list of nodes they are inherited in (call it "property cache").
This solution makes finding properties inherited by a node O(P) and a set of nodes O(P*N) very efficient.
PROBLEM: Tree manipulation is extremely slow. Basically, you need to re-compute O(PD + PN) (where D is a depth of a node where change occurred and N is amount of nodes being added/moved) AND then also spend time refreshing properties cache that can also be O(P*N) since you have to rebuild entire cache.
PROBLEM: Changing properties is also very slow because you also need to rebuild the entire properties cache for a changed property.