Bahnasy Tree is a dynamic tree that represents a sequence of length
-
$T$ (threshold): the maximum fanout a node is allowed to have before we consider it “too wide”. -
$S$ (split factor): used when splitting; computed from the node size using the smallest prime factor (SPF). If$S > T$ , we fallback to$S = 2$ .
The tree supports the usual sequence operations (find by index, insert, delete) and can be extended to support range query / range update by storing aggregates (sum, min, etc.) and optionally using lazy propagation.
Each node represents a contiguous block of the sequence. If a node represents a block of length sz, then its children partition this block into consecutive sub-blocks whose lengths sum to sz.
The key invariant is: every node always knows the sizes of its children subtrees (how many leaves are inside each child).
Assume a node has children with subtree sizes:
Define the prefix sums:
To route an index
- Find the smallest
$j$ such that$p_j \ge i$ . - Go to that child (and convert
$i$ into the child-local index by subtracting$p_{j-1}$ ).
Because
Figure 1 (routing by prefix sums)
Start with a root node representing
For a node representing
- If
$n \le T$ : stop splitting and create$n$ leaves under it. - If
$n > T$ : split it into$S$ children:$S = \mathrm{SPF}(n)$ - If
$S > T$ , use$S = 2$ - Distribute the
$n$ elements among those$S$ children (almost evenly), then recurse.
This guarantees that during the initial build, every internal node has at most
Define:
-
$D$ : the depth (number of levels). - At each level, routing uses binary search on at most
$T$ children, so it costs$O(\log_2 T)$ .
Therefore:
In a “fully expanded”
So a direct expression is:
Now simplify using change-of-base:
Multiplying both sides by
So the final form used later is:
Insertion happens at leaf level. After inserting a new element, the “leaf-parent” (the node whose children are leaves) increases its number of children.
As long as the leaf-parent still has at most
When the leaf-parent exceeds
- A new intermediate layer is created between the leaf-parent and its leaves.
- The old leaves are grouped into
$S$ buckets (where$S$ is computed by the same SPF rule capped by$T$ ). - This restores a bounded fanout and keeps future routing efficient.
Figure 2 (local split)
A local split creates about
Insert cost is:
- Routing:
$O(\log_2 N)$ - Plus possible local overflow handling:
$O(T)$
So the worst-case insert is:
Deletion also consists of:
- Routing to the target:
$O(\log_2 N)$ - Removing it from the leaf-parent’s ordered child list (bounded by the same width idea), so
$O(T)$
So the worst-case delete is:
Repeated insertions concentrated in the same area can create a “deep path” because local splits keep adding intermediate layers.
Consider the worst case where local splits behave like
- After a local overflow, the leaves are grouped into 2 buckets of size about
$T/2$ . - To overflow one bucket again and force another local split deeper, it takes about
$T/2$ further insertions in that same region. - Therefore, each new additional level costs about
$T/2$ insertions.
If the current depth is
In the worst case (starting from
When the tree becomes invalid, it is rebuilt.
A rebuild does:
- Collect leaves into an array of size
$N$ . - Reconstruct the tree from scratch using the global split rule.
The rebuild time is proportional to the number of nodes created.
Leaf level:
Previous levels shrink geometrically, so the maximum total node count is bounded by:
Therefore rebuild cost is:
In the worst case, rebuild happens every
So number of rebuilds is approximately:
Each rebuild costs
For operation costs in the same worst-style view, insertion can be treated as
Thus a total bound is:
A common practical choice is:
Because plugging
and the operations term
so both terms become the same order:
In practice (based on testing), good values often fall in the range:
with higher values helping insertion-heavy workloads and lower values helping query-heavy workloads.
Worst-case behavior happens when a test is engineered to keep inserting in exactly the same place so the tree reaches a depth close to
Common mitigations:
- Randomize
$T$ each rebuild within a safe exponent range (e.g.$N^{0.27}$ to$N^{0.39}$ ). - Randomize the rebuild target (instead of rebuilding exactly at depth
$T$ , rebuild at$(0.5/1/2/3/4)\cdot T$ ).
Minimal structure-only implementation (split / split_local / find / insert / delete):
Full implementation (find/query/update/insert/delete + rebuild):
This repo contains multiple C++ solutions (Bahnasy Tree variants + competitors) and multiple test suites under Benchmarks/tests/.
To avoid WA from input-format mismatch, pick the correct code variant for the chosen test suite.
| Test suite | Allowed Bahnasy Tree files | Allowed competitor files | Important notes |
|---|---|---|---|
Benchmarks/tests/point update + query |
Any Bahnasy version that matches point-update I/O (including generic versions) | src/Competitors src/segTree_point_update.cpp, src/Competitors src/treap.cpp, src/Competitors src/treap_fast.cpp |
If you run a generic Bahnasy version, it must be configured to Sum aggregation (not Min/Xor/And/Or). Also verify the main() operation order matches this test format (usually: op=1 point set, op=2 range query). |
Benchmarks/tests/Range update + query |
Any Bahnasy version except src/Bahnasy Tree src/competitive programming/bahnasy_point_update.cpp |
src/Competitors src/segTree_range_update.cpp, src/Competitors src/treap.cpp, src/Competitors src/treap_fast.cpp |
Ensure the code supports range add + range query, and that main() parses the correct operation IDs for this suite. |
Benchmarks/tests/All operations |
All Bahnasy versions except the specialized bahnasy_point_update.cpp and bahnasy_range_update.cpp |
Only Treap variants: src/Competitors src/treap.cpp, src/Competitors src/treap_fast.cpp |
This suite mixes update/query/insert/delete/range-add (depending on the generator). The compared programs must implement the same full API and the same op numbering in main(). |
If you are benchmarking a generic Bahnasy file (example: src/Generic/bahnasy_generic_version.cpp), make sure the operation is set to Sum (not Min).
Use a SumAdd operation (sum combine + range add lazy) and set using Op = SumAdd; before running tests.
The scripts are interactive: they show numbered menus so you can choose:
- The C++ file(s) to compile/run.
- The test suite and which test groups to include.
This compares two implementations against each other (no .out files needed).
Windows (repo root):
py tools\run_stress_interactive.pyLinux/macOS (repo root):
python3 tools/run_stress_interactive.pyOutput:
reports/stress_<A>_VS_<B>_<timestamp>/results.csvreports/stress_<...>/summary.csv- For any
DIFF, it saves<test>.A.txtand<test>.B.txtto help debug.
This runs one implementation and compares output to the official answer files.
Windows (repo root):
py tools\run_benchmark_interactive.pyLinux/macOS (repo root):
python3 tools/run_benchmark_interactive.pyNotes:
-
This script expects outputs beside inputs (for example
A 40beside40, or40.out). -
If a test shows
NO_REF, it means the script couldn’t locate the expected output file for that input.

