Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

715. Range Module - Leetcode Solution

Problem Description

The Range Module problem requires you to design a data structure that efficiently tracks ranges of numbers (intervals) and supports three main operations:

  • addRange(left, right): Adds the half-open interval [left, right) to the structure. This means all numbers from left (inclusive) up to right (exclusive) are now tracked.
  • queryRange(left, right): Returns true if every number in the interval [left, right) is currently being tracked by the structure, and false otherwise.
  • removeRange(left, right): Stops tracking every number in the interval [left, right).

The intervals can overlap, and the structure must automatically merge or split intervals as needed. The input numbers can be very large, so efficiency is key. There is no restriction on how many times the operations are called.

Thought Process

At first glance, it might seem reasonable to store every individual number being tracked. However, this would be extremely inefficient, especially if the ranges are large. Instead, we should focus on storing the intervals themselves in a way that makes it easy to add, query, and remove ranges.

One way to do this is to keep a sorted list of non-overlapping intervals. When adding or removing a range, we merge or split intervals as needed. For queries, we check if the entire query interval is covered by existing intervals.

The main challenge is efficiently updating and querying these intervals, especially when there are many overlapping or adjacent intervals. Using a balanced search structure (like SortedList in Python, TreeMap in Java, or std::set in C++) helps us achieve fast lookups and updates.

Solution Approach

We will maintain a sorted list (or set) of non-overlapping intervals, where each interval is represented as a pair [start, end).

  • Adding a Range:
    • Find all intervals that overlap with the new range.
    • Merge them with the new range to form a single interval.
    • Remove the old intervals and insert the merged interval.
  • Querying a Range:
    • Find the interval that could cover the query range (using binary search or tree lookup).
    • Check if this interval fully contains the query range.
  • Removing a Range:
    • Find all intervals that overlap with the removal range.
    • For each overlapping interval, split it into at most two intervals: one before and one after the removal range.
    • Remove the old intervals and insert the new (split) intervals if they are non-empty.

This approach ensures that the interval list is always sorted and non-overlapping, allowing efficient updates and queries.

Example Walkthrough

Let's walk through an example:

  • Step 1: addRange(10, 20)
    • Current intervals: []
    • Add [10, 20): [[10, 20)]
  • Step 2: addRange(15, 25)
    • Current intervals: [[10, 20)]
    • Overlap with [10, 20). Merge to [10, 25)
    • Intervals: [[10, 25)]
  • Step 3: removeRange(14, 16)
    • Current intervals: [[10, 25)]
    • Overlap with [10, 25). Split into [10, 14) and [16, 25)
    • Intervals: [[10, 14), [16, 25)]
  • Step 4: queryRange(10, 14)
    • Interval [10, 14) is fully covered.
    • Returns true.
  • Step 5: queryRange(13, 15)
    • [13, 14) is covered, but [14, 15) is not.
    • Returns false.

Time and Space Complexity

  • Brute-force approach:
    • Storing every integer explicitly: O(N) space and O(1) for each update/query, but N can be huge.
  • Optimized interval approach:
    • Let M be the number of intervals stored.
    • Each operation (add, remove, query) requires finding overlapping intervals, which takes O(\log M) time using binary search or balanced trees, plus up to O(M) in the worst case for merging or splitting, though typically much less.
    • Space is O(M), where M is the number of non-overlapping intervals.
  • In practice, because intervals are merged and split, M is kept relatively small.

Summary

The Range Module problem is best solved by maintaining a sorted list of non-overlapping intervals, merging and splitting as needed when adding or removing ranges. This approach is both efficient and elegant, allowing for fast updates and queries without explicitly storing every number. The key insight is to always keep the interval list minimal and sorted, which makes all operations straightforward and efficient.

Code Implementation

import bisect

class RangeModule:
    def __init__(self):
        self.intervals = []

    def addRange(self, left: int, right: int) -> None:
        new_intervals = []
        placed = False
        for start, end in self.intervals:
            if end < left:
                new_intervals.append([start, end])
            elif right < start:
                if not placed:
                    new_intervals.append([left, right])
                    placed = True
                new_intervals.append([start, end])
            else:
                left = min(left, start)
                right = max(right, end)
        if not placed:
            new_intervals.append([left, right])
        self.intervals = new_intervals

    def queryRange(self, left: int, right: int) -> bool:
        i = bisect.bisect_right(self.intervals, [left, float('inf')]) - 1
        if i >= 0 and self.intervals[i][0] <= left and self.intervals[i][1] >= right:
            return True
        return False

    def removeRange(self, left: int, right: int) -> None:
        new_intervals = []
        for start, end in self.intervals:
            if end <= left or start >= right:
                new_intervals.append([start, end])
            else:
                if start < left:
                    new_intervals.append([start, left])
                if end > right:
                    new_intervals.append([right, end])
        self.intervals = new_intervals
      
#include <vector>
#include <algorithm>

class RangeModule {
public:
    std::vector<std::pair<int, int>> intervals;

    RangeModule() {}

    void addRange(int left, int right) {
        std::vector<std::pair<int, int>> res;
        bool placed = false;
        for (auto &it : intervals) {
            if (it.second < left) {
                res.push_back(it);
            } else if (right < it.first) {
                if (!placed) {
                    res.push_back({left, right});
                    placed = true;
                }
                res.push_back(it);
            } else {
                left = std::min(left, it.first);
                right = std::max(right, it.second);
            }
        }
        if (!placed) {
            res.push_back({left, right});
        }
        intervals = std::move(res);
    }

    bool queryRange(int left, int right) {
        auto it = std::upper_bound(intervals.begin(), intervals.end(), std::make_pair(left, INT_MAX));
        if (it == intervals.begin()) return false;
        --it;
        return it->first <= left && it->second >= right;
    }

    void removeRange(int left, int right) {
        std::vector<std::pair<int, int>> res;
        for (auto &it : intervals) {
            if (it.second <= left || it.first >= right) {
                res.push_back(it);
            } else {
                if (it.first < left) res.push_back({it.first, left});
                if (it.second > right) res.push_back({right, it.second});
            }
        }
        intervals = std::move(res);
    }
};
      
import java.util.*;

class RangeModule {
    private List<int[]> intervals;

    public RangeModule() {
        intervals = new ArrayList<>();
    }

    public void addRange(int left, int right) {
        List<int[]> res = new ArrayList<>();
        boolean placed = false;
        for (int[] interval : intervals) {
            if (interval[1] < left) {
                res.add(interval);
            } else if (right < interval[0]) {
                if (!placed) {
                    res.add(new int[]{left, right});
                    placed = true;
                }
                res.add(interval);
            } else {
                left = Math.min(left, interval[0]);
                right = Math.max(right, interval[1]);
            }
        }
        if (!placed) {
            res.add(new int[]{left, right});
        }
        intervals = res;
    }

    public boolean queryRange(int left, int right) {
        int l = 0, r = intervals.size() - 1;
        while (l <= r) {
            int m = (l + r) / 2;
            int[] interval = intervals.get(m);
            if (interval[0] > left) {
                r = m - 1;
            } else if (interval[1] <= left) {
                l = m + 1;
            } else {
                return interval[0] <= left && interval[1] >= right;
            }
        }
        return false;
    }

    public void removeRange(int left, int right) {
        List<int[]> res = new ArrayList<>();
        for (int[] interval : intervals) {
            if (interval[1] <= left || interval[0] >= right) {
                res.add(interval);
            } else {
                if (interval[0] < left) res.add(new int[]{interval[0], left});
                if (interval[1] > right) res.add(new int[]{right, interval[1]});
            }
        }
        intervals = res;
    }
}
      
class RangeModule {
    constructor() {
        this.intervals = [];
    }

    addRange(left, right) {
        const res = [];
        let placed = false;
        for (const [start, end] of this.intervals) {
            if (end < left) {
                res.push([start, end]);
            } else if (right < start) {
                if (!placed) {
                    res.push([left, right]);
                    placed = true;
                }
                res.push([start, end]);
            } else {
                left = Math.min(left, start);
                right = Math.max(right, end);
            }
        }
        if (!placed) {
            res.push([left, right]);
        }
        this.intervals = res;
    }

    queryRange(left, right) {
        let l = 0, r = this.intervals.length - 1;
        while (l <= r) {
            const m = Math.floor((l + r) / 2);
            const [start, end] = this.intervals[m];
            if (start > left) {
                r = m - 1;
            } else if (end <= left) {
                l = m + 1;
            } else {
                return start <= left && end >= right;
            }
        }
        return false;
    }

    removeRange(left, right) {
        const res = [];
        for (const [start, end] of this.intervals) {
            if (end <= left || start >= right) {
                res.push([start, end]);
            } else {
                if (start < left) res.push([start, left]);
                if (end > right) res.push([right, end]);
            }
        }
        this.intervals = res;
    }
}