势能线段树

什么是势能线段树

与普通线段树不同。

普通线段树需要满足区间合并和 \(Lazy\) 标记来保证时间复杂度,每次递归到底部会使复杂度退化。

而势能线段树在此基础之上选用恰当的势能,保证暴力操作的上限从而保证时间复杂度。

例题

P4145 上帝造题的七分钟 2 / 花神游历各国

题意

维护一个长度为 \(n\) 的正整数序列 \(a_i\) ,支持以下两种操作:

  • 1 l r :将一个区间中的所有数开方,向下取整。
  • 2 l r :查询一个区间中所有数的和。

其中, \(1 \leq n,m \leq 1 \times 10^5\)\(\forall 1 \leq i \leq n\) ,都有 \(1 \leq a_i \leq 10^{12}\)

解法

区间开方操作不方便通过懒标记得到信息。

但是发现该题中最大的数 \(10^{12}\) 只需要执行 \(6\) 次开方操作就会变为 \(1\) ,所以一个数执行开方操作的次数的大约是 \(O(\log \log V)\) 的。

于是我们建立一颗线段树,每个结点维护最大值和区间和,如果最大值为 \(1\) 则不进行操作,否则暴力递归。

因为一个数最多执行 \(O(\log \log V ))\) 次开方操作,所以总复杂度是\(O(n \log n \log \log V + q \log n)\),可以通过本题。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+100;
int n,m;ll a[N];
struct Segment{
#define ls rt*2
#define rs rt*2+1
	struct node{
		ll tag,mx,mn,sum;
	}t[N*4];
	void pushtag(int rt,int l,int r,ll k){
		t[rt].mn-=k,t[rt].mx-=k;
		t[rt].tag+=k,t[rt].sum-=(r-l+1)*k;
	}
	void pushdown(int rt,int l,int r){
		if(t[rt].tag!=0){
			int mid=(l+r)/2;
			pushtag(ls,l,mid,t[rt].tag);
			pushtag(rs,mid+1,r,t[rt].tag);
			t[rt].tag=0;
		}
	}
	void pushup(int rt,int l,int r){
		t[rt].mx=max(t[ls].mx,t[rs].mx);
		t[rt].mn=min(t[ls].mn,t[rs].mn);
		t[rt].sum=t[ls].sum+t[rs].sum;
	}
	void update(int rt,int l,int r,int L,int R){
		if(L<=l&&R>=r&&t[rt].mx==t[rt].mn){
			pushtag(rt,l,r,t[rt].mx-(ll)sqrt(1.0*t[rt].mx));
			return;
		}
		pushdown(rt,l,r);
		int mid=(l+r)/2;
		if(L<=mid)update(ls,l,mid,L,R);
		if(R>mid)update(rs,mid+1,r,L,R);
		pushup(rt,l,r);
	}
	ll query(int rt,int l,int r,int L,int R){
		if(L<=l&&R>=r)return t[rt].sum;
		pushdown(rt,l,r);
		int mid=(l+r)/2;
		if(L<=mid&&R>mid)return query(ls,l,mid,L,R)+query(rs,mid+1,r,L,R);
		if(L<=mid)return query(ls,l,mid,L,R);
		return query(rs,mid+1,r,L,R);
	}
	void build(int rt,int l,int r){
		if(l==r){
			t[rt]={0,a[l],a[l],a[l]};
			return;
		}
		int mid=(l+r)/2;
		build(ls,l,mid);build(rs,mid+1,r);
		pushup(rt,l,r);
	}
}t;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	t.build(1,1,n);
	scanf("%d",&m);
	while(m--){
		int op,l,r;
		scanf("%d %d %d",&op,&l,&r);
		if(l>r)swap(l,r);
		if(op==0){
			t.update(1,1,n,l,r);
		}else{
			printf("%lld\n",t.query(1,1,n,l,r));
		}
	}
}

AT_abc256_h [ABC256Ex] I like Query Problem

题意

给定 \(N\)\(Q\) 以及 \(A = (a_1, a_2, \dots, a_N)\)
请处理 \(Q\) 个如下所述的查询。查询有以下三种类型之一:

  • 1 L R x :将 \(i = L, L+1, \dots, R\)\(a_i\) 更新为 \(\left\lfloor \frac{a_i}{x} \right\rfloor\)
  • 2 L R y :将 \(i = L, L+1, \dots, R\)\(a_i\) 更新为 \(y\)
  • 3 L R :输出 \(\sum_{i=L}^R a_i\)

其中, \(1 \leq N \leq 5 \times 10^5\)\(1 \leq Q \leq 1 \times 10^5\) ,输入的所有数均为在 \([1,10^5]\) 之间。

解法

普通线段树不好做整除操作,但是如果一个区间中的数全部相同,那么可以直接当成区间减法 。

注意到如果 \(x\)\(1\) ,整个区间不变,否则区间内的数至少变为原来的一半,所以一个区间最多整除 \(O(n \log(n))\) 次区间中的数就会全部相同。

有了这个发现,就可以建线段树,每个结点记录最大值和最小值,如果最大值等于最小值,就打区间减法标记。

虽然这样复杂度是正确的,但是我们还可以优化。

有这样一种情况,一个区间中只有两种数,设为 \(a\)\(b\) ,如果\(a + 1 = b\) 并且 \(a \bmod x=0\) 那么也可以当做减法来做,比如这个区间中只有 \(5\)\(6\) 两种数,除以 \(3\) 向下取整,就会得到 \(1\)\(2\) ,相当于区间减了 \(4\)

这样总复杂度是 \(O(n \log n \log V + q \log n \log V)\) ,可以通过本题。

P10639 BZOJ4695 最假女选手

题意

给定一个长度为 \(n\) 的序列,要求支持以下 \(6\) 种操作:

  • 给一个区间 \([l,r]\) 加上一个整数 \(x\)
  • 把一个区间 \([l,r]\) 内小于 \(x\) 的数都变成 \(x\)
  • 把一个区间 \([l,r]\) 内大于 \(x\) 的数都变成 \(x\)
  • 求区间 \([l,r]\) 的和;
  • 求区间 \([l,r]\) 的最大值;
  • 求区间 \([l,r]\) 的最小值;

数据保证,\(1\leq n,m\leq 5\times 10^5\)\(|a_i|\leq 10^8\)

当进行 \(1\) 操作时,\(|x| \leq 1000\)

当进行 \(2,3\) 操作时,\(|x| \leq 10^8\)

解法

发现除了操作 \(2,3\) 其他操作都可以使用普通线段树完成,复杂度为 \(O(n \log n + q \log n)\)

现在考虑操作 \(2\) 。操作 \(2\) 其实是对一段区间对 \(x\)\(\max\) 发现这个东西不是很好用普通线段树维护。

我们对每一个结点维护区间最小值、次小值和最小值个数。

设最小值为 \(mn\) ,次小值为 \(sec\)

现在分 \(3\) 种情况讨论:

  • 如果 \(x \leq mn\) ,没有影响,直接返回
  • 如果 \(mn \lt x \lt sec\) ,显然,这个操作只会影响这个区间中的最小值,在这个节点上打一个最小值加 \(x - mn\) 的标记。
  • 如果 \(x \geq sec\) ,暴力递归左右儿子,直到变为前两种情况。

现在来说明为什么这么做复杂度是对的。

定义线段树上每一个点的势能函数 \(\varphi(u)\)\([min(u) \neq min(fa)]\) 。当向下暴力递归时,一定至少有 \(1\) 个儿子的 \(\varphi(u)\) 会减少\(1\) ,所以 \(1\) 的势能最多带来 \(O(\log n)\) 的时间复杂度(最坏情况递归一条链)。

初始时势能是 \(O(n)\) 的,每一次区间修改会影响 \(O(\log n)\) 个点。

所以总时间复杂度就是 \(O(n \log n + q \log ^ 2 n)\) 的。

代码

#include<bits/stdc++.h>
#define mid (l+r)/2
#define ls rt*2
#define rs ls+1
#define lson ls,l,mid
#define rson rs,mid+1,r
typedef long long ll;
using namespace std;
const int N=5e5+100,inf=INT_MAX;
int a[N],n,m;
struct tree{
	int mx1,mx2,mxcnt,mn1,mn2,mncnt,tag1,tag2,tag3;ll sum;
}t[N*8];
void pushtag1(int rt,int l,int r,int x){
	t[rt].tag1+=x,t[rt].mx1+=x,t[rt].mn1+=x,t[rt].sum+=(ll)(r-l+1)*x;
	if(t[rt].mx2!=-inf)t[rt].mx2+=x;
	if(t[rt].mn2!=inf)t[rt].mn2+=x;
	if(t[rt].tag2!=-inf)t[rt].tag2+=x;
	if(t[rt].tag3!=inf)t[rt].tag3+=x;
}
void pushtag2(int rt,int l,int r,int x){
	if(t[rt].mn1>=x)return;
	t[rt].sum+=(ll)(x-t[rt].mn1)*t[rt].mncnt;
	if(t[rt].mn1==t[rt].mx1)t[rt].mx1=x;
	if(t[rt].mn1==t[rt].mx2)t[rt].mx2=x;
	if(t[rt].tag3<x)t[rt].tag3=x;
	t[rt].mn1=x,t[rt].tag2=x;
}
void pushtag3(int rt,int l,int r,int x){
	if(t[rt].mx1<=x)return;
	t[rt].sum+=(ll)(x-t[rt].mx1)*t[rt].mxcnt;
	if(t[rt].mx1==t[rt].mn1)t[rt].mn1=x;
	if(t[rt].mx1==t[rt].mn2)t[rt].mn2=x;
	if(t[rt].tag2>x)t[rt].tag2=x;
	t[rt].mx1=x,t[rt].tag3=x;
}
void pushdown(int rt,int l,int r){
	if(t[rt].tag1){
		pushtag1(lson,t[rt].tag1);
		pushtag1(rson,t[rt].tag1);
		t[rt].tag1=0;
	}
	if(t[rt].tag2!=-inf){
		pushtag2(lson,t[rt].tag2);
		pushtag2(rson,t[rt].tag2);
		t[rt].tag2=-inf;
	}
	if(t[rt].tag3!=inf){
		pushtag3(lson,t[rt].tag3);
		pushtag3(rson,t[rt].tag3);
		t[rt].tag3=inf;
	}
}
void pushup(int rt,int l,int r){
	t[rt].sum=t[ls].sum+t[rs].sum;
	t[rt].mx1=max(t[ls].mx1,t[rs].mx1);
	t[rt].mx2=(t[ls].mx1==t[rs].mx1)?max(t[ls].mx2,t[rs].mx2):
	(t[ls].mx1>t[rs].mx1)?max(t[rs].mx1,t[ls].mx2):max(t[ls].mx1,t[rs].mx2);
	t[rt].mxcnt=(t[ls].mx1==t[rs].mx1)?t[ls].mxcnt+t[rs].mxcnt:(t[ls].mx1>t[rs].mx1)?t[ls].mxcnt:t[rs].mxcnt;
	t[rt].mn1=min(t[ls].mn1,t[rs].mn1);
	t[rt].mn2=(t[ls].mn1==t[rs].mn1)?min(t[ls].mn2,t[rs].mn2):
	(t[ls].mn1<t[rs].mn1)?min(t[rs].mn1,t[ls].mn2):min(t[ls].mn1,t[rs].mn2);
	t[rt].mncnt=(t[ls].mn1==t[rs].mn1)?t[ls].mncnt+t[rs].mncnt:(t[ls].mn1<t[rs].mn1)?t[ls].mncnt:t[rs].mncnt;
}
void update1(int rt,int l,int r,int L,int R,int x){
	if(L<=l&&R>=r){
		pushtag1(rt,l,r,x);
		return;
	}
	pushdown(rt,l,r);
	if(L<=mid)update1(lson,L,R,x);
	if(R>mid)update1(rson,L,R,x);
	pushup(rt,l,r);
}
void update2(int rt,int l,int r,int L,int R,int x){
	if(t[rt].mn1>=x)return;
	if(L<=l&&R>=r&&t[rt].mn2>x){
		pushtag2(rt,l,r,x);
		return;
	}
	pushdown(rt,l,r);
	if(L<=mid)update2(lson,L,R,x);
	if(R>mid)update2(rson,L,R,x);
	pushup(rt,l,r);
}
void update3(int rt,int l,int r,int L,int R,int x){
	if(t[rt].mx1<=x)return;
	if(L<=l&&R>=r&&t[rt].mx2<x){
		pushtag3(rt,l,r,x);
		return;
	}
	pushdown(rt,l,r);
	if(L<=mid)update3(lson,L,R,x);
	if(R>mid)update3(rson,L,R,x);
	pushup(rt,l,r);
}
ll query1(int rt,int l,int r,int L,int R){
	if(L<=l&&R>=r)return t[rt].sum;
	pushdown(rt,l,r);
	ll ans=0;
	if(L<=mid)ans+=query1(lson,L,R);
	if(R>mid)ans+=query1(rson,L,R);
	return ans;
}
ll query2(int rt,int l,int r,int L,int R){
	if(L<=l&&R>=r)return t[rt].mx1;
	pushdown(rt,l,r);
	ll ans=-inf;
	if(L<=mid)ans=max(ans,query2(lson,L,R));
	if(R>mid)ans=max(ans,query2(rson,L,R));
	return ans;
}
ll query3(int rt,int l,int r,int L,int R){
	if(L<=l&&R>=r)return t[rt].mn1;
	pushdown(rt,l,r);
	ll ans=inf;
	if(L<=mid)ans=min(ans,query3(lson,L,R));
	if(R>mid)ans=min(ans,query3(rson,L,R));
	return ans;
}
void build(int rt,int l,int r){
	t[rt].tag2=-inf,t[rt].tag3=inf;
	if(l==r){
		t[rt]={a[l],-inf,1,a[l],inf,1,0,-inf,inf,a[l]};
		return;
	}
	build(lson),build(rson);
	pushup(rt,l,r);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	build(1,1,n);
	scanf("%d",&m);
	while(m--){
		int op,l,r,x;
		scanf("%d %d %d",&op,&l,&r);
		if(op<=3)scanf("%d",&x);
		if(op==1)update1(1,1,n,l,r,x);
		if(op==2)update2(1,1,n,l,r,x);
		if(op==3)update3(1,1,n,l,r,x);
		if(op==4)printf("%lld\n",query1(1,1,n,l,r));
		if(op==5)printf("%lld\n",query2(1,1,n,l,r));
		if(op==6)printf("%lld\n",query3(1,1,n,l,r));
	}
}

P5693 EI 的第六分块

题意

给定一个整数序列 \(a\),支持两种操作:

  • 1 l r x 表示给区间 \([l,r]\) 中每个数加上 \(x\)
  • 2 l r 表示查询区间 \([l,r]\) 的最大子段和(可以为空)

其中, \(1\le n,q \le 4\times 10^5\)\(|a_i| \le 10^9\)\(1 \le x \le 10^6\)

解法

如果这个题是单点修改,那么是好做的。

每个结点维护区间和 \(sum\) ,最大前缀和 \(lmax\) ,最大后缀和 \(rmax\) ,最大字段和 \(max\)

区间合并时,就有

\(sum = ls.sum + rs.sum\)

\(lmax = \max(ls.lmax , ls.sum + rs.lmax)\)

\(rmax = \max(rs.rmax , rs.sum + rs.rmax)\)

\(max = \max(ls.max , rs.max , ls.rmax + rs.lmax)\)

这一部分是简单的。

现在考虑加上区间加操作,注意到这个题加的 \(x\) 都是正数,所以 \(lmax , rmax , max\) 的长度都不会缩短。

在实现的时候 \(lmax , rmax , max\) 都用一个二元组 \((k,b)\) 来表示,\(b\) 表示现在的值, \(k\) 就表示区间的长度。

在修改的时候,如果这些的区间长度不变,那么可以直接用 \(y = kx + b\) 来得到新的 \(b\)

现在的问题是\(lmax , rmax , max\)的区间长度我们不知道是否变化,以及要变化多少。

考虑对每一个结点维护一个阈值 \(x\) ,代表如果加的数 \(\geq x\)\(lmax , rmax , max\) 中至少有一个的长度会发生变化。

递归时如果这个点的区间都不会发生变化,那么直接打懒标记,否则暴力递归,知道区间长度不发生改变为止。

阈值 \(x\) 合并的时候根据继承的情况取个 \(\min\) 即可。

总复杂度为 \(O( ( n + m ) \log ^ 3 n + q \log n)\) ,其中 \(n\) 是序列长度, \(m\) 是修改次数, \(q\) 是查询次数。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4e5+100;
const ll inf=1e16;
int a[N],n,q;
struct func{
	int k;ll b;
	func operator+(const func &a)const{return {k+a.k,b+a.b};}
	void add(ll w){b+=k*w;}
};
pair<func,ll> max(func a,func b){
	if(a.k<b.k||(a.k==b.k&&a.b<b.b))swap(a,b);
	if(a.b>=b.b)return {a,inf};
	return {b,(b.b-a.b)/(a.k-b.k)};
}
struct tree{
	func lmax,rmax,tot,sum;ll x;
	tree operator+(const tree &a)const{
		tree res;
		pair<func,ll> tmp;
		res.x=min(x,a.x);
		tmp=max(lmax,sum+a.lmax);
		res.lmax=tmp.first,res.x=min(res.x,tmp.second);
		tmp=max(a.rmax,a.sum+rmax);
		res.rmax=tmp.first,res.x=min(res.x,tmp.second);
		tmp=max(tot,a.tot);
		res.x=min(res.x,tmp.second);
		tmp=max(tmp.first,rmax+a.lmax);
		res.tot=tmp.first,res.x=min(res.x,tmp.second);
		res.sum=sum+a.sum;
		return res;
	}
};
struct KTT{
#define ls rt*2
#define rs ls+1
#define mid (l+r)/2
#define lson ls,l,mid
#define rson rs,mid+1,r
	struct node{
		tree a;ll tag;
	}t[N*4];
	void pushup(int rt,int l,int r){
		t[rt].a=t[ls].a+t[rs].a;
	}
	void pushtag(int rt,int l,int r,ll v){
		t[rt].tag+=v;
		t[rt].a.x-=v;
		t[rt].a.lmax.add(v);
		t[rt].a.rmax.add(v);
		t[rt].a.tot.add(v);
		t[rt].a.sum.add(v);
	}
	void pushdown(int rt,int l,int r){
		if(t[rt].tag){
			pushtag(lson,t[rt].tag);
			pushtag(rson,t[rt].tag);
			t[rt].tag=0;
		}
	}
	void build(int rt,int l,int r){
		if(l==r){
			func q={1,a[l]};
			t[rt].a={q,q,q,q,inf};
			return;
		}
		build(lson);build(rson);
		pushup(rt,l,r);
	}
	void defeat(int rt,int l,int r,ll v){
		if(v>t[rt].a.x){
			ll tt=t[rt].tag+v;
			t[rt].tag=0;
			defeat(lson,tt);
			defeat(rson,tt);
			pushup(rt,l,r);
			return;
		}
		pushtag(rt,l,r,v);
	}
	void update(int rt,int l,int r,int L,int R,ll v){
		if(L<=l&&R>=r){
			defeat(rt,l,r,v);
			return;
		}
		pushdown(rt,l,r);
		if(L<=mid)update(lson,L,R,v);
		if(R>mid)update(rson,L,R,v);
		pushup(rt,l,r);
	}
	tree query(int rt,int l,int r,int L,int R){
		if(L<=l&&R>=r)return t[rt].a;
		pushdown(rt,l,r);
		if(L<=mid&&R>mid)return query(lson,L,R)+query(rson,L,R);
		if(L<=mid)return query(lson,L,R);
		return query(rson,L,R);
	}
}t;
int main(){
	scanf("%d %d",&n,&q);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	t.build(1,1,n);
	while(q--){
		int op,l,r,x;
		scanf("%d %d %d",&op,&l,&r);
		if(op==1){
			scanf("%d",&x);
			t.update(1,1,n,l,r,x);
		}else{
			printf("%lld\n",max(0ll,t.query(1,1,n,l,r).tot.b));
		}
	}
}
posted @ 2026-09-04 21:30  _lipeiran  阅读(15)  评论(0)    收藏  举报