AlgoMaster Logo

Range Module

hardFrequencyUpdated September 21, 2026

Understanding the Problem

We need to maintain a dynamic collection of non-overlapping intervals and support three operations: adding a range (merging it with any overlapping intervals), removing a range (splitting intervals if necessary), and querying whether a range is fully covered.

The difficult part is handling the merging and splitting correctly. When we add [10, 20) and then remove [14, 16), our collection becomes {[10, 14), [16, 20)}. If we then add [13, 18), we need to merge it with the existing pieces to get {[10, 20)} again.

The value range goes up to 10^9, so we can't use a boolean array indexed by value: a billion-entry array is too large. We need to store intervals and detect overlap without touching every integer in a range. The number of operations is at most 10^4, which leaves room for approaches that are O(n) per operation, as long as they stay correct under merging and splitting.

Key Constraints:

  • 1 <= left < right <= 10^9: the coordinate space is too large to index directly, so we store intervals rather than individual values.
  • At most 10^4 calls: with n = 10^4 operations, an O(n) per-operation approach does about 10^8 total work, which runs in time. An O(log n) per-operation approach has plenty of margin.

Approach 1: Brute Force with Sorted List

Intuition

Maintain a list of non-overlapping intervals sorted by start position. For each operation, scan the list to find which intervals overlap the given range, then merge, split, or check as needed.

The invariant is that the intervals are always sorted by start and never overlap. As long as addRange and removeRange rebuild the list to preserve that property, queryRange can decide coverage by finding a single interval that contains the query: if any interval starts at or before left and ends at or after right, the range is fully tracked. No two intervals can ever combine to cover a range, because a gap between them means some value is untracked.

Algorithm

For addRange(left, right):

  1. Scan through all intervals. For each interval [a, b) that overlaps with [left, right) (i.e., a < right && b > left), extend left to min(left, a) and right to max(right, b).
  2. Remove all overlapping intervals from the list.
  3. Insert the merged interval [left, right) at the correct sorted position.

For queryRange(left, right):

  1. Scan through all intervals. If any interval [a, b) satisfies a <= left && b >= right, the query range is fully covered. Return true.
  2. If no single interval covers the entire range, return false.

For removeRange(left, right):

  1. Build a new list. For each existing interval [a, b): if it doesn't overlap, keep it. If it does overlap, add [a, left) if a < left, and [right, b) if b > right.
  2. Replace the old list with the new one.

Example Walkthrough

1Initial state: no intervals tracked
[]
1/6

Code

This approach scans every interval on every operation. Since the intervals are sorted, binary search can jump directly to the relevant ones instead of starting from the front.

Approach 2: TreeMap (Sorted Map with Binary Search)

Intuition

The intervals are already sorted and non-overlapping, so a linear scan repeats work that binary search avoids. A TreeMap (or equivalent sorted map) keyed by interval start lets us jump directly to the interval that matters in O(log n).

For a range [left, right), the only interval that can overlap its left edge is the one whose start is the largest value at or before left. A TreeMap's floorKey operation returns exactly that key in O(log n), which replaces the front-to-back scan of the brute force.

Algorithm

For addRange(left, right):

  1. Use floorKey(left) to find the interval starting at or before left. If its end >= left, it overlaps, so extend left to min(left, its start).
  2. Use floorKey(right) to find the interval starting at or before right. If its end > left, extend right to max(right, its end).
  3. Remove all entries from the map whose keys fall in [left, right).
  4. Insert the merged interval with key left and value right.

For queryRange(left, right):

  1. Use floorKey(left) to find the interval starting at or before left.
  2. If such an interval exists and its end >= right, return true. Otherwise, return false.

For removeRange(left, right):

  1. Save portions of overlapping intervals that extend beyond the removal range.
  2. Remove all entries whose keys fall in [left, right).
  3. Reinsert the preserved portions.

Example Walkthrough

1Initial state: empty TreeMap
1/7

Code

The TreeMap gives O(log n) queries, but add and remove can still cost O(n) when a single call merges or splits many intervals. A segment tree bounds every operation by the depth of the tree instead.

Approach 3: Segment Tree with Lazy Propagation

Intuition

A segment tree supports range updates and range queries in O(log n). The obstacle here is that the value range goes up to 10^9, so building a full tree over every coordinate upfront would allocate billions of nodes. A dynamic segment tree (also called an implicit segment tree) avoids that by creating a node only when an operation first touches its subrange.

The tree covers [1, 10^9). Each node represents a subrange and stores whether that entire subrange is tracked. An update marks a range as tracked or untracked and uses lazy propagation to defer pushing the change to children until a later operation visits them. A query reports whether every part of the query range is tracked.

Each operation visits at most O(log C) nodes, where C = 10^9 is the size of the coordinate space. Since log2(10^9) is about 30, an operation touches at most around 30 levels regardless of how many intervals exist.

Algorithm

  1. Create a dynamic segment tree with root covering [1, 10^9).
  2. Each node stores: tracked (whether the entire range is fully tracked), lazy (a pending update: 1 = mark as tracked, -1 = mark as untracked, 0 = no pending), and left/right child pointers (created on demand).
  3. For addRange(left, right): do a range update marking [left, right) as tracked.
  4. For removeRange(left, right): do a range update marking [left, right) as untracked.
  5. For queryRange(left, right): do a range query checking if the entire [left, right) is tracked.

Example Walkthrough

1Initial: root covers [1, 10^9), all untracked
[1, 10^9)
root
untracked
1/6

Code