A depth-first traversal can encode an ordered rooted tree by writing an opening parenthesis on entry to a node and a closing parenthesis on exit. Every subtree occupies one contiguous balanced interval. Its node count is half the interval length; an ancestor's interval contains every descendant interval. The example builds opening and closing positions and parent links for an organization hierarchy, then answers subtree size and ancestry directly. It finds a lowest common unit by walking parent links from one node and testing ancestors of the other. This is a teaching index with explicit dictionaries; a genuinely succinct representation would pack the bit sequence and add compact rank, select, and matching support.
Balanced-parentheses trees: encode an ordered hierarchy
Operational case
An operations root has dispatch and quality branches. Dispatch owns east and west, while quality owns audit. The encoding is ((()())(())); dispatch spans three nodes and contains east. The lowest common unit for east and audit is operations. Child order affects the encoding even when the parent relationships are unchanged, so writers must define that order before publication. The constructor rejects repeated or unreachable mapped nodes. This index is static: inserting a unit changes positions after it and requires rebuilding the encoding and its maps.
Working Python program
class OrganizationParentheses:
def __init__(self, root_id, children_by_id):
self.sequence = []
self.open_at = {}
self.close_at = {}
self.parent = {}
self.labels_at_open = {}
visited = set()
def visit(unit_id, parent_id):
if unit_id in visited:
raise ValueError("hierarchy must be a tree")
visited.add(unit_id)
opening = len(self.sequence)
self.sequence.append("(")
self.open_at[unit_id] = opening
self.labels_at_open[opening] = unit_id
self.parent[unit_id] = parent_id
for child_id in children_by_id.get(unit_id, []):
visit(child_id, unit_id)
self.close_at[unit_id] = len(self.sequence)
self.sequence.append(")")
visit(root_id, None)
if set(children_by_id) - visited:
raise ValueError("unreachable hierarchy node")
def subtree_size(self, unit_id):
return (self.close_at[unit_id] - self.open_at[unit_id] + 1) // 2
def contains_descendant(self, ancestor_id, unit_id):
return (self.open_at[ancestor_id] <= self.open_at[unit_id]
< self.close_at[unit_id] <= self.close_at[ancestor_id])
def lowest_common_unit(self, first_id, second_id):
ancestors = set()
while first_id is not None:
ancestors.add(first_id)
first_id = self.parent[first_id]
while second_id not in ancestors:
second_id = self.parent[second_id]
return second_id
if __name__ == "__main__":
tree = OrganizationParentheses("operations", {
"operations": ["dispatch", "quality"],
"dispatch": ["east", "west"],
"quality": ["audit"],
})
print("encoding:", "".join(tree.sequence))
print("dispatch subtree:", tree.subtree_size("dispatch"))
print("east under dispatch:", tree.contains_descendant("dispatch", "east"))
print("east and audit:", tree.lowest_common_unit("east", "audit"))Output
encoding: ((()())(()))
dispatch subtree: 3
east under dispatch: True
east and audit: operationsTime, space, and tradeoff
Building the sequence and explicit maps visits N nodes and edges once, using O(N) time and O(N) Python objects. Subtree size and interval-based ancestry are O(1) after construction. This direct lowest-common-unit query takes O(H) time and O(H) temporary set space for hierarchy height H; it does not claim constant-time succinct navigation. Storage is not 2N bits because the example retains strings, dictionaries, and labels. A binary-lifting table is preferable for many common-ancestor queries when O(N log N) auxiliary space is acceptable.
Common Mistakes
- Do not call an unpacked Python representation succinct.
- Do not confuse a parenthesis position with a node label.
- Do not use stale positions after a hierarchy edit.
- Do not claim this direct parent-walk common-ancestor query is constant time.
Connected lessons
- Trees and Heaps
- Data Structures
- Binary lifting: ancestors and common managers
- Bitvector rank and select: count and locate set bits
- Level-order unary degree tries: encode child runs as bits
- Projects
- Quizzes
Compare its update and query contract with Linear hashing: split one bucket at a time as a table grows, Compressed 2D Fenwick trees: toggle known points and count rectangles, Persistent two-list queues: fork FIFO dispatch history, then complete the structure audit and decision quiz.
Euler-tour RMQ: answer static common ancestors in constant query time adds a distinct structure contract to compare.
