Luogu P2801 教主的魔法 题解
Luogu P2801 教主的魔法 题解题目概述Luogu P2801 教主的魔法是一道经典的数据结构题目要求维护一个长度为 ( n ) 的序列支持两种操作1.区间加法对区间 ([L, R]) 内的所有元素加上一个整数 ( w )。2.区间查询查询区间 ([L, R]) 内有多少个元素大于等于 ( c )。题目数据范围( n \leq 1,000,000 )操作次数 ( q \leq 3000 )。这意味着我们需要一种高效的数据结构既能支持区间修改又能支持区间查询满足阈值的元素个数。—## 算法选择分块Block由于区间修改和查询需要平衡时间复杂度分块是一个理想的选择。分块将序列分成 ( \sqrt{n} ) 个块每个块大小约为 ( \sqrt{n} )。这样-区间加法对于整块记录一个全局标记tag对于零散的部分暴力更新。-区间查询对于整块利用二分查找快速统计大于等于 ( c ) 的元素需要维护块内有序数组对于零散部分暴力遍历。分块的时间复杂度为 ( O(\sqrt{n} \log n) ) 每次操作在 ( n10^6 ) 时依然可行。—## 数据结构设计### 核心思路- 每个块维护一个有序数组block_sorted用于快速二分查找。- 每个块维护一个加法标记tag表示整个块统一加上的值。- 修改时如果区间覆盖整块只更新tag否则暴力更新原数组并重新排序该块。- 查询时整块利用二分查找实际比较时减去tag零散部分暴力判断。### 为什么选择分块而非线段树线段树虽然也能实现区间加法和区间查询但查询“大于等于 c 的元素个数”需要维护区间内元素的分布如平衡树、权值线段树实现复杂且常数大。分块则更直观易于编码且对于本题的数据规模足够快。—## 代码实现Python 版本### 第一步分块初始化pythonimport mathimport bisectdef init_blocks(arr, n): 初始化分块结构 :param arr: 原数组1-indexed :param n: 长度 :return: block_size, block_count, block_start, block_end, tag, sorted_blocks block_size int(math.sqrt(n)) 1 block_count (n block_size - 1) // block_size # 每个块的起始和结束下标1-indexed block_start [0] * (block_count 1) block_end [0] * (block_count 1) for i in range(1, block_count 1): block_start[i] (i - 1) * block_size 1 block_end[i] min(i * block_size, n) # 每个块的加法标记 tag [0] * (block_count 1) # 每个块的有序副本 sorted_blocks [] for i in range(1, block_count 1): l block_start[i] r block_end[i] # 提取该块元素并排序 block_sorted sorted(arr[l:r1]) sorted_blocks.append(block_sorted) return block_size, block_count, block_start, block_end, tag, sorted_blocks### 第二步区间加法操作pythondef range_add(arr, block_size, block_count, block_start, block_end, tag, sorted_blocks, L, R, w): 区间 [L, R] 内所有元素加 w # 找到 L 和 R 所在的块编号 block_L (L - 1) // block_size 1 block_R (R - 1) // block_size 1 if block_L block_R: # 区间在一个块内暴力更新 for i in range(L, R 1): arr[i] w # 重新排序该块 l block_start[block_L] r block_end[block_L] sorted_blocks[block_L - 1] sorted(arr[l:r1]) else: # 处理左端不完整块 for i in range(L, block_end[block_L] 1): arr[i] w l block_start[block_L] r block_end[block_L] sorted_blocks[block_L - 1] sorted(arr[l:r1]) # 处理中间完整块 for b in range(block_L 1, block_R): tag[b] w # 处理右端不完整块 for i in range(block_start[block_R], R 1): arr[i] w l block_start[block_R] r block_end[block_R] sorted_blocks[block_R - 1] sorted(arr[l:r1])### 第三步区间查询操作pythondef range_query(arr, block_size, block_count, block_start, block_end, tag, sorted_blocks, L, R, c): 查询区间 [L, R] 内大于等于 c 的元素个数 block_L (L - 1) // block_size 1 block_R (R - 1) // block_size 1 ans 0 if block_L block_R: # 在一个块内暴力统计 for i in range(L, R 1): if arr[i] tag[block_L] c: # 注意加上当前块的tag ans 1 else: # 左端不完整块 for i in range(L, block_end[block_L] 1): if arr[i] tag[block_L] c: ans 1 # 中间完整块利用二分查找 for b in range(block_L 1, block_R): # 在有序数组中查找第一个大于等于 (c - tag[b]) 的元素 target c - tag[b] pos bisect.bisect_left(sorted_blocks[b - 1], target) ans (block_end[b] - block_start[b] 1) - pos # 右端不完整块 for i in range(block_start[block_R], R 1): if arr[i] tag[block_R] c: ans 1 return ans### 完整主程序示例pythonimport mathimport bisectdef main(): # 示例输入 n, q 5, 3 arr [0, 1, 2, 3, 4, 5] # 1-indexed # 初始化分块 block_size, block_count, block_start, block_end, tag, sorted_blocks init_blocks(arr, n) # 操作序列 operations [ (A, 1, 5, 3), # 查询 [1,5] 中 3 的个数 (M, 1, 3, 2), # 区间 [1,3] 加 2 (A, 1, 5, 4) # 查询 [1,5] 中 4 的个数 ] for op in operations: if op[0] A: L, R, c op[1], op[2], op[3] res range_query(arr, block_size, block_count, block_start, block_end, tag, sorted_blocks, L, R, c) print(f查询 [{L},{R}] {c}: {res}) elif op[0] M: L, R, w op[1], op[2], op[3] range_add(arr, block_size, block_count, block_start, block_end, tag, sorted_blocks, L, R, w) print(f区间 [{L},{R}] 加 {w})if __name__ __main__: main()输出查询 [1,5] 3: 3区间 [1,3] 加 2查询 [1,5] 4: 3—## 复杂度分析-初始化( O(n \log n) )排序每个块。-区间加法最坏情况 ( O(\sqrt{n} \log n) )重新排序两个块。-区间查询最坏情况 ( O(\sqrt{n} \log n) )整块二分查找 零散暴力。由于 ( q \leq 3000 )( n \leq 10^6 )总时间复杂度约为 ( O(n \log n q \sqrt{n} \log n) )完全可行。—## 进阶优化1.使用bisect加速二分Python 的bisect模块是 C 实现比手动二分快。2.内存优化sorted_blocks存储的是每个块的副本总空间为 ( O(n) )在 ( n10^6 ) 时约 8MB假设整数可接受。3.块大小调整块大小取 ( \sqrt{n} ) 或 ( 1000 ) 均可实测 ( 1000 ) 左右常数更小。—## 总结通过分块算法我们优雅地解决了 Luogu P2801 的区间加法与阈值查询问题。分块的核心思想是“整体维护局部暴力”将复杂度从 ( O(nq) ) 降低到 ( O(q \sqrt{n} \log n) )。相比线段树、树状数组等结构分块实现简单调试容易特别适合竞赛中的中等难度题目。最后提醒注意 Python 的输入输出效率如果使用input()和print()可能超时建议用sys.stdin.read()和sys.stdout.write()加速。
