【题目描述】
原题来自: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的序列,支持两种操作:
-
区间开平方 :对区间
[L,R]内的每一个数向下取整开平方(x =)。
-
区间求和 :查询区间
[L,R]内所有数的总和。
数据规模 :,
,初始数值
。
面对高频的区间修改和查询,我们的第一反应通常是带懒标记的线段树。但是本题藏着一个致命的冲突:
开平方运算不满足分配律, 即 。
这意味着我们无法通过一个区间的"原总和"直接推算出它"整体开平方后的总和"。传统的Lazy Tag 在这里彻底失效,如果强行每次遍历到底层叶子节点去修改,时间复杂度必然退化为O(NM)导致 TLE。
二、 思考过程:数学常识带来的降维打击
既然无法在区间层面直接修改,这题的破局点究竟在哪?答案隐藏在数字的衰减速度中。
题目给定最大数字为10^9。我们来徒手开几次平方:
惊天发现 :即使是10^9这么巨大的数字,**最多只需要连续开6次平方,就会变成1,**而1和0再怎么开平方,永远都是它自己。
这意味着,在20万次的修改操作中,对于序列中的任意一个数字,只有前6次修改是有效的,后面的修改全是废话。
三、 算法设计:势能线段树(暴力单点修改+剪枝)
基于上述发现,我们抛弃复杂的懒标记,采用一种"看似暴力,实则极快"的策略:势能线段树。
-
结构升级 :除了维护区间和
val,我们在每个节点多维护一个区间最大值ma。 -
剪枝(核心) :在执行区间修改时,如果当前节点管辖区间的最大值
ma<=1,说明这个区间里全都是0或1,直接return,绝对不往下走。 -
暴力触底 :如果
ma>1,说明区间里还有可以开方的数,我们就硬着头皮顺着线段树往下找,一直找到叶子节点,对其单独开平方。 -
精准制导 :在分发任务时,利用
L<=mid和R>mid作为精准过滤器,只去与目标区间有交集的子树,绝不访问毫无关系的节点。
四、 时空复杂度分析
-
时间复杂度:
-
区间查询:标准线段树查询,单次
。
-
区间修改(势能分析):看似每次都要深入叶子节点,但每个叶子节点一生中最多只会被深入修改6次 。一旦变成 1,它祖先节点的
ma就会形成天然的"防弹衣",挡住后续所有的修改指令。因此,全局所有修改操作的均摊时间复杂度为(带一个极小的常数 6)。
-
总时间复杂度:完美通过20万的数据测试。
-
-
空间复杂度:线段树标准4倍空间,即O(4N)。
五、 易错点总结
-
出题人的恶意(乱序区间):
信息学奥赛一本通保证了l<=r,但是洛谷的测试数据中存在大量l>r的情况。如果不加判断直接丢进线段树,会导致死递归或越界。
防雷措施 :读入后立刻加上
if(l>r) swap(l,r);。 -
数据类型溢出:
10^5个10^9级别的数相加,总和远超
int范围。原数组a、线段树的val乃至求和函数query的返回值,都必须使用long long。 -
进一步优化:
在优秀的精准制导下(
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;
}