Overview
The Chtholly Tree, also known as ODT (Old Driver Tree), gained popularity through Codeforces problem 896C.
It's crucial to understand that this approach is fundamentally based on color segment amortization for random data, rather than being a strict data structure. The operations described below represent specific implementations of this concept.
Prerequisites
Basic familiarity with STL's set container is sufficient.
Core Idea
The key insight is to merge consecutive segments with identical values into single nodes stored within a set structure.
Applications
Use case: Quick solution for problems involving range assignment operations. This technique typically performs well on random data but may become inefficient on specially crafted test cases where data randomness cannot be guaranteed.
For correctness guarantees, data must be random. Formal complexity proofs are available in Codeforces discussions regarding Chthol Tree analysis.
Rigorous analysis shows that for add, assign, and sum operations, the set-based implementation achieves O(n log log n) complexity, while the linked-list version maintains O(n log n).
Implementation Details
Node Structure
struct Segment {
int start, end;
mutable int value;
Segment(int s, int e, int val) : start(s), end(e), value(val) {}
bool operator<(const Segment& other) const {
return start < other.start;
}
};
The value field stores user-defined metadata.
Understanding the mutable Keyword
The mutable keyword enables modification of member variables even within const member functions. In C++, this specifier allows a data member to be altered regardless of the constness of the containing object. It can only be used with non-static class members.
This permits direct updates to value for elements already inserted into the set without requiring removal and reinsertion.
Set Declaration
std::set<Segment> segments;
using Iterator = std::set<Segment>::iterator;
// or simply use 'auto' in C++11 and later
The split Operation
The split operation is fundamental. It divides a segment containing position pos into two parts: [start, pos-1] and [pos, end], returning an iterator to the latter segment.
Iterator split(int pos, int maxLimit) {
if (pos > maxLimit) return segments.end();
auto it = std::prev(segments.upper_bound(Segment{pos, 0, 0}));
if (it->start == pos) return it;
int segmentStart = it->start;
int segmentEnd = it->end;
int segmentValue = it->value;
segments.erase(it);
segments.insert(Segment(segmentStart, pos - 1, segmentValue));
return segments.insert(Segment(pos, segmentEnd, segmentValue)).first;
}
This operation transforms any range query [l, r] into a half-open interval [split(l), split(r + 1)) on the set.
The assign Operation
The assign operation assigns a uniform value across a specified range. This operation is critical for maintaining complexity, as it reduces the total number of segments in the structure.
void assign(int left, int right, int newValue, int bound) {
Iterator itr = split(right + 1, bound);
Iterator itl = split(left, bound);
segments.erase(itl, itr);
segments.insert(Segment(left, right, newValue));
}
Range Operations
For other range operations, simply iterate over the segments:
void processRange(int left, int right, int bound) {
Iterator itr = split(right + 1, bound);
Iterator itl = split(left, bound);
for (; itl != itr; ++itl) {
// Custom operations on each segment
}
}
Important: When performing range queries, always execute split on the right boundary before the left boundary. Splitting the left boundary first may invalidate the returned iterator when the right boundary is subsequently split, potentially causing runtime errors.