70 打点计数器的区间合并题解 | 豆包MarsCode AI刷题

42 阅读5分钟

问题概述

给出一系列全闭的一维区间。输出这些区间覆盖的不同整数的个数。 很典型的扫描线图形问题。


测试样例

输入:inputArray = [[1, 4], [7, 10], [3, 5]]
输出:9

分析

将所有的端点放入events数组并排序,为了保留端点起始或结束信息,以二元组形式保存。例如:[1, -1]表示这个端点的值为1, 是起始端点。[4, 1]表示这个端点的值为4, 是结束端点。 将events进行多级排序,第一级为按照端点值排序,第二级为同位置端点起始端点优先。

第二级排序这样设计的作用是,在扫描到同样值的端点时,先看开始的再看结束的。

因为算法设计为遇到起始端点时,计数+1,遇到结束端点则-1。计数为0时表示扫描完了一个合并区间。先看起始端点可以有效合并两个相交部分只有端点值的区间。

代码

def solution(inputArray):
    events=[]
    for interval in inputArray:
        events.append([interval[0], -1])
        events.append([interval[1], 1])
    def compare(e):
        return e[0], e[1]
    events.sort(key=compare)
    start=0
    end=0
    count=0
    res=0
    for e in events:
        if e[1]==1:
            if count==0:
                start=e[0]
            count+=1
        else:
            count-=1
            if count==0:
                end=e[0]
        if count==0:
            res+=(end-start+1)
    return res

if __name__ == "__main__":
    testArray1 = [[1,4], [7, 10], [3, 5]]
    print(solution(testArray1) == 9 )

延伸知识

树状数组与线段树

这两种数据结构通常用于解决区间查询与更新问题。

前序知识

Low-Bits

一个非负整数的二进制数制表示中从低位开始数第一个1以及所有低位的0共同构成一个数的低位比特。

计算获得低位比特的算法:

def lowBits(x):
    return x&-x

正确性证明:

x为非负整数,-x的补码表示为x的二进制数据反转后加1。

假设x的低维比特为10...0。0为k个。

则二进制反转为01...1。1为k个。

再加1后则为10...0。因此-x与x的低位比特相同且高位相反,这时只需按位取反即可得到x的低位比特。

树状数组

树状数组使用Tree[]数据结构存储原始数组的各个部分。在查询时使用差分计算获得需要的解。 优点是占用空间与原数组相同。查询与更新操作的时间复杂度均为O(log(n))O(\log(n))

线段树

线段树可以支持除了区间求和之外更复杂的操作。

扫描线算法

扫描线算法流程就是按照一定依据将数据排序,然后使用一个虚拟的线进行“扫描”,即将排序后的数据按序取出。 取出后的数据通常会结合线段树进行辅助处理。

以下给出使用扫描线算法与线段树结合解决二维矩形面积并问题的例子。

延伸题目-二维矩形面积并问题

矩形面积问题通常涉及计算多个矩形覆盖下的总面积,这些矩形可能有重叠部分。结合线段树和扫描线算法解决矩形面积问题的步骤如下:

步骤 1: 预处理
  1. 矩形表示:将所有矩形的左右边界和高度存储起来。
  2. 事件排序:将所有矩形的左边界和右边界作为事件点,按照x坐标排序。如果两个事件点的x坐标相同,则按照事件的类型(左边界或右边界)排序,确保左边界事件在右边界事件之前处理。
步骤 2: 扫描线
  1. 初始化:设置一条垂直的扫描线,初始位置在所有矩形的最左边。
  2. 处理事件:按顺序处理事件点:
    • 当扫描线遇到矩形的左边界时,将该矩形的高度加入到一个数据结构中(通常是平衡二叉搜索树或线段树)。
    • 当扫描线遇到矩形的右边界时,将该矩形的高度从数据结构中移除。
步骤 3: 线段树维护
  1. 构建线段树:线段树用于维护当前扫描线下所有矩形的高度及其覆盖的长度。
  2. 更新线段树
    • 当加入一个新矩形的高度时,更新线段树以包含这个新高度。
    • 当移除一个矩形的高度时,更新线段树以移除这个高度。
  3. 查询线段树:在每次处理事件后,查询线段树以获取当前扫描线下的最大高度。这个最大高度乘以扫描线移动的距离就是这一段区域的覆盖面积。
步骤 4: 计算面积
  1. 累加面积:在每个事件点,计算扫描线移动的距离与当前最大高度的乘积,并将这个乘积累加到总面积中。
步骤 5: 完成扫描
  1. 继续移动:将扫描线移动到下一个事件点,并重复步骤2到步骤4,直到所有事件点都被处理。
示例代码:
# 假设 events 是排序后的事件列表,segment_tree 是线段树实例
total_area = 0
current_x = events[0].x  # 初始扫描线位置
for event in events:
    if event.is_left:  # 左边界事件
        segment_tree.insert(event.height)
    else:  # 右边界事件
        segment_tree.remove(event.height)
    
    # 计算面积
    max_height = segment_tree.query_max()
    total_area += max_height * (event.x - current_x)
    current_x = event.x
return total_area

通过这种方式,线段树和扫描线算法可以高效地计算出多个矩形覆盖下的总面积,即使这些矩形有重叠部分。线段树在这里的作用是维护当前扫描线下所有矩形的高度信息,并快速地更新和查询最大高度。