跳转至

BISHI127 区间根号与区间求和

中等通过率 41.92%python3样例通过牛客 AC

牛客原题  源码

讲解章节树状数组与线段树

一句话

区间开根 + 区间求和。

解题思路

这题考什么

势能分析 + 并查集跳跃,是「单次最坏 O(n)、但总量有界」的经典模型。

关键观察:开根收敛极快。

1e7 -> 3162 -> 56 -> 7 -> 2 -> 1 -> 1 -> ...

任何数最多开 6 次根就落到 <= 1,之后再开根不变。 所以「区间开根」的单点修改次数是 O(6n),而不是 O(qn)。

剩下的问题是「怎么跳过已经稳定(<= 1)的位置」——用并查集

nxt[i] = i 右边第一个还没稳定的位置。

稳定一个就把它并到 i+1,之后所有查询自动跳过。

区间和为什么用分块而不是树状数组

两者都能做「单点改 + 区间查」,但代价结构正好相反

树状数组:单点改 O(log n) = 17 步 Python 循环,区间查 2 * 17 步
分块:    单点改 O(1)(改 a[i] 和所属块的和),区间查 O(sqrt n)
          ——而且查询那一步是 sum(切片),**整段跑在 C 层**

本题的修改次数(6e5)远多于查询次数(1e5), 所以要把成本压到修改这一侧:分块的单点改只有两条赋值语句, 而树状数组要走 17 步纯 Python 循环,两者差了一个数量级。

n = q = 1e5 的最坏数据实测:树状数组版 0.56 秒,分块版 0.35 秒。 注意 0.56 秒并不安全:判题机的机器通常比本机慢数倍, 本地耗时要留出 3~4 倍余量再去对照时限, 否则「本地看着够快」和「判题机上超时」完全可以同时成立。

数据规模与复杂度

n, q <= 1e5,a_i <= 1e7。块长取 B = 320(约 sqrt(n))。 开根总次数 <= 6n = 6e5,每次 O(1); 查询每次至多扫 2B 个元素 + n/B 个块和,都由 C 层的 sum 完成。

坑在哪

  1. 必须用 math.isqrt,不能用 int(x ** 0.5): 后者在 1e7 附近可能因浮点误差差 1; (注意 isqrt 是 3.8+ 才有的,牛客的 PyPy3 比 3.8 老,没有这个函数, 所以这题只能用 Python3 交,不能退化到 PyPy3。)
  2. 判「稳定」的条件是 <= 1(0 和 1 开根都是自己),不是 == 1;
  3. 2026-01-21 题面更新后 a_i >= 0,不必讨论负数开根;
  4. 并查集的 find 用迭代 + 路径减半,别写递归(深度可到 1e5);
  5. 查询要分「同块」与「跨块」两种情形:同块时直接 sum(a[l:r+1]), 跨块才是「左残块 + 中间整块的块和 + 右残块」, 写成统一形式会在 kl == kr 时把中间那段算重。

参考实现

solutions/BISHI127.py
import sys
from math import isqrt

B = 320                                      # 块长,约 sqrt(n)


def main() -> None:
    # 输入最多 3e5 个整数,一次性读入再切分,比逐行 readline 快一个数量级
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); q = int(data[1])
    a = [int(v) for v in data[2:2 + n]]      # 这里用 0 下标,块号 = i // B

    # 分块预处理:把数组切成 nb 个长为 B 的块,bsum[k] 是第 k 块的元素和。
    # 之后单点修改只需同时改 a[i] 和 bsum[i // B],两条赋值语句。
    nb = (n + B - 1) // B
    bsum = [sum(a[k * B:(k + 1) * B]) for k in range(nb)]   # 末块通常不满,切片会自动截断

    # 并查集初始化:nxt[i] 指向 i 右边第一个还没稳定的位置。
    # 多开一格到 n 作哨兵,find 走到 n 就表示「右边全稳定了」,不必额外判越界。
    nxt = list(range(n + 1))                 # 并查集:右边第一个 a > 1 的位置
    for i in range(n):
        if a[i] <= 1:                        # 0 和 1 开根还是自己,一开始就摘出去
            nxt[i] = i + 1

    def find(x):
        """返回 x 自身或它右边第一个尚未稳定(a > 1)的位置。"""
        while nxt[x] != x:
            nxt[x] = nxt[nxt[x]]             # 路径减半
            x = nxt[x]
        return x

    p = 2 + n                                # 操作段在 data 里的起始位置
    out = []
    push = out.append                        # 绑定成局部名,省掉循环里每次的属性查找
    for _ in range(q):
        op = data[p]                         # 保持 bytes 原样比较,省一次 int() 转换
        l = int(data[p + 1]) - 1             # 题面下标从 1 起,这里统一减 1 转成 0 下标
        r = int(data[p + 2]) - 1
        p += 3
        if op == b"1":                       # 区间开根:只碰还没稳定的位置
            i = find(l)                      # 从 l 起第一个 a > 1 的位置开始
            # 每个数最多被开 6 次根就落到 <= 1,所以这个循环全程总共只转 O(6n) 次
            while i <= r:
                old = a[i]
                new = isqrt(old)
                a[i] = new
                bsum[i // B] += new - old    # 单点改块和,O(1)
                if new <= 1:                 # 稳定了,从并查集里摘掉
                    nxt[i] = i + 1
                i = find(i + 1)              # 跳到下一个未稳定位置,跳过的都是死值
        else:                                # 区间求和
            kl = l // B                      # 左端点所在块
            kr = r // B                      # 右端点所在块
            if kl == kr:                     # 同块,直接扫这一段
                push(sum(a[l:r + 1]))
            else:                            # 左残块 + 中间整块 + 右残块
                push(sum(a[l:(kl + 1) * B])
                     + sum(bsum[kl + 1:kr])
                     + sum(a[kr * B:r + 1]))
    sys.stdout.write("\n".join(map(str, out)) + "\n")


main()
[:octicons-arrow-left-16: BISHI126](BISHI126.md) [BISHI128 :octicons-arrow-right-16:](BISHI128.md)