news 2026/10/15 9:59:09

线段树实现二进制数组区间取反与统计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线段树实现二进制数组区间取反与统计

1. 题目背景与核心概念解析

这道来自牛客网的编程练习题"区间取反与区间数一"看似简单,却蕴含着典型的区间操作思想。题目要求我们对一个二进制数组进行两种操作:区间取反(0变1,1变0)和区间统计1的个数。这类问题在实际开发中并不少见,比如在图像处理中的像素反转、数据压缩中的位操作等场景都会遇到类似需求。

从数据结构角度看,题目考察的核心是如何高效处理大规模数据的区间更新与查询。暴力解法虽然直观(每次操作遍历整个区间),但当数据量达到1e5量级时,O(n)的单次操作复杂度显然无法满足要求。这就需要我们引入更高级的数据结构——线段树(Segment Tree),它能将两种操作的时间复杂度都优化到O(logn)。

提示:线段树不是唯一解,树状数组(Binary Indexed Tree)也能处理部分区间问题,但本题需要区间修改和区间查询,线段树的实现更为直观。

2. 线段树设计思路拆解

2.1 基础线段树结构设计

对于这个特定问题,我们需要在线段树节点中维护三个关键信息:

  1. 区间内1的计数(cnt)
  2. 懒标记(lazy)记录待下传的取反操作次数
  3. 区间左右边界(标准线段树属性)
class SegmentTreeNode: def __init__(self, l, r): self.l = l # 区间左边界 self.r = r # 区间右边界 self.left = None # 左子节点 self.right = None # 右子节点 self.cnt = 0 # 区间内1的个数 self.lazy = 0 # 待下传的取反操作次数(奇数次才生效)

2.2 取反操作的特殊处理

二进制取反有一个重要特性:连续两次取反等于无操作。这提示我们可以用异或(XOR)来记录操作次数:

  • 每次对区间执行取反时,只需将lazy标记^=1
  • 下传标记时,若lazy为1则实际执行取反,然后重置lazy为0
def push_down(node): if node.lazy and node.left: # 有标记且非叶子节点 # 左子树处理 node.left.cnt = (node.left.r - node.left.l + 1) - node.left.cnt node.left.lazy ^= node.lazy # 右子树处理 node.right.cnt = (node.right.r - node.right.l + 1) - node.right.cnt node.right.lazy ^= node.lazy node.lazy = 0 # 清除当前节点标记

2.3 建树与查询实现

建树过程采用标准递归二分法,初始化时根据原始数组设置每个叶子节点的cnt值。查询时需要注意:

  1. 当前区间完全包含在目标区间时直接返回cnt
  2. 否则先下传懒标记,再分别查询左右子树
def build(l, r, arr): node = SegmentTreeNode(l, r) if l == r: # 叶子节点 node.cnt = arr[l] return node mid = (l + r) // 2 node.left = build(l, mid, arr) node.right = build(mid+1, r, arr) node.cnt = node.left.cnt + node.right.cnt return node def query_range(node, l, r): if node.r < l or node.l > r: # 无交集 return 0 if l <= node.l and node.r <= r: # 完全包含 return node.cnt push_down(node) # 关键:查询前下传标记 return query_range(node.left, l, r) + query_range(node.right, l, r)

3. 完整解决方案实现

3.1 区间取反操作实现

区间更新操作需要处理三种情况:

  1. 当前区间与目标区间无交集:直接返回
  2. 完全包含时:更新cnt和lazy标记
  3. 部分重叠时:先下传标记,再递归处理子节点
def update_range(node, l, r): if node.r < l or node.l > r: # 情况1 return if l <= node.l and node.r <= r: # 情况2 node.cnt = (node.r - node.l + 1) - node.cnt node.lazy ^= 1 return push_down(node) # 情况3:先下传 update_range(node.left, l, r) update_range(node.right, l, r) node.cnt = node.left.cnt + node.right.cnt # 回溯更新

3.2 主程序逻辑整合

将上述操作封装为类,并处理输入输出:

class BinaryArray: def __init__(self, arr): self.n = len(arr) self.root = build(0, self.n-1, arr) def flip(self, l, r): update_range(self.root, l, r) def count_ones(self, l, r): return query_range(self.root, l, r) # 示例用法 arr = [0,1,0,1,0] ba = BinaryArray(arr) print(ba.count_ones(1,3)) # 输出2 ba.flip(1,3) print(ba.count_ones(1,3)) # 输出1

4. 复杂度分析与优化技巧

4.1 时间复杂度证明

每个操作(更新/查询)最多访问从根到叶子的路径,线段树高度为⌈logn⌉,因此:

  • 单次操作时间复杂度:O(logn)
  • 建树时间复杂度:O(n)(每个节点处理一次)

相比暴力解法的O(n)单次操作,在m次操作下:

  • 线段树:O(n + mlogn)
  • 暴力:O(mn)

当n,m都达到1e5时,线段树方案能在1秒内完成,而暴力解法会超时。

4.2 空间优化技巧

标准线段树需要4n空间(满二叉树最坏情况),可采用以下优化:

  1. 动态开点:只在需要时创建子节点,适合稀疏操作
  2. 离散化:当数据范围很大但实际使用点较少时,先对坐标离散化
# 动态开点线段树示例 class DynamicSegmentTreeNode: def __init__(self, l, r): self.l = l self.r = r self.left = None # 初始为空 self.right = None self.cnt = 0 self.lazy = 0 def dynamic_update(node, l, r, L, R): if R < node.l or L > node.r: return if L <= node.l and node.r <= R: node.cnt = (node.r - node.l + 1) - node.cnt node.lazy ^= 1 return mid = (node.l + node.r) // 2 # 动态创建子节点 if not node.left: node.left = DynamicSegmentTreeNode(node.l, mid) if not node.right: node.right = DynamicSegmentTreeNode(mid+1, node.r) # 下传标记 if node.lazy: node.left.cnt = (node.left.r - node.left.l + 1) - node.left.cnt node.left.lazy ^= node.lazy node.right.cnt = (node.right.r - node.right.l + 1) - node.right.cnt node.right.lazy ^= node.lazy node.lazy = 0 dynamic_update(node.left, l, r, L, R) dynamic_update(node.right, l, r, L, R) node.cnt = node.left.cnt + node.right.cnt

5. 边界条件与测试用例设计

5.1 常见边界情况

  1. 单元素区间:[i,i]的查询和更新
  2. 全区间操作:[0,n-1]的更新和查询
  3. 连续操作:多次取反后验证正确性
  4. 交错区间:更新和查询区间部分重叠

5.2 测试用例示例

def test(): # 用例1:基础测试 arr1 = [0,1,0,1,0] ba1 = BinaryArray(arr1) assert ba1.count_ones(0,4) == 2 ba1.flip(1,3) assert ba1.count_ones(0,4) == 2 # [0,0,1,0,0] # 用例2:边界测试 arr2 = [1]*1000 ba2 = BinaryArray(arr2) assert ba2.count_ones(0,999) == 1000 ba2.flip(0,999) assert ba2.count_ones(0,999) == 0 # 用例3:压力测试 import random arr3 = [random.randint(0,1) for _ in range(100000)] ba3 = BinaryArray(arr3) for _ in range(1000): l = random.randint(0,99999) r = random.randint(l,99999) if random.random() > 0.5: ba3.flip(l,r) else: # 验证与暴力解一致 expected = sum(arr3[l:r+1]) assert ba3.count_ones(l,r) == expected print("All tests passed!") test()

6. 同类问题扩展与变种

6.1 支持多种区间操作

可以扩展线段树节点设计,支持更多操作:

  • 区间置0/置1
  • 区间AND/OR/XOR运算
  • 区间最大值/最小值查询
class AdvancedSegmentTreeNode: def __init__(self, l, r): self.l = l self.r = r self.cnt = 0 # 1的个数 self.sum = 0 # 区间和(当元素非0/1时) self.max = 0 # 区间最大值 self.min = 0 # 区间最小值 self.lazy_flip = 0 # 取反标记 self.lazy_set = -1 # 置0/置1标记(-1表示无操作)

6.2 二维区间操作

当问题扩展到二维矩阵时(如图像处理),可以使用:

  1. 二维线段树:每个节点再包含四个子节点(NW, NE, SW, SE)
  2. 树套树:外层线段树的每个节点内嵌一个线段树
class QuadTreeNode: def __init__(self, x1, y1, x2, y2): self.x1, self.y1 = x1, y1 # 左上角 self.x2, self.y2 = x2, y2 # 右下角 self.children = [None]*4 # 四个象限 self.cnt = 0 self.lazy = 0

6.3 实际应用场景

  1. 图像处理:像素块的反转与统计
  2. 数据压缩:位图的区间操作
  3. 游戏开发:地图区块的状态管理
  4. 金融分析:交易信号的区间统计

我在实际开发中曾用类似技术处理过视频帧的掩码操作,对特定区域的像素进行批量取反和统计,线段树的效率优势在60FPS的视频处理中体现得尤为明显。一个经验是:当处理区域超过屏幕的1/4时,GPU加速可能更高效,这时需要权衡CPU/GPU的数据传输开销。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 9:59:07

E2E_自动驾驶---地区合规性

E2E:End to End,AD:Autonomous Driving,FSD:Full Self-Driving,ODD:Operational Design Domain‌,DMS:Driver Monitoring System‌ 核心前置结论 截至到2026年10月,目前全球没有任何地区批准【L3/L4 级、纯端到端大模型(无显式感知 / 预测 / 规划模块)】可以脱离…

作者头像 李华
网站建设 2026/10/15 9:59:07

UE5.7插件编译失败问题解析与解决方案

1. 问题现象与背景分析最近在升级到Unreal Engine 5.7版本后&#xff0c;不少开发者遇到了插件自动编译失败的问题。典型报错包括"Couldnt compile plugin module"、"Missing precompiled manifest"等错误提示。这个问题尤其容易出现在从旧版本迁移项目或首…

作者头像 李华
网站建设 2026/10/15 9:58:29

深入理解CAS与ABA问题:从无锁编程原理到AtomicStampedReference实战

1. 从一次诡异的并发Bug说起&#xff1a;CAS凭什么成为无锁编程的地基几个月前&#xff0c;我被拉去帮某团队看一个并发问题。场景很朴素&#xff1a;一套配置开关服务&#xff0c;某个开关的值需要满足"当前值是100才更新为50"的精确条件&#xff0c;改动频率也不高…

作者头像 李华
网站建设 2026/10/15 9:57:58

Qt自定义控件:手写滑动选择器的完整实现与避坑指南

简介&#xff1a;基于Qt自定义的滑动选择器是一套面向Qt开发者的自定义控件示例&#xff0c;用于解决系统自带控件无法满足个性化交互需求的问题&#xff0c;适合有基础Qt编程知识、希望提升自定义控件设计能力的开发者。压缩包共8个文件&#xff0c;大小仅9KB&#xff0c;包含…

作者头像 李华
网站建设 2026/10/15 9:57:54

Plotly数据可视化:交互式图表与实战应用

1. 为什么选择Plotly进行数据可视化第一次接触Plotly是在三年前的一个客户项目中&#xff0c;当时需要展示一组复杂的销售趋势数据。传统的静态图表根本无法满足需求——市场部门的同事需要能够自由缩放查看季度细节&#xff0c;又需要随时切换不同产品线的对比。当我将第一个P…

作者头像 李华
网站建设 2026/10/15 9:57:39

H5网站SEO前端优化:预渲染+语义强化实战指南

1. 项目概述&#xff1a;为什么H5网站的SEO前端处理不是“加个title标签”就完事了 “H5网站SEO前端处理优化”——这八个字背后&#xff0c;藏着大量开发者踩过坑、交过学费才明白的现实逻辑。我从2014年开始做移动端H5项目&#xff0c;经手过电商落地页、教育课程页、本地生…

作者头像 李华