线段树+扫描线解矩形面积


线段树+扫描线解决矩形面积问题

蓝桥杯2017年省赛最后一题是求重叠矩形面积的题目,这类题目最常用的做法就是用线段树+扫描线来解决。又由于数据给的比较大,所以还需要对数据进行离散化处理。下面分别介绍线段树,扫面线的概念。

预备知识

线段树

线段树是常用来维护 区间信息的数据结构。

线段树可以在$O(logN)$的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。常用于解决矩形面积,矩形周长,区间和(最大最小值)等问题。

下面结合代码详细说明(以区间和为例):

class SegmentTreeNode: # 树节点
    def __init__(self, start, end, val, left=None, right=None):
        self.start = start # 区间开始点
        self.end = end # 区间结束点
        self.sum = val # 区间和 可以替换成最大值等等
        self.left = left # 左子树
        self.right = right # 右子树

def build_tree(start, end, vals): # 构建线段树
    if start == end: # 叶子节点
        return SegmentTreeNode(start, end, val[start])
     mid = (start+end)//2 # 类似二分思想

    # 递归构建左右子树
    left = build_tree(start, mid, vals) 
    right = build_tree(mid+1, end, vals)

    # 返回根节点
    return SegmentTreeNode(start, end, left.sum+right.sum, left, right)

def update_tree(root, index, val): # 更新index节点的值并更新整棵树
    if root.start == index == root.end: # 找到index节点
        root.sum = val # 更新节点值
        return
    mid = (root.start+root.end)//2 # 二分
    if index > mid: # index在右子树
        update_tree(root.right, index, val) # 递归更新右子树
    else: # 在左子树
        update_tree(root.left, index, val) # 递归更新左子树
    root.sum = root.left.sum+root.right.sum # 更新当前节点

def query_sum(root, i, j): # 区间查询
    if root.start == i and root.end == j: # 查询区间等于当前节点区间
        return root.sum
    mid = (root.start+root.end)//2 # 二分
    if  i>mid: # 查询区间左端点大于 mid 直接查询右子树
        return query_sum(root.right, i, j)
    elif j <= mid: # 同理
        return query_sum(root.left, i, j)
    # 查询区间左端点在左子树,右端点在右子树 分别查询再合并
    else return query_sum(root.left, i, mid)+query_sum(root.right, mid+1, j)

代码不是很难理解,最主要是mid用的非常秒。mid参数也可以存下来避免每次计算

扫描线

扫描线其实并不是一个具体的数据结构,只是一种思想。利用扫描线,我们可以快速并且无重复的计算出矩形的面积。扫描线结构如下:

class Line:
    def __init__(self, x1, x2, height, f):
        self.x1 = x1 # 扫描线左端点
        self.x2 = x2 # 扫描线右端点
        self.height = height # 高度
        self.f = f # 1为入边 -1为出边

分析

本题思路其实非常清晰,是扫描线+线段树的模板题。

首先将扫描线设定为平行于X轴的方向,将矩形的边都当作扫面线处理并储存起来。

代码

下面给出代码实现并结合代码解释具体细节。

from functools import cmp_to_key

class Line: # 扫面线数据结构
    def __init__(self, x1, x2, h, f):
        self.x1 = x1 # 线段左端点
        self.x2 = x2 # 右端点
        self.height = h # 高度
        self.f = f # 入边=1  出边=-1

def line_cmp(l1:Line, l2:Line): # 扫描线排序
    return l1.height - l2.height

class SegTree: #线段树
    def __init__(self, pl, pr):
        self.pl = pl # 区间左端点
        self.pr = pr # 区间右端点
        self.cnt = 0 # 覆盖次数计数
        self.l = 0 # 区间长度
        self.lson = None # 左子树
        self.rson = None # 右子树

N = 10000
X = [0] * (2*N) # 存储N个矩形的2N个顶点
lines = [] # 扫描线数组

def build_tree(pl ,pr): # 构建扫描树(空)
    t = SegTree(pl, pr)
    if pl == pr:
        return t
    mid = (pl+pr)//2
    t.lson = build_tree(pl, mid)
    t.rson = build_tree(mid+1, pr)
    return t

def update_length(pTree: SegTree, tl, tr): # 更新当前节点的长度
    if pTree.cnt: # 如果被覆盖(入边)
        pTree.l = X[tr]-X[tl-1] # 长度为该扫描线的长度
    elif tl == tr: # 如果为出边,而且区间左右端点相同
        pTree.l = 0 # 长度为0
    else: # 为出边而且区间左右端点不同
        pTree.l = pTree.lson.l + pTree.rson.l # l为其左右子树的长度之和

def update(tree: SegTree, pl, pr, value): # 更新区间树,pl,pr都是X的索引,value是要更新的值
    if not tree:
        return
    tl = tree.pl # 当前树节点的左端点
    tr = tree.pr # 右端点
    if pl <= tl and pr >= tr: # 要更新的区间(pl,pr)整好在该节点的范围内 即属于该节点
        tree.cnt += value # 更新cnt value是f即出入边 与cnt相加为覆盖次数
        update_length(tree, tl, tr) # 更新区间长度
        return
    m = (tl + tr)//2 
    if pl <= m: # 更新其左子树
        update(tree.lson, pl, pr, value)
    if pr > m: # 更新其右子树
        update(tree.rson, pl, pr, value)

    update_length(tree, tl, tr) # 更新长度

if __name__ == "__main__":
    n = int(input())
    index = 0 # 扫描线个数计数
    ans = 0 # 最终答案
    for i in range(n):
        tmp = input().split()
        x1, y1, x2, y2 = int(tmp[0]), int(tmp[1]), int(tmp[2]), int(tmp[3])
        # 每读入一个矩形 添加两条扫描线
        X[index] = x1
        lines.append(Line(min(x1, x2), max(x1, x2), y1, 1))
        index += 1

        X[index] = x2
        lines.append(Line(min(x1, x2), max(x1, x2), y2, -1))
        index += 1

    X = X[:index]
    X = list(set(X)) # 端点去重
    X.sort() # 排序
    lines.sort(key=cmp_to_key(line_cmp)) # 扫描线按照高度排序

    # x_end = len(set(X))
    x_end = len(X) # 去重后的总数量
    root = build_tree(1, x_end) # 构建扫描树
    for i in range(index-1):
        pl = X.index(lines[i].x1) # 从X中找到line[i]的左端点的真正索引(X经过排序被打乱)
        pr = X.index(lines[i].x2) # 从X中找到line[i]的右端点
        update(root, pl+1, pr, lines[i].f) # 更新区间树
        # 用当前扫描线长度*两扫描线高度之差记为当前区域长方形面积
        ans += root.l*(lines[i+1].height-lines[i].height) 
    print(ans)

评论
 上一篇
write-up-蓝桥杯2016省赛真题 write-up-蓝桥杯2016省赛真题
蓝桥杯2016省赛真题 这两天又做了2016年的蓝桥杯题目,感觉难度比2017年简单一些,是正常的蓝桥杯题目难度,难度逐年降低(反序😋)。话不多说,下面开始逐题分析。 ps: 最近被win折磨的不行,动不动explorer.exe cpu
2020-03-26
下一篇 
write-up 蓝桥杯2017省赛真题 write-up 蓝桥杯2017省赛真题
蓝桥杯2017省赛真题最近做了一下蓝桥杯2017年省赛的真题,越发让我感到自己水平低下🙃,看来上次三等奖名副其实。差不多一半的题目没有很好的思路,甚至有些题在看题解时又双触及到了知识盲区。对比前几年的情况,17年蓝桥杯感觉略有难度(暴力杯
2020-03-19
  目录