上帝造题的七分钟(线段树 + 区间开方 + 势能分析剪枝)

上帝造题的七分钟(线段树 + 区间开方 + 势能分析剪枝)

一、题目描述

维护一个正整数数列,支持两种操作:

  • 0 l r:把区间 [l, r] 中的每个数开平方(下取整)
  • 1 l r:询问区间 [l, r] 的和

注意:输入中可能出现 l > r,此时要交换 l 和 r。

对应题目:洛谷 P4145 上帝造题的七分钟 2 / 花神游历各国

二、思路分析

直接做的问题

区间开方不像区间加、区间乘那样可以直接用懒标记处理,因为:

sqrt(a + b) ≠ sqrt(a) + sqrt(b)

无法在O(1)的时间快速求解
所以无法把「开方」这个操作直接打在懒标记上。

关键观察

一个数 x 反复开平方,会迅速变小:

  • 10^12 开方 1 次 → 10^6
  • 开方 2 次 → 10^3
  • 开方 3 次 → 31
  • 开方 4 次 → 5
  • 开方 5 次 → 2
  • 开方 6 次 → 1

到 1 之后,再怎么开方还是 1,不再变化。

所以每个数最多被有效开方 6 次左右,之后就是无效操作。

优化策略

线段树每个节点额外维护一个 mx(区间最大值):

  • 如果 mx <= 1,说明这个区间所有数都已经是 1,开方没有意义,直接返回
  • 否则继续递归

这样每个位置最多被真正更新 6 次,总复杂度 O(n log n × 6)。

三、算法流程

  1. 建树,每个节点维护 sum(区间和)和 mx(区间最大值)
  2. 对每个操作:
    • op = 0(开方):
      • 如果 seg[p].mx <= 1,直接返回
      • 如果是叶子节点,sum = mx = sqrt(mx)
      • 否则递归左右儿子,再 push_up
    • op = 1(查询):
      • 标准区间查询和

四、AC 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N = 1e5 + 5;
ll a[N];
int n;

struct node
{
    int L, R;
    ll mx;    // 区间最大值
    ll sum;   // 区间和
} seg[N * 4];

// 向上合并
void push_up(int p)
{
    seg[p].mx = max(seg[p << 1].mx, seg[p << 1 | 1].mx);
    seg[p].sum = seg[p << 1].sum + seg[p << 1 | 1].sum;
}

// 建树
void build(int p, int L, int R)
{
    seg[p].L = L, seg[p].R = R;
    if (L == R)
    {
        seg[p].mx = seg[p].sum = a[L];
        return;
    }
    int mid = (L + R) >> 1;
    build(p << 1, L, mid);
    build(p << 1 | 1, mid + 1, R);
    push_up(p);
}

// 区间开方
void update(int p, int L, int R)
{
    // 关键剪枝:区间最大值 <= 1,开方无意义
    if (seg[p].mx <= 1) return;

    int l = seg[p].L, r = seg[p].R;

    // 叶子节点:直接开方
    if (l == r)
    {
        seg[p].sum = seg[p].mx = sqrt(seg[p].mx);
        return;
    }

    int mid = (l + r) >> 1;
    if (L <= mid) update(p << 1, L, R);
    if (R > mid) update(p << 1 | 1, L, R);

    push_up(p);
}

// 区间求和
ll query(int p, int L, int R)
{
    int l = seg[p].L, r = seg[p].R;
    if (L <= l && r <= R) return seg[p].sum;

    int mid = (l + r) >> 1;
    ll res = 0;
    if (L <= mid) res += query(p << 1, L, R);
    if (R > mid) res += query(p << 1 | 1, L, R);
    return res;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    build(1, 1, n);

    int m;
    cin >> m;
    int op, l, r;

    while (m--)
    {
        cin >> op >> l >> r;
        if (l > r) swap(l, r);   // 题目可能出现 l > r

        if (op == 1)
            cout << query(1, l, r) << endl;
        else
            update(1, l, r);
    }

    return 0;
}

五、复杂度

操作 时间复杂度
建树 O(n)
单次开方 均摊 O(log n)
单次查询 O(log n)
总复杂度 O((n + m) log n)

每个数最多被有效开方 6 次(因为 10^12 开 6 次方就到 1),所以开方操作的总代价是 O(6n log n),完全可以接受。

六、相关题目

七、总结

要点 内容
核心难点 区间开方不能用懒标记直接处理
关键观察 一个数最多有效开方 6 次,到 1 后不再变化
优化方式 维护区间最大值,mx <= 1 时剪枝
复杂度 均摊 O((n + m) log n)
posted @ 2026-10-14 23:23  leaderling  阅读(6)  评论(0)    收藏  举报