势能线段树
势能
在信息学中,势能被用于计算某一个过程,或者某一类过程时间复杂度的总和。
势能均摊复杂度
在计算时间复杂度的时候
我们比起用O(总复杂度)=ΣO(f)这种和式的表示方法。
更喜欢使用O(总复杂度)=N*O(f)这种嵌套乘法原理的形式。
这样就提出了势能均摊复杂度
在N个数求gcd问题中,总时间复杂度为O(N+logC)
那么势能均摊复杂度就为O(N+logC)/N=O(1+logC/N)=O(1)
我们就称在N个数求gcd问题中,gcd函数的“势能均摊复杂度”为O(1)。
注意:
在可持久化数据结构的学习中,经常能看见“XX数据结构不支持可持久化,会使势能均摊失效”。
当你使用“势能均摊”的时候,就必须保证函数调用是均匀的,或者整个函数的调用状态是不可重现的。
可持久化的结构由于其O(1)版本记录的特性,显然会使得操作被重现,当然,不止是使用可持久化数据结构的时候要注意,任何可重置函数调用状态的操作在均摊算法中都是应该被警惕的。
势能线段树
具体例题
所谓势能线段树,是指在懒标记无法正常使用的情况下,暴力到叶子将线段树当成数组一样用进行修改。
这个时候的复杂度计算就不是很直观了,所以引入势能的概念。
每一个节点都有一个关于开根号操作的“势能”,然后开根号的时候势能一定是递减的。
注意引入势能分析是忽略过程,只考虑所有过程的总和。
即我们不管在一次暴力中到底一个树节点被访问了几次(单次操作的时间复杂度可能很大,可能是O(N))但是这些暴力的总量是势能上限。
这道题的时间复杂度就为
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#define ll long long
#define pa pair<int,int>
using namespace std;
const int maxn=1000000+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,m,a[maxn];
struct Segment{
ll tr[maxn],sum[maxn];
void update(int k){
tr[k]=max(tr[k<<1],tr[k<<1|1]);
sum[k]=sum[k<<1]+sum[k<<1|1];
}
void build(int k,int l,int r){
if(l==r){tr[k]=a[l];sum[k]=tr[k];return;}
int mid=(l+r)>>1;
build(k<<1,l,mid);build(k<<1|1,mid+1,r);
update(k);
}
void modify(int k,int l,int r,int L,int R){
if(l>R || r<L)return;
if(L<=l && r<=R && tr[k]==1)return ;
if(l==r){
tr[k]=sqrt(tr[k]);sum[k]=tr[k];return;
}
int mid=(l+r)>>1;
tr[k]=sqrt(tr[k]);
modify(k<<1,l,mid,L,R);modify(k<<1|1,mid+1,r,L,R);
update(k);
}
ll query(int k,int l,int r,int L,int R){
if(l>R || r<L)return 0;
if(L<=l && r<=R)return sum[k];
int mid=(l+r)>>1;
return query(k<<1,l,mid,L,R)+query(k<<1|1,mid+1,r,L,R);
}
}T;
int main(){
n=read();m=read();
for(int i=1;i<=n;i++)a[i]=read();
T.build(1,1,n);
while(m--){
int opt=read(),l=read(),r=read();
if(opt==1)T.modify(1,1,n,l,r);
else printf("%lld\n",T.query(1,1,n,l,r));
}
return 0;
}
例题2
此题是上一题待修改版本
显然如果势能定义为开根号的次数,显然是会使势能均摊失效,因为修改数会使得开根号的势能改变
所以如果势能不是单调减少的,那么就必须确保每次对于势能提高的上限不能太大。
首先,一个区间的数多次开根号后会相同
所以我们定义势能为区间的max-min=diff,diff在开根号的过程中在单调递减
而在区间加某个数的时候,区间diff=0(所有数相同)时,diff不改变,那么我们可以直接区间开根求和,可以O(1)处理一个区间
而, 类似如下图,紫色为最大值,绿色为最小值
首先设当前线段树diff=0,每个数都为1
那么我们[2,7]加数100后(如上图所示)
[1,8]开根,那么需要暴力开根的有5个节点(蓝色圈出的)
不难开出上述操作是最坏情况
那么可以算出时间复杂度约为
那么m次操作时间复杂度上限为
可以这么认为,对于一个当前势能为0的线段树,进行修改后,单次修改有logN个节点的势能改变
那么此线段树的势能就近似增加了logN*logN,这个势能就是修改后的线段树进行开根为势能为0的线段树的时间复杂度
也就是暴力修改的时间复杂度
那么总时间复杂度可以看做
O(M×∣0势能时线段树操作时间复杂度∣+N×∣节点势能上限降低至0势能时间复杂度∣+M×∣线段树单次操作影响到的节点数目∣×∣操作额外提供的势能∣)。
但要注意diff在开根号的过程中并不是严格单调递减
这是因为最大最小值是开根取整,会有精度损失
比如4,3开根号为2,1;diff的没有变化,都是1
那么对于一个序列4 3 4 3 4 3
开完一次根为2 1 2 1 2 1
在加2为4 3 4 3 4 3
会导致每次都不能区间整个开根,时间复杂度变为
虽然开根会有精度损失,但精度损失不会超过0.9999···这就意味着只有在整数diff差值为1的时候会出现,那么我们特判一下就好了
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#define ll long long
#define pa pair<int,int>
using namespace std;
const int maxn=1000000+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,m,a[maxn];
struct Segment{
struct wzq{
ll val,sum;
ll maxx,minn;
ll lz;
//lz是区间加减一个数
wzq(){val=sum=lz=0;}
}tr[maxn];
ll ssqrt(ll x){return sqrt(x+0.5);}
ll cal_dalta(ll x){ //求x和x开根后的改变量
return x-ssqrt(x);
}
void update(int k){
tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
tr[k].maxx=max(tr[k<<1].maxx,tr[k<<1|1].maxx);
tr[k].minn=min(tr[k<<1].minn,tr[k<<1|1].minn);
}
void pushdown(int k,int l,int r){
if(tr[k].lz){
if(l!=r){
tr[k<<1].lz+=tr[k].lz;
tr[k<<1|1].lz+=tr[k].lz;
}
tr[k].sum+=(ll)(r-l+1)*tr[k].lz;
tr[k].maxx+=tr[k].lz;
tr[k].minn+=tr[k].lz;
}
tr[k].lz=0;
}
void build(int k,int l,int r){
if(l==r){
tr[k].sum=a[l];
tr[k].maxx=tr[k].minn=a[l];
return;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);build(k<<1|1,mid+1,r);
update(k);
}
void add(int k,int l,int r,int L,int R,ll val){
pushdown(k,l,r);
if(l>R || r<L)return ;
if(L<=l && r<=R){
tr[k].lz+=val;
pushdown(k,l,r);
return ;
}
int mid=(l+r)>>1;
add(k<<1,l,mid,L,R,val);add(k<<1|1,mid+1,r,L,R,val);
update(k);
return ;
}
void modify(int k,int l,int r,int L,int R){
pushdown(k,l,r);
if(l>R || r<L)return;
if(L<=l && r<=R && cal_dalta(tr[k].maxx)==cal_dalta(tr[k].minn)){
//改变量相同就可以区间操作==0势能 或势能为1的情况
tr[k].lz-=cal_dalta(tr[k].maxx);
pushdown(k,l,r);
return ;
}
int mid=(l+r)>>1;
modify(k<<1,l,mid,L,R);modify(k<<1|1,mid+1,r,L,R);
update(k);
}
ll query(int k,int l,int r,int L,int R){
pushdown(k,l,r);
if(l>R || r<L)return 0;
if(L<=l && r<=R)return tr[k].sum;
int mid=(l+r)>>1;
ll ans=query(k<<1,l,mid,L,R)+query(k<<1|1,mid+1,r,L,R);
update(k);return ans;
}
}T;
int main(){
n=read();m=read();
for(int i=1;i<=n;i++)a[i]=read();
T.build(1,1,n);
while(m--){
int opt=read(),l=read(),r=read();
if(opt==1)T.modify(1,1,n,l,r);
else if(opt==3)printf("%lld\n",T.query(1,1,n,l,r));
else {
ll x=read();
T.add(1,1,n,l,r,x);
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效