势能线段树

势能线段树包括了吉司机线段树。思想就是正常线段树可能遇到难 pushup 的情况,我们直接暴力递归,然后根据势能分析说明这个暴力复杂度是均摊可接受的。

【势能线段树】

【例一】

CF438D The Child and Sequence

题意:

支持三种操作。

  1. 单点修改。

  2. 区间对给定数取模。

  3. 区间求和。

n,q105,时限 4s.


注意到一个事实:当 xpxmodpx/2。也就是如果不计修改,每个数有效取模次数只有 O(logV) 次。

对线段树每个结点记录区间最大值和区间和。单点修改很好做。区间取模时,如果当前结点最大值 <mod,就没必要修改可以直接返回了;如果最大值 mod,暴力递归左右儿子取模,然后 pushup 回来。


复杂度分析。在势能线段树的复杂度分析时,我们只需要说明 "暴力递归" 的复杂度不会太高即可。因为其他部分和普通线段树是严格 O(logn) 的。

对于结点 u:[l,r],定义其势能 ϕ(u)=i=lr(logai)

  1. 当暴力递归时,说明 u 代表的区间内有至少一个数会折半,也就是 logai 会减少 1

    所有结点的势能不增。同时所有包含被取模元素的区间势能都会减少。

  2. 单点修改最多只会让一条链上所有线段树结点的势能增加 logV,也就是总共增加 lognlogV。所以最多增加 O(qlognlogV)

  3. 初始势能是 O(nlogVlogn) 的。因为一层势能是 O(nlogV) 的,一共 logn 层。

综上所述,每次递归总势能减少 1,初始势能 O(nlogVlogn),单点修改最多增加 O(qlogVlogn),因此总复杂度 O((n+q)logVlogn)

typedef pair<long long, long long> pii;

pii operator+(pii a, pii b) {
	return make_pair(a.first + b.first, max(a.second, b.second));
}
pii operator%(pii a, long long b) {
	return make_pair(a.first % b, a.second % b);
}

struct SegmentTree {
	long long sz;
	vector<pair<long long, long long> > val;
	pii ide1 = make_pair(0ll, 0ll);
	
	void pushup(long long x) {
		val[x] = val[x * 2] + val[x * 2 + 1];
	}
	void build(long long x, long long lx, long long rx, vector<long long> &a) {
		if (lx + 1 == rx) {
			val[x] = (lx <= a.size() ? make_pair(a[lx - 1], a[lx - 1]) : ide1);
			return ;
		}
		long long m = (lx + rx) / 2;
		build(x * 2, lx, m, a);
		build(x * 2 + 1, m, rx, a);
		pushup(x);
	}
	SegmentTree(){}
	SegmentTree(vector<long long> &a) {
		for (sz = 1; sz < a.size(); sz *= 2);
		val.assign(2 * sz, ide1);
		build(1, 1, sz + 1, a);
	}
	void mdf_mod(long long x, long long lx, long long rx, long long l, long long r, long long p) {
//		cout << x << ' ' << lx << ' ' << rx << ' ' << l << ' ' << r << ' ' << p << endl;
		if (val[x].second < p)
			return ;
		if (lx + 1 == rx) {
//			cout << '!';
			val[x] = val[x] % p;
//			cout << val[x].first << ' ' << val[x].second << endl;
			return ;
		}
		long long m = (lx + rx) / 2;
		if (l < m)
			mdf_mod(x * 2, lx, m, l, r, p);
		if (r > m)
			mdf_mod(x * 2 + 1, m, rx, l, r, p);
		pushup(x);
	}
	void mdf(long long x, long long lx, long long rx, long long p, long long v) {
		if (lx + 1 == rx) {
			val[x] = make_pair(v, v);
			return ;
		}
		long long m = (lx + rx) / 2;
		if (p < m)
			mdf(x * 2, lx, m, p, v);
		else
			mdf(x * 2 + 1, m, rx, p, v);
		pushup(x);
	}
	pii qry(long long x, long long lx, long long rx, long long l, long long r) {
		if (l <= lx && rx <= r)
			return val[x];
		if (l >= rx || lx >= r)
			return ide1;
		long long m = (lx + rx) / 2;
		return qry(x * 2, lx, m, l, r) + qry(x * 2 + 1, m, rx, l, r); 
	}
} st;

【例二】

gym103107A And RMQ

题意:

支持三种操作。

  1. 区间求最大值。

  2. 单点修改。

  3. 区间对给定数按位与。

n,q4×105,时限 3s.


势能线段树最重要的就是想明白什么情况可以剪枝

显然,假设我们要对区间与上 v,如果 v0 的位已经全都是 0 了,可以剪枝。换句话说,设区间的或等于 x,如果 (x | v) == v 就可以剪枝。

对结点记录区间 max 和区间按位或的值。前两个正常做,在区间按位与时,如果 (valor[x] | v) == v 就退出。


复杂度分析。

一个结点的势能定义为 i=lrpopcount(ai),显然总势能 O(nlognlogV),单点修改至多增加 O(lognlogV)

同时每次暴力递归总势能至少 1,按位与说明 1 的个数不增,也就是任何点势能不会增加。总共 O((n+q)lognlogV)

int n, m;
int a[N];

int mx[N << 2], valor[N << 2];

void pushup(int x) {
	mx[x] = max(mx[x * 2], mx[x * 2 + 1]);
	valor[x] = valor[x * 2] | valor[x * 2 + 1];
}
void mdf(int x, int lx, int rx, int l, int r, int v) {
	if ((v | valor[x]) == v)
		return ;
	if (l >= rx || lx >= r)
		return ;
	if (lx + 1 == rx) {
		valor[x] &= v;
		mx[x] = valor[x];
		return ;
	}
	int m = (lx + rx) / 2;
	mdf(x * 2, lx, m, l, r, v);
	mdf(x * 2 + 1, m, rx, l, r, v);
	pushup(x);
}
void mdf(int x, int lx, int rx, int p, int v) {
	if (lx + 1 == rx) {
		mx[x] = valor[x] = v;
		return ;
	}
	int m = (lx + rx) / 2;
	if (p < m)
		mdf(x * 2, lx, m, p, v);
	else
		mdf(x * 2 + 1, m, rx, p, v);
	pushup(x);
}
int qry(int x, int lx, int rx, int l, int r) {
	if (l <= lx && rx <= r)
		return mx[x];
	if (l >= rx || lx >= r)
		return -1;
	int m = (lx + rx) / 2;
	return max(qry(x * 2, lx, m, l, r), qry(x * 2 + 1, m, rx, l, r));
}
void build(int x, int lx, int rx) {
	if (lx + 1 == rx) {
		mx[x] = valor[x] = a[lx];
		return ;
	}
	int m = (lx + rx) / 2;
	build(x * 2, lx, m);
	build(x * 2 + 1, m, rx);
	pushup(x);
}

【例三】

CF444C DZY Loves Colors

思路:当整个区间的颜色都相同时,可以直接打懒标记区间加。

对结点 u 保存:

  1. clr,若 clr=0 表示区间颜色个数 2;否则表示区间颜色都是 clr

  2. tag,区间加的懒标记。我们认为 tag 已经对 sum 作用了,也就是只作用真子孙。

  3. sum,区间权值和。


复杂度分析。区别在于有区间修改,需要分析会不会有势能增加

定义结点 u 势能为 ϕ(u)=[clru=0]。显然因为线段树结点 O(n) 个,初始势能是 O(n) 的。接下来考虑暴力递归对势能的影响。

每次暴力递归,说明要修改的区间完全包含 u,同时 u 内颜色个数 2。考虑所有结点的势能变化:

  1. u 肯定减少,从 2 种颜色变成赋值的颜色。

  2. u 的子孙们势能不增。因为全部赋值成 1 种颜色了。

  3. u 的祖先、子孙的结点势能不变,因为在线段树修改过程中都不会访问到它们。

  4. 剩下是 u 的真祖先。考虑所有 u 的祖先们,即线段树在正常拆区间时访问到的结点。

    如果粗略来看,每个被访问的结点都可能势能 +1,线段树每一层有 2 个区间被访问,一共正常访问 O(logn) 个结点,这么说一次操作增加 O(log2n) 的势能,总复杂度是 O(mlog3n) 吗?

    其实并不是,因为 u 内的颜色个数都 2 了,所以 u 的祖先颜色个数也 2,所以祖先的势能本就是 1,不可能增加了,反而可能减少。

综上所述,暴力递归势能必定减少。而势能初值为 O(n) 的,所以复杂度是 O((n+m)logn)

【例四】

冒险

题意:

支持三个操作。

  1. 区间按位与。

  2. 区间按位或。

  3. 求区间最大值。

n,q2×105ai<220


这题麻烦一些,有两个剪枝:

  1. 类似例一,对结点记录 valor, valand,如果发现修改没用就直接返回。

  2. 如果修改对于整个区间的作用是一样的,用懒标记修改。

第一条很好理解,第二条是什么意思呢?

例如我们要与上 11000011,如果 u 中所有数中间四位都相同(全都是 ??ABCD?? 的形式),相当于区间整体减 ABCD00。或是同理的。

所以我们对结点额外记录 tag:区间整体加上 tag 的懒标记。在我们发现第二条剪枝生效的时候,比如我们要对区间整体 +v,我们就让 tag, valor, valand+v。注意 valor, valand 在这里也是可以加减的,因为我们要么是让一些 1 整体变 0,要么是让一些 0 整体变 1,没有进位退位的情况。显然是可以直接放到 valor, valand 身上的。

具体判定:valor ^ valand0 的位是整个区间都一样的。


复杂度分析。这题涉及到势能增加

valor ^ valand1 的位简称为 "有分歧的"。对结点 u 定义其势能 ϕ(u)u 区间有分歧的位数。初始总势能是 O(nlogV) 的。

首先强行递归肯定会使得 ϕ(u) 至少减一。然后对于每个正常访问到的结点,势能最多增加 O(logV),所以一次操作势能增加量为 O(lognlogV)。总增加 O(qlognlogV)

因此总复杂度是 O(qlognlogV) 的。

void tagadd(int u, int v){
	tagAnd[u] += v; tagOr[u] += v; mx[u] += v; lazy[u] += v;
}
	
void pushUp(int i){
    tagAnd[i] = tagAnd[lc] & tagAnd[rc];
    tagOr[i] = tagOr[lc] | tagOr[rc];
    mx[i] = max(mx[lc], mx[rc]);
}

void pushDown(int i){
	tagadd(lc, lazy[i]);
	tagadd(rc, lazy[i]);
	lazy[i] = 0;
}

void rangeAnd(int i,int L,int R,int l,int r,int x){
    if ((tagOr[i] & x) == tagOr[i]) return;
    int check = tagAnd[i]^tagOr[i]; // check 表示相等的位置。 
    if (l <= L && r >= R && ((check | x) == x))  // x中为0的地方,check也为0. 
		tagadd(i, (tagAnd[i]&x)-tagAnd[i]);  
	else{    
	    int mid=(L+R)>>1;
    	pushDown(i);
    	if(l<=mid) rangeAnd(lc,L,mid,l,r,x);
    	if(r>mid) rangeAnd(rc,mid+1,R,l,r,x);
    	pushUp(i);
    }
}

void rangeOr(int i,int L,int R,int l,int r,int x){
    if ((tagAnd[i] | x) == tagAnd[i]) return;
    int check = tagAnd[i]^tagOr[i]; // check 表示相等的位置。 
    if (l <= L && r >= R && ((check & x) == 0))  // x中为1的地方,check也为0. 
		tagadd(i, (tagAnd[i]|x)-tagAnd[i]); 
	else{    
	    int mid=(L+R)>>1;
    	pushDown(i);
    	if(l<=mid) rangeOr(lc,L,mid,l,r,x);
    	if(r>mid) rangeOr(rc,mid+1,R,l,r,x);
    	pushUp(i);
    }
}

int queryMax(int i,int L,int R,int l,int r){
    if(l<=L&&r>=R) return mx[i];
    int mid=(L+R)>>1,res=-1;
    pushDown(i);
    if(l<=mid) res=max(res,queryMax(lc,L,mid,l,r));
    if(r>mid) res=max(res,queryMax(rc,mid+1,R,l,r));
    return res;
}

void build(int i,int L,int R){
    if(L==R){
    	int a; scanf("%d", &a);
    	mx[i] = tagAnd[i] = tagOr[i] = a; lazy[i]=0;  
    }
    else{
    	int mid=(L+R)>>1;
    	build(lc,L,mid); build(rc,mid+1,R); 
		pushUp(i);
    }
}

【例五】

UOJ228 Rikka with sequence

支持三个操作。

  1. 区间求和。

  2. 区间加。

  3. 区间开方下取整。

n,m,ai105


考虑怎么剪枝。记录区间 mnmx

  1. mn=mx,等价于区间减法。

  2. mn+1=mx,则要么等价于区间减法,要么等价于区间赋值。

对于 mxmn>1 就暴力递归。


复杂度分析。这题的势能分析采用对数的形式,是个势能分析的 trick

这里的定义比较特别。记 ϕ(u)=log(mxmn)(若 mxmnϕ(u)=0)。

  1. 分析区间开方。

    如果暴力递归了,说明 mxmn2。我们在这个条件下求证 u 势能至少 1

    [mn]=C,[mx]=C+Δ。转证 mxmn2Δ

    mxmn2Δ(C+Δ)2((C+1)21)2Δ=2CΔ+Δ22C2Δ=Δ(2C+Δ)2(C+Δ)

    Δ2 时,显然成立。而当 Δ=0/1 时,ϕ(u)=0 显然变小了。

posted @   FLY_lai  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
历史上的今天:
2024-02-20 数学题目合集
点击右上角即可分享
微信分享提示