花神游历各国(信息学奥赛一本通- P1550)(洛谷-P4145)

【题目描述】

原题来自:BZOJ 3211

花神喜欢步行游历各国,顺便虐爆各地竞赛。花神有一条游览路线,它是线型的,也就是说,所有游历国家呈一条线的形状排列,花神对每个国家都有一个喜欢程度(当然花神并不一定喜欢所有国家)。

每一次旅行中,花神会选择一条旅游路线,它在那一串国家中是连续的一段,这次旅行带来的开心值是这些国家的喜欢度的总和,当然花神对这些国家的喜欢程序并不是恒定的,有时会突然对某些国家产生反感,使他对这些国家的喜欢度 δ 变为 δ‾√(可能是花神虐爆了那些国家的 OI,从而感到乏味)。

现在给出花神每次的旅行路线,以及开心度的变化,请求出花神每次旅行的开心值。

【输入】

第一行是一个整数 N,表示有 N 个国家;

第二行有 N 个空格隔开的整数,表示每个国家的初始喜欢度 δi ;

第三行是一个整数 M,表示有 M 条信息要处理;

第四行到最后,每行三个整数 x,l,r,当 x=1 时询问游历国家 l 到 r 的开心值总和,就是 ∑ri=lδi ,当 x=2 时国家 l 到 r 中每个国家的喜欢度 δi变为 δi‾‾√ 。

【输出】

每次 x=1 时,每行一个整数。表示这次旅行的开心度。

【输入样例】

复制代码
4
1 100 5 5
5
1 1 2
2 1 2
1 1 2
2 2 3
1 1 4

【输出样例】

复制代码
101
11
11

【提示】

数据范围与提示:

对于全部数据,1≤n≤10^5,1≤m≤2×10^5,1≤l≤r≤n,0≤δi≤10^9 。

注:建议使用 sqrt 函数,且向下取整。

一、 题目分析

本题要求我们维护一个长度为N的序列,支持两种操作:

  1. 区间开平方 :对区间 [L,R] 内的每一个数向下取整开平方(x =)。

  2. 区间求和 :查询区间 [L,R] 内所有数的总和。

数据规模,初始数值

面对高频的区间修改和查询,我们的第一反应通常是带懒标记的线段树。但是本题藏着一个致命的冲突:

开平方运算不满足分配律,

这意味着我们无法通过一个区间的"原总和"直接推算出它"整体开平方后的总和"。传统的Lazy Tag 在这里彻底失效,如果强行每次遍历到底层叶子节点去修改,时间复杂度必然退化为O(NM)导致 TLE。


二、 思考过程:数学常识带来的降维打击

既然无法在区间层面直接修改,这题的破局点究竟在哪?答案隐藏在数字的衰减速度中。

题目给定最大数字为10^9。我们来徒手开几次平方:

惊天发现 :即使是10^9这么巨大的数字,**最多只需要连续开6次平方,就会变成1,**而1和0再怎么开平方,永远都是它自己。

这意味着,在20万次的修改操作中,对于序列中的任意一个数字,只有前6次修改是有效的,后面的修改全是废话


三、 算法设计:势能线段树(暴力单点修改+剪枝)

基于上述发现,我们抛弃复杂的懒标记,采用一种"看似暴力,实则极快"的策略:势能线段树

  1. 结构升级 :除了维护区间和val,我们在每个节点多维护一个区间最大值ma

  2. 剪枝(核心) :在执行区间修改时,如果当前节点管辖区间的最大值ma<=1,说明这个区间里全都是0或1,直接return,绝对不往下走

  3. 暴力触底 :如果 ma>1,说明区间里还有可以开方的数,我们就硬着头皮顺着线段树往下找,一直找到叶子节点,对其单独开平方。

  4. 精准制导 :在分发任务时,利用L<=midR>mid作为精准过滤器,只去与目标区间有交集的子树,绝不访问毫无关系的节点。


四、 时空复杂度分析

  • 时间复杂度

    • 区间查询:标准线段树查询,单次

    • 区间修改(势能分析):看似每次都要深入叶子节点,但每个叶子节点一生中最多只会被深入修改6次 。一旦变成 1,它祖先节点的ma就会形成天然的"防弹衣",挡住后续所有的修改指令。因此,全局所有修改操作的均摊时间复杂度为(带一个极小的常数 6)。

    • 总时间复杂度:完美通过20万的数据测试。

  • 空间复杂度:线段树标准4倍空间,即O(4N)。


五、 易错点总结

  1. 出题人的恶意(乱序区间)

    信息学奥赛一本通保证了l<=r,但是洛谷的测试数据中存在大量l>r的情况。如果不加判断直接丢进线段树,会导致死递归或越界。

    防雷措施 :读入后立刻加上 if(l>r) swap(l,r);

  2. 数据类型溢出

    10^5个10^9级别的数相加,总和远超int范围。原数组a、线段树的val乃至求和函数 query的返回值,都必须使用long long

  3. 进一步优化

    在优秀的精准制导下(L<=mid 进左,R>mid 进右),一旦触发l==r 触底,该点必定 在目标区间 [L,R] 内。因此,我写的越界防御 if(L>r||R<l) return; 其实是一句永远不会被触发的死代码,直接删去可进一步优化常数。


六、 完整代码

cpp 复制代码
// //区间修改 区间查询 线段树 
// #include <iostream>
// #include <algorithm>//对应max函数
// #include <cmath>//对应sqrt函数
// using namespace std;
// int n,m;
// const int maxn=100010;//可能的最多国家数
// long long a[maxn];//每个国家的初始喜欢度
// struct node{
//     long long val;//记录当前节点管辖区间的开心值总和
//     long long ma;//当前节点管辖区间的最大快乐值
// }tree[maxn<<2];//线段树大小要开国家数4倍

// //通过左右儿子更新父节点总快乐值,及区间最大快乐值
// void pushup(int rt){
//     tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
//     tree[rt].ma=max(tree[rt<<1].ma,tree[rt<<1|1].ma);
// }

// //当前节点为rt,当前节点管辖区间为[l,r]
// void build(int l,int r,int rt){
//     //当递归到叶子节点时 叶子节点的开心值就是自己的开心值
//     //叶子节点管辖区间最大值也就是自己的开心值
//     if(l==r){
//         tree[rt].val=a[l];
//         tree[rt].ma=a[l];
//         return;
//     }
//     int mid=(l+r)>>1;
//     //递归构造左子树
//     build(l,mid,rt<<1);
//     //递归构造右子树
//     build(mid+1,r,rt<<1|1);
//     //回溯 通过左右儿子更新rt本身
//     pushup(rt);
// }

// //[L,R]区间修改 当前节点为rt rt所管辖区间为[l,r]
// void update(int L,int R,int l,int r,int rt){
//     //当前节点所管辖区间属于[L,R]
//     if(L<=l&&R>=r){
//         //如果区间最大值已经小于等于1 则不会再发生变化
//         if(tree[rt].ma<=1){
//             return;
//         }
//         //如果区间最大值大于1,则递归修改区间内每个叶子节点
//         else{
//             //递归到叶子节点 
//             if(l==r){
//                 tree[rt].val=sqrt(tree[rt].val);
//                 tree[rt].ma=sqrt(tree[rt].ma); 
//                 return;
//             }
//         }
//     }
//     if(L>r||R<l) return;
//     int mid=(l+r)>>1;
//     if(L<=mid) update(L,R,l,mid,rt<<1);
//     if(R>mid) update(L,R,mid+1,r,rt<<1|1);
//     pushup(rt);
// }

// //[L,R]为当前查询区间,rt为当前节点,[l,r]为当前节点管辖区间
// long long query(int L,int R,int l,int r,int rt){
//     if(L<=l&&R>=r) return tree[rt].val;
//     if(L>r||R<l) return 0;
//     int mid=(l+r)>>1;
//     long long ans=0;
//     if(L<=mid) ans+=query(L,R,l,mid,rt<<1);
//     if(R>mid) ans+=query(L,R,mid+1,r,rt<<1|1);
//     return ans;
// }

// int main(){
//     //io加速
//     ios::sync_with_stdio(false);
//     cin.tie(0);
//     cin>>n;
//     for(int i=1;i<=n;i++) cin>>a[i];
//     cin>>m;
//     build(1,n,1);
//     //执行m次操作
//     while(m--){
//         int x,l,r;
//         cin>>x>>l>>r;
//         //信息学奥赛一本通这道题给出了l<=r,但是洛谷这道题写出了l可能大于r
//         //所以洛谷需要判断l r大小
//         if (l>r) swap(l,r);
//         if(x==1){//询问游历国家l到r的开心值总和
//             cout<<query(l,r,1,n,1)<<"\n";
//         }
//         else{//国家l到r中每个国家的喜欢度δ变为sqrt(δ)
//             update(l,r,1,n,1);
//         }
//     }
//     return 0;
// }

//区间修改 区间查询 线段树 优化
#include <iostream>
#include <algorithm>//对应max函数
#include <cmath>//对应sqrt函数
using namespace std;
int n,m;
const int maxn=100010;//可能的最多国家数
long long a[maxn];//每个国家的初始喜欢度
struct node{
    long long val;//记录当前节点管辖区间的开心值总和
    long long ma;//当前节点管辖区间的最大快乐值
}tree[maxn<<2];//线段树大小要开国家数4倍

//通过左右儿子更新父节点总快乐值,及区间最大快乐值
void pushup(int rt){
    tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
    tree[rt].ma=max(tree[rt<<1].ma,tree[rt<<1|1].ma);
}

//当前节点为rt,当前节点管辖区间为[l,r]
void build(int l,int r,int rt){
    //当递归到叶子节点时 叶子节点的开心值就是自己的开心值
    //叶子节点管辖区间最大值也就是自己的开心值
    if(l==r){
        tree[rt].val=a[l];
        tree[rt].ma=a[l];
        return;
    }
    int mid=(l+r)>>1;
    //递归构造左子树
    build(l,mid,rt<<1);
    //递归构造右子树
    build(mid+1,r,rt<<1|1);
    //回溯 通过左右儿子更新rt本身
    pushup(rt);
}

//[L,R]区间修改 当前节点为rt rt所管辖区间为[l,r]
void update(int L,int R,int l,int r,int rt){
    //如果区间最大值已经小于等于1 则不会再发生变化
    if(tree[rt].ma<=1){
        return;
    }
    //如果区间最大值大于1,则递归修改区间内每个叶子节点
    //递归到叶子节点 
    if(l==r){
        tree[rt].val=sqrt(tree[rt].val);
        tree[rt].ma=sqrt(tree[rt].ma); 
        return;
    }
    if(L>r||R<l) return;
    int mid=(l+r)>>1;
    if(L<=mid) update(L,R,l,mid,rt<<1);
    if(R>mid) update(L,R,mid+1,r,rt<<1|1);
    pushup(rt);
}

//[L,R]为当前查询区间,rt为当前节点,[l,r]为当前节点管辖区间
long long query(int L,int R,int l,int r,int rt){
    if(L<=l&&R>=r) return tree[rt].val;
    if(L>r||R<l) return 0;
    int mid=(l+r)>>1;
    long long ans=0;
    if(L<=mid) ans+=query(L,R,l,mid,rt<<1);
    if(R>mid) ans+=query(L,R,mid+1,r,rt<<1|1);
    return ans;
}

int main(){
    //io加速
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    cin>>m;
    build(1,n,1);
    //执行m次操作
    while(m--){
        int x,l,r;
        cin>>x>>l>>r;
        //信息学奥赛一本通这道题给出了l<=r,但是洛谷这道题写出了l可能大于r
        //所以洛谷需要判断l r大小
        if (l>r) swap(l,r);
        if(x==1){//询问游历国家l到r的开心值总和
            cout<<query(l,r,1,n,1)<<"\n";
        }
        else{//国家l到r中每个国家的喜欢度δ变为sqrt(δ)
            update(l,r,1,n,1);
        }
    }
    return 0;
}
相关推荐
Mr_Xuhhh2 小时前
LeetCode 热题 100 刷题笔记:数组与排列的经典解法(续)
算法·leetcode·职场和发展
炽烈小老头2 小时前
【每天学习一点算法 2026/03/29】搜索二维矩阵 II
学习·算法·矩阵
靴子学长2 小时前
Qwen3.5 架构手撕源码
算法·架构·大模型
寒月小酒3 小时前
3.28 OJ
算法
AI成长日志3 小时前
【笔面试算法学习专栏】堆与优先队列专题:数组中的第K个最大元素与前K个高频元素
学习·算法·面试
雅俗共赏1003 小时前
医学图像重建中常用的正则化分类
算法
IronMurphy3 小时前
【算法三十二】
算法
Mr_Xuhhh3 小时前
LeetCode 热题 100 刷题笔记:高频面试题详解(215 & 347)
算法·leetcode·排序算法
mmz12073 小时前
贪心算法3(c++)
c++·算法·贪心算法