red-black-tree-typed
Version:
634 lines (336 loc) • 12 kB
text/typescript
/**
* data-structure-typed
*
* @author Pablo Zeng
* @license MIT License
*/
export type SegmentTreeOptions<E> = {
merger: (a: E, b: E) => E;
identity: E;
};
/**
* Generic Segment Tree with flat array internals.
*
* Supports any associative merge operation (sum, min, max, gcd, etc.).
* Reference: AtCoder Library segtree<S, op, e>.
*
* @example
* ```ts
* const sumTree = SegmentTree.sum([1, 2, 3, 4, 5]);
* sumTree.query(1, 3); // 9 (2+3+4)
* sumTree.update(2, 10); // [1, 2, 10, 4, 5]
* sumTree.query(1, 3); // 16 (2+10+4)
*
* const minTree = SegmentTree.min([5, 2, 8, 1, 9]);
* minTree.query(0, 4); // 1
* ```
*/
export class SegmentTree<E = number> implements Iterable<E> {
protected readonly _merger: (a: E, b: E) => E;
protected readonly _identity: E;
protected _n: number; // number of leaf elements
protected _tree: E[]; // flat array, 1-indexed, size 2*_size
protected _treeSize: number; // internal tree size (next power of 2 >= _n)
constructor(elements: E[], options: SegmentTreeOptions<E>) {
this._merger = options.merger;
this._identity = options.identity;
this._n = elements.length;
// Round up to next power of 2
this._treeSize = 1;
while (this._treeSize < this._n) this._treeSize <<= 1;
// Allocate and fill with identity
this._tree = new Array(2 * this._treeSize).fill(this._identity);
// Place elements in leaves
for (let i = 0; i < this._n; i++) {
this._tree[this._treeSize + i] = elements[i];
}
// Build internal nodes bottom-up
for (let i = this._treeSize - 1; i >= 1; i--) {
this._tree[i] = this._merger(this._tree[2 * i], this._tree[2 * i + 1]);
}
}
// ─── Convenience factories ─────────────────────────────────
/**
* Create a sum segment tree.
* @example
* ```ts
* const st = SegmentTree.sum([1, 2, 3, 4, 5]);
* st.query(0, 2); // 6 (1+2+3)
* st.update(1, 10);
* st.query(0, 2); // 14 (1+10+3)
* ```
*/
static sum(elements: number[]): SegmentTree<number> {
return new SegmentTree<number>(elements, {
merger: (a, b) => a + b,
identity: 0
});
}
/**
* Create a min segment tree.
* @example
* // Temperature monitoring with range queries
* // Hourly temperatures for a day (24 readings)
* const temps = [18, 17, 16, 15, 16, 18, 21, 24, 27, 29, 31, 32, 33, 32, 31, 29, 27, 25, 23, 21, 20, 19, 18, 17];
* const tree = SegmentTree.sum(temps);
*
* // Average temperature during work hours (9-17)
* const workSum = tree.query(9, 17);
* console.log(workSum / 9); // toBeCloseTo;
*
* // Sum of morning temps (6-11)
* console.log(tree.query(6, 11)); // 164;
*/
static min(elements: number[]): SegmentTree<number> {
return new SegmentTree<number>(elements, {
merger: (a, b) => Math.min(a, b),
identity: Infinity
});
}
/**
* Create a max segment tree.
* @example
* ```ts
* const st = SegmentTree.max([3, 1, 4, 1, 5]);
* st.query(1, 4); // 5
* ```
*/
static max(elements: number[]): SegmentTree<number> {
return new SegmentTree<number>(elements, {
merger: (a, b) => Math.max(a, b),
identity: -Infinity
});
}
// ─── Core operations ───────────────────────────────────────
/**
* Point update: set element at index to value.
* Time: O(log n)
* @example
* // Dynamic range sum with updates
* // Monthly revenue data (in thousands)
* const revenue = [120, 95, 140, 110, 85, 130, 150, 100, 160, 125, 90, 175];
* const tree = SegmentTree.sum(revenue);
*
* // Q1 revenue (Jan-Mar)
* console.log(tree.query(0, 2)); // 355;
*
* // Update March revenue from 140 to 200
* tree.update(2, 200);
*
* // Q1 revenue after update
* console.log(tree.query(0, 2)); // 415;
*
* // H1 revenue (Jan-Jun)
* console.log(tree.query(0, 5)); // 740;
*/
update(index: number, value: E): void {
if (index < 0 || index >= this._n) return;
let pos = this._treeSize + index;
this._tree[pos] = value;
// Propagate up
pos >>= 1;
while (pos >= 1) {
this._tree[pos] = this._merger(this._tree[2 * pos], this._tree[2 * pos + 1]);
pos >>= 1;
}
}
/**
* Range query: returns merger result over [start, end] (inclusive).
* Time: O(log n)
* @example
* // Range sum query on an array
* const tree = SegmentTree.sum([1, 3, 5, 7, 9, 11]);
*
* // Query sum of range [1, 3] → 3 + 5 + 7 = 15
* console.log(tree.query(1, 3)); // 15;
*
* // Query entire range
* console.log(tree.query(0, 5)); // 36;
*
* // Query single element
* console.log(tree.query(2, 2)); // 5;
*/
query(start: number, end: number): E {
if (start < 0) start = 0;
if (end >= this._n) end = this._n - 1;
if (start > end) return this._identity;
let resultLeft = this._identity;
let resultRight = this._identity;
let left = this._treeSize + start;
let right = this._treeSize + end + 1; // exclusive
while (left < right) {
if (left & 1) {
resultLeft = this._merger(resultLeft, this._tree[left]);
left++;
}
if (right & 1) {
right--;
resultRight = this._merger(this._tree[right], resultRight);
}
left >>= 1;
right >>= 1;
}
return this._merger(resultLeft, resultRight);
}
/**
* Get element at index.
* Time: O(1)
* @example
* // Point access on segment tree
* const st = SegmentTree.sum([10, 20, 30, 40]);
* console.log(st.get(0)); // 10;
* console.log(st.get(2)); // 30;
*/
get(index: number): E {
if (index < 0 || index >= this._n) return this._identity;
return this._tree[this._treeSize + index];
}
// ─── Binary search on tree (ACL-style) ─────────────────────
/**
* Find the largest r such that predicate(query(left, r)) is true.
* Returns left-1 if predicate(identity) is false.
* Returns n-1 if predicate holds for the entire range [left, n-1].
* Time: O(log n)
* @example
* // Find rightmost position where predicate holds
* // Prefix sums: find the rightmost index where prefix sum < 10
* const st = SegmentTree.sum([3, 1, 4, 1, 5]);
* // maxRight(0, sum => sum < 10) — prefix [3,4,8,9,14]
* // sum < 10 holds through index 3 (prefix=9), fails at 4 (prefix=14)
* const result = st.maxRight(0, sum => sum < 10);
* console.log(result); // 3;
*/
maxRight(left: number, predicate: (segValue: E) => boolean): number {
if (left >= this._n) return this._n - 1;
let acc = this._identity;
if (!predicate(acc)) return left - 1;
let pos = this._treeSize + left;
// Go up while we're a right child or predicate still holds
while (true) {
// Find the lowest relevant node
while (pos < this._treeSize) {
// Try going left
const combined = this._merger(acc, this._tree[2 * pos]);
if (predicate(combined)) {
acc = combined;
pos = 2 * pos + 1; // go right
} else {
pos = 2 * pos; // go left (dig deeper)
}
}
// At leaf level
const combined = this._merger(acc, this._tree[pos]);
if (!predicate(combined)) {
return pos - this._treeSize - 1;
}
acc = combined;
// Move to next segment
pos++;
// Check if we've gone past the end
if (pos - this._treeSize >= this._n) return this._n - 1;
// Go up while we're a right child
while (pos > 1 && (pos & 1) === 0) {
pos >>= 1;
}
/* istanbul ignore next -- defensive: pos===1 unreachable when _n < _treeSize guard above catches exit */
if (pos === 1) return this._n - 1;
}
}
/**
* Find the smallest l such that predicate(query(l, right)) is true.
* Returns right+1 if predicate(identity) is false.
* Returns 0 if predicate holds for the entire range [0, right].
* Time: O(log n)
* @example
* // Find leftmost position where predicate holds
* const st = SegmentTree.sum([3, 1, 4, 1, 5]);
* // minLeft(5, sum => sum < 7) — suffix sums from right
* // From right: [5]=5 < 7, [1,5]=6 < 7, [4,1,5]=10 ≥ 7
* const result = st.minLeft(5, sum => sum < 7);
* console.log(result); // 3;
*/
minLeft(right: number, predicate: (segValue: E) => boolean): number {
if (right < 0) return 0;
if (right >= this._n) right = this._n - 1;
let acc = this._identity;
if (!predicate(acc)) return right + 1;
let pos = this._treeSize + right;
while (true) {
while (pos < this._treeSize) {
const combined = this._merger(this._tree[2 * pos + 1], acc);
if (predicate(combined)) {
acc = combined;
pos = 2 * pos; // go left
} else {
pos = 2 * pos + 1; // go right (dig deeper)
}
}
const combined = this._merger(this._tree[pos], acc);
if (!predicate(combined)) {
return pos - this._treeSize + 1;
}
acc = combined;
// Move to previous segment
if (pos === this._treeSize) return 0;
pos--;
// Go up while we're a left child
while (pos > 1 && (pos & 1) === 1) {
pos >>= 1;
}
/* istanbul ignore next -- defensive: pos===1 unreachable when _treeSize guard above catches exit */
if (pos === 1) return 0;
}
}
// ─── Standard interface ────────────────────────────────────
get size(): number {
return this._n;
}
isEmpty(): boolean {
return this._n === 0;
}
clone(): SegmentTree<E> {
const elements: E[] = [];
for (let i = 0; i < this._n; i++) {
elements.push(this._tree[this._treeSize + i]);
}
return new SegmentTree<E>(elements, {
merger: this._merger,
identity: this._identity
});
}
toArray(): E[] {
const result: E[] = [];
for (let i = 0; i < this._n; i++) {
result.push(this._tree[this._treeSize + i]);
}
return result;
}
/**
* Iterates over leaf values in index order.
*/
[Symbol.iterator](): IterableIterator<E> {
const tree = this._tree;
const treeSize = this._treeSize;
const n = this._n;
let i = 0;
return {
[Symbol.iterator]() {
return this;
},
next(): IteratorResult<E> {
if (i < n) {
return { value: tree[treeSize + i++], done: false };
}
return { value: undefined, done: true } as IteratorResult<E>;
}
};
}
forEach(callback: (value: E, index: number) => void): void {
for (let i = 0; i < this._n; i++) {
callback(this._tree[this._treeSize + i], i);
}
}
print(): void {
console.log(this.toArray());
}
}