Algorithmics study and revision notes
2026-08-22
A compressed file saves storage, but a query may require decompressing it first. A succinct data structure aims for both: space close to the information-theoretic minimum and direct support for useful operations.
This matters when topology is large. A pointer-based tree may spend more space on addresses than on its values, and scattered nodes make poor use of caches. A compact bit sequence can hold the same topology in a few bits per node while still supporting parent, child, depth, and subtree queries.
Succinctness is always relative to a stated object family and interface. Encoding only a trie’s shape does not also encode edge characters, terminal markers, or values.
After studying this note, you should be able to:
findclose and subtree-size
operations;For a first pass, follow one dependency chain: counting lower bounds, the word-RAM model, rank/select, and then balanced parentheses and LOUDS. DFUDS and the heap-like binary encoding are useful comparisons after that core chain is clear.
Suppose a family contains possible objects. Any lossless representation needs at least
bits in the worst case, because fewer bits provide fewer than distinct codewords.
Terminology varies slightly across the literature, but a useful hierarchy is:
The lower bound must count the right objects. There are
binary-tree shapes with internal nodes. These are Catalan numbers, and
Thus roughly two bits per node are necessary just to distinguish arbitrary binary-tree shapes. A representation using bits is genuinely near the lower bound; two 64-bit child pointers per node are not.
The standard static results use a word-RAM with word size . Arithmetic, bitwise Boolean operations, and shifts on one word take constant time. Small lookup tables shared by the whole structure are permitted.
Static and dynamic structures have different trade-offs. The -bit, constant-time rank/select results below concern a fixed bit vector. Supporting insertions and deletions into the middle of that vector requires more machinery and usually weaker bounds.
Let
be a bit vector. In this note, rank uses a zero-based
inclusive position:
For an occurrence number ,
Thus rank maps a position to an occurrence count, while select maps an occurrence count back to a position. Define and analogously.
Many libraries instead define on the half-open prefix . Under that convention, this note’s inclusive value at is the library’s value at . Check the boundary and occurrence-number conventions before comparing formulas or test results.
For
index: 0 1 2 3 4 5 6 7 8 9 10 11
B: 0 1 1 0 1 0 0 1 0 1 1 1
we have , , and . Checking a small vector by hand is the best way to catch an off-by-one convention.
Storing every prefix count would cost bits. Instead, use two sampling levels:
Superblock counters use about
bits. Relative subblock counters and the shared table also use bits with suitable constants. The original vector plus index therefore occupies bits and answers rank in time. Select uses a related sampling scheme; the construction is more involved but reaches the same asymptotic bounds.
The index is part of the representation’s space. Calling a raw bit string “succinct” while ignoring a large query index is incomplete accounting.
Rank and select are the navigation primitives, not the final application. A tree encoding turns structural questions into a small number of rank, select, and excess queries on its topology bit vector.
An ordered rooted tree distinguishes the order of a node’s children. Tree encodings below describe that topology. Applications usually need more:
These components should be reported separately. A trie can share many prefixes, so its number of nodes is at most, not necessarily equal to, the total number of characters in its keys.
Perform a depth-first traversal of an ordered rooted tree. Write
( when entering a node and ) when leaving it.
Equivalently, write 1 for an opening parenthesis and 0 for a closing
parenthesis. An
-node
tree produces exactly
bits.
The running excess
is the number of currently open nodes after position . For an opening parenthesis at position , the node depth is when the root has depth zero.
If findclose(i) returns the matching closing
parenthesis, the complete encoding of that node’s subtree is the
contiguous interval from
through findclose(i). Therefore
The parent is represented by the nearest opening parenthesis that encloses the node’s pair. A first child begins immediately after the node’s opening parenthesis if that next symbol is open; a next sibling begins immediately after its matching close if that next symbol is also open.
A simple linear scan is enough to validate the representation:
findclose(B, i):
require B[i] == '('
balance = 1
j = i + 1
while balance > 0:
if B[j] == '(': balance += 1
else: balance -= 1
j += 1
return j - 1
This scan can take . A succinct excess-search index adds bits and supports matching, enclosing, depth, parent, child, and many other navigation operations in constant time.
LOUDS stands for level-order unary degree sequence. Visit nodes in breadth-first order. For a node of degree , write one-bits followed by a zero. The degrees sum to , so a nonempty -node tree uses bits.
Suppose a root has children and , and has one child . Breadth-first degrees are , hence
110 10 0 0 -> 1101000
Zeros delimit nodes; one-bits introduce their children in breadth-first order.
For the following formulas only, use one-based bit positions and one-based node numbers. Let
Here counts -bits in positions through , with for .
Therefore .
The one-bits between positions and represent the children of node . A one-bit of rank introduces node . For a non-root node , let ; then
If , its children have consecutive node numbers beginning at
LOUDS supports parent, degree, child, and sibling navigation naturally in breadth-first numbering. A subtree is not generally one contiguous LOUDS interval, so subtree-size queries are less direct.
DFUDS writes the same unary degree code, , but visits nodes in depth-first preorder, with a conventional extra opening bit to align its navigation formulas. Subtrees are contiguous because depth-first traversal finishes one subtree before entering the next.
With rank/select and balanced-parentheses primitives, DFUDS can support parent, degree, child, sibling, and subtree-size operations in constant time using bits. It combines unary-degree information with depth-first locality.
For a binary tree, label internal nodes 1 and external null nodes 0. Emit the root label, then, for each internal node in breadth-first order, emit the labels of its left and right children; null nodes emit no children. There are internal and external nodes, so the sequence uses bits.
Let be this sequence. If an internal node occurs at bit position , its breadth-first internal-node number is , and its two child labels occur at bit positions and . Conversely, a non-root internal node at bit position has parent number , whose bit position is
The multiplication applies to the compact internal-node number , not directly to the node’s bit position . Rank and select provide exactly that translation.
| Encoding | Order | Topology bits | Natural strengths | Main caution |
|---|---|---|---|---|
| Balanced parentheses | depth first | matching, depth, ancestors, subtree interval | needs excess-search index | |
| LOUDS | breadth first | degree, parent, child range, level order | subtree not contiguous | |
| DFUDS | depth first | about | degree navigation plus subtree locality | formulas are convention-sensitive |
| Heap-like binary | level order | binary child/parent mapping | includes explicit external nodes |
No encoding is universally best. Choose the operation interface first, then the representation and auxiliary index that support it.
Consider keys a, ac, and b.
The topology has four nodes: a root with children a and
b, and node a has child c. The
labels a,b,c and terminal markers for all three keys are
payload; the topology alone does not contain them.
Depth-first balanced parentheses are
((())())
01234567
Positions 0, 1, 2, and 5 open the root, a,
c, and b. For the prefix node a
at position 1, findclose(1)=4. Its subtree size is
corresponding to nodes a and c. Its next
symbol is another opening parenthesis, so it has a child. A fast
implementation replaces the scan with a matching/excess index but
returns the same structural answer.
LOUDS for the same topology uses degrees
and is 1101000. The two encodings describe the same tree in
different traversal orders.
| Task | Simple representation | Succinct target |
|---|---|---|
| Store ordered-tree topology | pointer words per node | bits |
| Bit-vector rank/select | scan: time | time, bits total |
| Parenthesis matching | scan: time | with -bit index |
| Construction | often straightforward | may need temporary workspace |
| Dynamic updates | local pointer change | substantially more difficult |
Compact storage can improve cache behaviour and reduce I/O, but extra bit operations and construction complexity can dominate on small instances. Measure the complete structure, including alignment, allocator overhead, labels, and temporary workspace.
findclose reveal subtree size?bitarray
package provides compact bit storage and fast C-level bit operations. It
is a useful prototype substrate, but it does not by itself add the
rank/select directory required for constant-time succinct queries.sux provides
composable bit vectors, several rank/select trade-offs, Elias-Fano
dictionaries, and basic balanced-parentheses support.
Performance-oriented unchecked operations and nested support structures
make boundary conventions and total memory accounting especially
important.popcnt and
BMI2, so portable and native builds can have materially different
timings.bitarray to implement naive inclusive
rank1 and one-based select1. Exhaustively test
all bit vectors up to length 12, then adapt the calls to one library
above and verify the half-open/inclusive translation at every
boundary.