势能线段树

势能

在信息学中,势能被用于计算某一个过程,或者某一类过程时间复杂度的总和。

势能均摊复杂度

在计算时间复杂度的时候
我们比起用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))但是这些暴力的总量是势能上限。

这道题的时间复杂度就为O(mlogN+4N6())

点击查看代码
#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)处理一个区间
diff0, 类似如下图,紫色为最大值,绿色为最小值
image
首先设当前线段树diff=0,每个数都为1
那么我们[2,7]加数100后(如上图所示)
[1,8]开根,那么需要暴力开根的有5个节点(蓝色圈出的)
不难开出上述操作是最坏情况
那么可以算出时间复杂度约为O(5(566)O(log2N)
那么m次操作时间复杂度上限为O(mlog2N)
可以这么认为,对于一个当前势能为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
会导致每次都不能区间整个开根,时间复杂度变为O(NM)
虽然开根会有精度损失,但精度损失不会超过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;
}
posted @   I_N_V  阅读(967)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示