Hard

题目描述

给定一个空的区间集合,实现一个数据结构,该数据结构支持:

  • 向区间集合添加一个区间。
  • 统计至少被一个区间包含的整数个数。

实现 CountIntervals 类:

  • CountIntervals() 用空区间集合初始化对象。
  • void add(int left, int right) 向区间集合添加区间 [left, right]
  • int count() 返回至少被一个区间包含的整数个数。

注意区间 [left, right] 表示所有满足 left <= x <= right 的整数 x

示例 1:

输入
["CountIntervals", "add", "add", "count", "add", "count"]
[[], [2, 3], [7, 10], [], [5, 8], []]
输出
[null, null, null, 6, null, 8]

解释
CountIntervals countIntervals = new CountIntervals(); // 用空区间集合初始化对象
countIntervals.add(2, 3);  // 添加 [2, 3] 到区间集合
countIntervals.add(7, 10); // 添加 [7, 10] 到区间集合  
countIntervals.count();    // 返回 6
                           // 整数 2 和 3 在区间 [2, 3] 中
                           // 整数 7, 8, 9, 10 在区间 [7, 10] 中
countIntervals.add(5, 8);  // 添加 [5, 8] 到区间集合
countIntervals.count();    // 返回 8
                           // 整数 2 和 3 在区间 [2, 3] 中
                           // 整数 5 和 6 在区间 [5, 8] 中
                           // 整数 7 和 8 在区间 [5, 8] 和 [7, 10] 中
                           // 整数 9 和 10 在区间 [7, 10] 中

约束条件:

  • 1 <= left <= right <= 10^9
  • 最多总共进行 10^5addcount 调用
  • 至少会调用一次 count

解题思路

这道题需要我们维护一个区间集合,支持动态添加区间和查询覆盖的整数总数。

核心思路:区间合并

关键在于维护一组不重叠的区间,每次添加新区间时需要:

  1. 找出所有与新区间重叠的已有区间
  2. 将这些重叠区间与新区间合并成一个大区间
  3. 移除被合并的原区间,添加新的合并区间
  4. 更新总计数

实现方案: 使用 map/TreeMap 存储区间,其中 key 为区间左端点,value 为区间右端点。这样可以利用有序性快速找到重叠区间。

合并策略: 对于新区间 [left, right]

  • 使用二分查找找到第一个可能重叠的区间(右端点 >= left-1 的最小左端点)
  • 向后遍历所有重叠区间(左端点 <= right+1),计算合并后的新区间
  • 删除所有被合并的区间,插入新的合并区间

时间复杂度:每次 add 操作最坏 O(n),但由于区间会被合并,实际上均摊复杂度较好。count 操作 O(1)。

代码实现

class CountIntervals {
private:
    map<int, int> intervals;  // left -> right
    int totalCount;
    
public:
    CountIntervals() : totalCount(0) {}
    
    void add(int left, int right) {
        int newLeft = left, newRight = right;
        
        // 找到所有重叠的区间并合并
        auto it = intervals.upper_bound(right);
        while (it != intervals.begin()) {
            --it;
            if (it->second < left - 1) {
                ++it;
                break;
            }
            // 当前区间与新区间重叠,需要合并
            newLeft = min(newLeft, it->first);
            newRight = max(newRight, it->second);
            totalCount -= (it->second - it->first + 1);
            it = intervals.erase(it);
        }
        
        // 插入合并后的新区间
        intervals[newLeft] = newRight;
        totalCount += (newRight - newLeft + 1);
    }
    
    int count() {
        return totalCount;
    }
};
from sortedcontainers import SortedDict

class CountIntervals:
    def __init__(self):
        self.intervals = SortedDict()  # left -> right
        self.total_count = 0
    
    def add(self, left: int, right: int) -> None:
        new_left, new_right = left, right
        
        # 找到所有重叠的区间
        to_remove = []
        for l, r in self.intervals.items():
            if r < left - 1:
                continue
            if l > right + 1:
                break
            # 重叠,需要合并
            new_left = min(new_left, l)
            new_right = max(new_right, r)
            self.total_count -= (r - l + 1)
            to_remove.append(l)
        
        # 删除被合并的区间
        for l in to_remove:
            del self.intervals[l]
        
        # 插入新的合并区间
        self.intervals[new_left] = new_right
        self.total_count += (new_right - new_left + 1)
    
    def count(self) -> int:
        return self.total_count
public class CountIntervals {
    private SortedDictionary<int, int> intervals;
    private int totalCount;
    
    public CountIntervals() {
        intervals = new SortedDictionary<int, int>();
        totalCount = 0;
    }
    
    public void Add(int left, int right) {
        int newLeft = left, newRight = right;
        var toRemove = new List<int>();
        
        foreach (var kvp in intervals) {
            int l = kvp.Key, r = kvp.Value;
            if (r < left - 1) continue;
            if (l > right + 1) break;
            
            // 重叠,需要合并
            newLeft = Math.Min(newLeft, l);
            newRight = Math.Max(newRight, r);
            totalCount -= (r - l + 1);
            toRemove.Add(l);
        }
        
        // 删除被合并的区间
        foreach (int l in toRemove) {
            intervals.Remove(l);
        }
        
        // 插入新的合并区间
        intervals[newLeft] = newRight;
        totalCount += (newRight - newLeft + 1);
    }
    
    public int Count() {
        return totalCount;
    }
}
var CountIntervals = function() {
    this.intervals = new Map(); // left -> right
    this.totalCount = 0;
};

CountIntervals.prototype.add = function(left, right) {
    let newLeft = left, newRight = right;
    let toRemove = [];
    
    // 获取所有区间并排序
    let sortedIntervals = Array.from(this.intervals.entries()).sort((a, b) => a[0] - b[0]);
    
    for (let [l, r] of sortedIntervals) {
        if (r < left - 1) continue;
        if (l > right + 1) break;
        
        // 重叠,需要合并
        newLeft = Math.min(newLeft, l);
        newRight = Math.max(newRight, r);
        this.totalCount -= (r - l + 1);
        toRemove.push(l);
    }
    
    // 删除被合并的区间
    for (let l of toRemove) {
        this.intervals.delete(l);
    }
    
    // 插入新的合并区间
    this.intervals.set(newLeft, newRight);
    this.totalCount += (newRight - newLeft + 1);
};

CountIntervals.prototype.count = function() {
    return this.totalCount;
};

复杂度分析

操作时间复杂度空间复杂度
addO(n) 最坏情况,均摊 O(log n)O(n)
countO(1)O(1)
总体O(n log n) n次操作O(n)

其中 n 为调用 add 的次数。虽然单次 add 最坏为 O(n),但由于区间合并的特性,实际性能通常更好。

相关题目