线段树

线段树

普通线段树

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 4e5 + 5;

int n;
LL a[N];

#define ls u << 1
#define rs u << 1 | 1
#define mid (l + r >> 1)
#define segroot int u = 1, int l = 1, int r = n
#define lson ls, l, mid
#define rson rs, mid + 1, r

LL sum[N], tag[N], len[N];

void up(int u){ sum[u] = sum[ls] + sum[rs]; }

void bld(segroot){
	len[u] = r - l + 1;
	if(l == r) return sum[u] = a[l], void();
	bld(lson), bld(rson), up(u); 
}

void down(int u, LL x){ tag[u] += x, sum[u] += x * len[u]; }

void down(int u) { down(ls, tag[u]), down(rs, tag[u]), tag[u] = 0; }

void mdf(int ql, int qr, LL x, segroot){
	if(l > qr or r < ql) return ;
	if(l >= ql and r <= qr) return down(u, x);
	down(u), mdf(ql, qr, x, lson), mdf(ql, qr, x, rson), up(u);
}

LL qry(int ql, int qr, segroot){
	if(l > qr or r < ql) return 0;
	if(l >= ql and r <= qr) return sum[u];
	down(u); return qry(ql, qr, lson) + qry(ql, qr, rson);
}

signed main(){
	ios::sync_with_stdio(NULL); cin.tie(nullptr), cout.tie(nullptr);
	int q; cin>>n>>q; 
	for(int i = 1; i <= n; i ++) cin>>a[i];
	bld();
	while(q --){
		int op, x, y; LL v; cin>>op>>x>>y;
		if(op == 1) cin>>v, mdf(x, y, v);
		else cout<<qry(x, y)<<'\n';
	}
	return 0;
}
#include<bits/stdc++.h>

using namespace std;

#define int long long

const int N = 1e5 + 5;

int n, q, m;
int a[N]; 

namespace segtree{
	#define ls (u<<1)
	#define rs (u<<1|1)
	#define mid (l+r>>1)
	#define segroot int u=1,int l=1,int r=n
	#define lson ls,l,mid
	#define rson rs,mid+1,r
	struct { long long len,sum,add,mul; } t[N*4];
	void up(int u) { t[u].sum = (t[ls].sum + t[rs].sum) % m; }
	void bld(segroot) {
		t[u].mul = 1, t[u].len = r-l+1;
		if( l == r ) { cin>>t[u].sum; return; }
		bld(lson), bld(rson), up(u);
	}
	void add(int u,long long x) {
		t[u].sum = (t[u].sum + t[u].len * x) % m,
		t[u].add = (t[u].add + x) % m;
	}
	void mul(int u,long long x) {
		t[u].sum = t[u].sum * x % m,
		t[u].mul = t[u].mul * x % m,
		t[u].add = t[u].add * x % m;
	}
	void down(int u) {
		if( t[u].mul != 1 ) mul(ls,t[u].mul), mul(rs,t[u].mul), t[u].mul = 1;
		if( t[u].add ) add(ls,t[u].add), add(rs,t[u].add), t[u].add = 0;
	}
	long long qry(int ql,int qr,segroot) {
		if( qr < l || r < ql ) return 0;
		if( ql <= l && r <= qr ) return t[u].sum;
		down(u);
		return (qry(ql,qr,lson) + qry(ql,qr,rson)) % m;
	}
	void add(int ql,int qr,long long x,segroot) {
		if( qr < l || r < ql ) return;
		if( ql <= l && r <= qr ) return add(u,x);
		down(u), add(ql,qr,x,lson), add(ql,qr,x,rson), up(u);
	}
	void mul(int ql,int qr,long long x,segroot) {
		if( qr < l || r < ql ) return;
		if( ql <= l && r <= qr ) return mul(u,x);
		down(u), mul(ql,qr,x,lson), mul(ql,qr,x,rson), up(u);
	}
}

using namespace segtree;

signed main(){
	scanf("%lld%lld%lld", &n, &q, &m);
	bld();
	while(q --){
		int op, x, y;
		scanf("%lld%lld%lld", &op, &x, &y);
		if(op == 1){ 
			int k;
			scanf("%lld", &k);
			mul(x, y, k);
		} else if(op == 2){
			int k;
			scanf("%lld", &k);
			add(x, y, k);
		} else {
			printf("%lld\n", qry(x, y));
		}
	}
	return 0;
}	

动态开点线段树

我们发现,又是线段树的很多节点都是无用的,于是我们考虑动态开点,节点按需索取,这样就可以节省一定的空间,防止出现 MLE 的情况。

扫描线

扫描线,就是离线下来所有询问,从小到大扫描下标,并在进行线段树上修改,查询操作。

线段树合并 & 线段树分裂

首先,合并、分裂线段树所消耗的线段树一定很多,所以我们需要动态开点。

线段树合并

对于线段树的合并,我们直接将线段树上位于同一个位置的节点合并起来即可,简单。

int merge(int v, segroot){
	if(!u) return v;
	if(!v) return u;
	sum[u] += sum[v]; // 直接合并
	ls[u] = merge(ls[v], ls[u]); // 合并左子树
	rs[u] = merge(rs[v], rs[u]); // 合并右子树
	return u;
}

线段树分裂

一种类似于二分的方式:

我们考虑将线段树 \([1, n]\) 分成 \([1, x]\)\([x + 1, n]\)

加入我们现在在节点 \((u, [l, r])\),我们判断 \(mid\)\(x\) 的关系。

\(x \le mid\) 则我们要分裂左子树,并将左子树分裂后的右子树作为 \(u\) 的左子树。

\(x > mid\) 则我们要分裂右子树,并将右子树分裂后的左子树作为 \(u\) 的右子树。

我们发现,如果直接连上,线段树的层数会紊乱,所以我们要补上新节点,维护层数恒定。

pair<int, int> split(int x, segroot){
	if(l == r) return {u, 0}; 
	if(mid >= x){
		auto tmp = split(x, lson); // 分离
		ls[u] = tmp.second; 
		int nd = ++ idx;
		ls[nd] = tmp.first; up(nd); // 补上新节点
		up(u);
		return {nd, u}; 
	} 
	auto tmp = split(x, rson);
	rs[u] = tmp.second;
	int nd = ++ idx;
	rs[nd] = tmp.first;
	ls[nd] = ls[u], ls[u] = 0; 
	up(nd);
	up(u);
	return {nd, u}; 
}

最终成品代码:

struct segtree{
	LL sum[N * 18];
	int ls[N * 18], rs[N * 18];
	int idx;

	#define mid (l + r >> 1)
	#define segroot int &u, int l = 1, int r = n
	#define lson ls[u], l, mid
	#define rson rs[u], mid + 1, r

	void up(int u) { sum[u] = sum[ls[u]] + sum[rs[u]]; }

	void add(int x, int v, segroot){
		if(l > x || r < x) return ;
		if(!u) u = ++ idx;
		if(l == r) return sum[u] += v, void();
		add(x, v, lson), add(x, v, rson), up(u);
	}

	LL qry(int ql, int qr, segroot){
		if(!u || l > qr || r < ql) return 0;
		if(l >= ql && r <= qr) return sum[u];
		return qry(ql, qr, lson) + qry(ql, qr, rson);
	}

	int find(int k, segroot){
		if(!u || sum[u] < k) return -1;
		if(l == r) return l;
		if(sum[ls[u]] >= k) return find(k, lson);
		else return find(k - sum[ls[u]], rson);
	}

	int merge(int v, segroot){
		if(!u) return v;
		if(!v) return u;
		sum[u] += sum[v];
		ls[u] = merge(ls[v], ls[u]);
		rs[u] = merge(rs[v], rs[u]);
		return u;
	}

	pair<int, int> split(int x, segroot){
		if(l == r) return {u, 0}; 
		if(mid >= x){
			auto tmp = split(x, lson);
			ls[u] = tmp.second;
			int nd = ++ idx;
			ls[nd] = tmp.first; up(nd);
			up(u);
			return {nd, u}; 
		} 
		auto tmp = split(x, rson);
		rs[u] = tmp.second;
		int nd = ++ idx;
		rs[nd] = tmp.first;
		ls[nd] = ls[u], ls[u] = 0; 
		up(nd);
		up(u);
		return {nd, u}; 
	}
};

线段树分治

实际上应该叫时间分治。

给定一个 \(n\) 的点的图,在第\([l_i, r_i]\) 的时间内有边 \([u_i, v_i]\),询问每个时刻内这个图是否是二分图(或者有其他情况)。

我们这里与二分图有关的不在阐述,只说什么是线段树分治。

我们发现,加边好说,但减边不好做,因为我们无法在一个二分图上快速地判断是否合法。

于是,我们对于时间轴建一颗线段树。

对于每一时间内的每一条边,我们将其加到线段树上,线段树上将会正好将其分成 \(\log k\) 块,\(k = r - l + 1\)

接下来,我们递归线段树即可求解。

总时间复杂度为 \(\mathcal O(n \log n)\)

具体代码:

#include<bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;

int n, m, k;
int fa[N << 1], sz[N << 1];

#define ls u << 1
#define rs u << 1 | 1
#define mid (l + r >> 1)
#define segroot int u = 1, int l = 1, int r = k
#define lson ls, l, mid
#define rson rs, mid + 1, r

int find(int x){ return x == fa[x] ? x : find(fa[x]); }

vector<pair<int, int>> tr[N << 2];

void add(int ql, int qr, int x, int y, segroot){ // 加边 [ql, qr] x -> y
	if(l > qr || r < ql) return ;
	if(l >= ql && r <= qr) return tr[u].push_back({x, y});
	add(ql, qr, x, y, lson), add(ql, qr, x, y, rson);
} 

int cnt = 0;

void dfs(segroot){
	int k = 0;
	stack<array<int, 5>> stk;
	for(auto it : tr[u]){
		int x = it.first, y = it.second;
		int uu = find(x), vv = find(y);
		if(fa[uu] == fa[vv]) { cnt ++, k ++; continue; }
		int un = find(x + n), vn = find(y + n); // 这里我们使用扩展域并查集维护二分图
	    if(sz[uu] < sz[vn]){
	    	stk.push({uu, fa[uu], sz[uu], vn, sz[vn]});
			fa[uu] = vn; sz[vn] += sz[uu];
		} else {
			stk.push({vn, fa[vn], sz[vn], uu, sz[uu]});
			fa[vn] = uu; sz[uu] += sz[vn];
		}
	    if(sz[un] < sz[vv]){
	    	stk.push({un, fa[un], sz[un], vv, sz[vv]});
			fa[un] = vv; sz[vv] += sz[un];
		} else {
			stk.push({vv, fa[vv], sz[vv], un, sz[un]});
			fa[vv] = un; sz[un] += sz[vv];
		}
	} // 加边
	if(l == r){
		puts(cnt == 0 ? "Yes" : "No");
		while(!stk.empty()) { auto tp = stk.top(); fa[tp[0]] = stk.top()[1]; sz[tp[0]] = tp[2]; sz[tp[3]] = tp[4]; stk.pop();}
		cnt -= k;
		return ;
	}
	dfs(lson);
	dfs(rson);
	while(!stk.empty()) { auto tp = stk.top(); fa[tp[0]] = stk.top()[1]; sz[tp[0]] = tp[2]; sz[tp[3]] = tp[4]; stk.pop();} // 删边
	cnt -= k;
}

int main(){
	ios::sync_with_stdio(NULL); cin.tie(nullptr), cout.tie(nullptr); 
	cin>>n>>m>>k;
	for(int i = 1; i <= n; i ++) fa[i] = i, fa[i + n] = i + n, sz[i] = sz[i + n] = 1;
	for(int i = 1, x, y, l, r; i <= m; i ++){
		cin>>x>>y>>l>>r; 
		l ++;
		add(l, r, x, y);
	} 
	dfs(1);
	return 0;
} 

吉司机线段树

给你一个序列,支持一下操作:

  • 区间加
  • 区间取 \(\min\)
  • 求区间和
  • 求区间最大值
  • 求区间历史最大值

思路

发现:这里要求的是区间历史最大值,如果仅用普通的线段树维护,必然是无法做到的,于是就有了吉司机线段树 SBT

我们先考虑怎么维护历史最大值。

一个显然的想法是,我们可以再开一个数组 hmx[N << 2] 维护区间历史最大值,每次更新的时候都用当前的 \(\max\) 更新 hmx

但是我们会发现,线段树保持时间复杂度的秘诀在于懒标记,但是有时,我们的懒标记还没来得及下传,就会被覆盖修改,这就造成了子节的 hmx 并没有被这个点历史最大值更新。

于是我们可以考虑再开一个 tag2 记录区间历史最大懒标记,用来下传更新子节点的历史最大值。


但是我们发现,这样的做法并不能很好地实现区间取 \(\min\) 操作。

于是我们考虑将区间取 \(min\) 转化为区间加。

设当前区间 \([l, r]\) 将要对 \(v\)\(\min\),这就相当于,我们将区间内所有 \(> v\) 的数都变成 \(v\)。形式化地,\(\forall x \in [l,r]: a_x += v - a_x\)

那么我们接下来还是无法实现,原因在于,我们无法有效率地找到我们的 \(x > v\)

下面有一个神奇做法:

我们将最大值同非最大值划分出来

我们不仅维护最大值 max 同时维护次大值 second_max(下文统一写作 se)。对 \(v\) 进行分类讨论:

  1. \(v \ge mx\):所有数均不超过 \(v\),则该区间无需修改。
  2. \(se < v < mx\):只需将最大值修改为 \(v\),其余不变,需要打标记。
  3. \(v \le se\):不止最大值更新,此时则递归到子节点。

这种方式,我们就可以完成区间取 \(\min\) 的工作,总时间复杂度为 \(\mathcal O(n \log^2n)\)

显然,这颗线段树的时间复杂度分析并不显然,下面势能分析:

时间复杂度势能分析

我们定义一个节点有区分度当且仅当 \(mx > se\),即有严格的最大值和次大值之分,则我们认为这个节点携带了 $1 $ 点势能。

整棵树的势能 \(\Phi = 所有 有区分度 的节点总数\)

势能分析:

只考虑区间取 \(\min\)(仅此时间复杂度不同于普通线段树),分三种情况:

  • \(mx \le v\):没有修改,势能不变。
  • \(se < v < mx\):只有最大值降低到 \(v\),是能仍然不变。
  • \(se \le v\):此时会有可能递归下去,使得 \(se = mx\),此时势能 \(\Phi\) 减少 \(1\)

势能增加发生在下传里,每次下传有几率使无区分变成有区分,使 \(\Phi + 1\)

初始状态下,\(\Phi \le n \log n\)

势能增加最多每次 down \(\mathcal O(\log n)\) 层,每层时间 \(O(1)\),故总增加量为 \(\mathcal O(q \log n)\)

因此:

总递归深度 \(\le\) 初始势能 \(+\) 增加的势能 \(+\) 普通线段树

总时间复杂度 \(\approx \mathcal O((n + q)\log^2 n)\)

代码实现

我们需要维护的东西:

int len[N << 2], cnt[N << 2]; // 区间长度、最大值数量(便于批次修改)
LL sum[N << 2], mx[N << 2], se[N << 2], hmx[N << 2];	// 和、max、second_max、历史 max
LL atag[N << 2], btag[N << 2], hatag[N << 2], hbtag[N << 2]; // 最大值标记、非最大值标记、历史最大值标记、非历史最大值标记

上传合并:

inline void up(int u){ 
	sum[u] = sum[ls] + sum[rs], mx[u] = max(mx[ls], mx[rs]), hmx[u] = max(hmx[ls], hmx[rs]); // 普通合并
	if(mx[ls] == mx[rs]) cnt[u] = cnt[ls] + cnt[rs], se[u] = max(se[ls], se[rs]);
	else if(mx[ls] > mx[rs]) cnt[u] = cnt[ls], se[u] = max(se[ls], mx[rs]);
	else cnt[u] = cnt[rs], se[u] = max(mx[ls], se[rs]); // 针对次大值的合并
}

建树:

inline void bld(segroot){
	len[u] = r - l + 1;
	if(l == r) return cin>>sum[u], mx[u] = hmx[u] = sum[u], cnt[u] = 1, se[u] = -inf, void();
	bld(lson), bld(rson), up(u);
}

\(\boxed{\textbf{标记下传}}\)

inline void down(int u, LL a, LL b, LL ha, LL hb){
	sum[u] += 1ll * a * cnt[u] + 1ll * b * (len[u] - cnt[u]); // 分别更新最大次大值和
	hmx[u] = max(hmx[u], mx[u] + ha); // 历史最大值
	mx[u] += a; // 当前最大值
	if(se[u] != -inf) se[u] += b; // 次大值
	hatag[u] = max(hatag[u], atag[u] + ha), hbtag[u] = max(hbtag[u], btag[u] + hb);
	atag[u] += a, btag[u] += b; // 更新标记
}

inline void down(int u){
	int mxu = max(mx[ls], mx[rs]);
	if(mx[ls] == mxu) down(ls, atag[u], btag[u], hatag[u], hbtag[u]); // 下传最大值
	else down(ls, btag[u], btag[u], hbtag[u], hbtag[u]); // 下传次大值
	if(mx[rs] == mxu) down(rs, atag[u], btag[u], hatag[u], hbtag[u]); // 下传最大值
	else down(rs, btag[u], btag[u], hbtag[u], hbtag[u]); // 下传次大值
	atag[u] = btag[u] = hatag[u] = hbtag[u] = 0; // 清空标记
}

区间加

inline void add(int ql, int qr, int x, segroot){
	if(qr < l || ql > r) return ;
	if(l >= ql && r <= qr) return down(u, x, x, x, x);
	down(u), add(ql, qr, x, lson), add(ql, qr, x, rson), up(u);
}

\(\boxed{\textbf{区间取最小值}}\):

inline void mdf(int ql, int qr, int x, segroot){
	if(qr < l || ql > r || mx[u] <= x) return ; // mx[u] <= x 没有影响,无需修改
	if(l >= ql && r <= qr && se[u] < x) return down(u, x - mx[u], 0, x - mx[u], 0); // 只对最大值有影响
	down(u), mdf(ql, qr, x, lson), mdf(ql, qr, x, rson), up(u);
}

最后的板子:

struct STB{
	#define ls (u << 1)
	#define rs (u << 1 | 1)
	#define mid (l + r >> 1)
	#define segroot int u = 1, int l = 1, int r = n
	#define lson ls, l, mid
	#define rson rs, mid + 1, r

	int len[N << 2], cnt[N << 2];
	LL sum[N << 2], mx[N << 2], se[N << 2], hmx[N << 2];
	LL atag[N << 2], btag[N << 2], hatag[N << 2], hbtag[N << 2];

	inline void up(int u){ 
		sum[u] = sum[ls] + sum[rs], mx[u] = max(mx[ls], mx[rs]), hmx[u] = max(hmx[ls], hmx[rs]);
		if(mx[ls] == mx[rs]) cnt[u] = cnt[ls] + cnt[rs], se[u] = max(se[ls], se[rs]);
		else if(mx[ls] > mx[rs]) cnt[u] = cnt[ls], se[u] = max(se[ls], mx[rs]);
		else cnt[u] = cnt[rs], se[u] = max(mx[ls], se[rs]);
	}

	inline void bld(segroot){
		len[u] = r - l + 1;
		if(l == r) return cin>>sum[u], mx[u] = hmx[u] = sum[u], cnt[u] = 1, se[u] = -inf, void();
		bld(lson), bld(rson), up(u);
	}

	inline void down(int u, LL a, LL b, LL ha, LL hb){
		sum[u] += 1ll * a * cnt[u] + 1ll * b * (len[u] - cnt[u]);
		hmx[u] = max(hmx[u], mx[u] + ha);
		mx[u] += a;
		if(se[u] != -inf) se[u] += b;
		hatag[u] = max(hatag[u], atag[u] + ha), hbtag[u] = max(hbtag[u], btag[u] + hb);
		atag[u] += a, btag[u] += b;
	}

	inline void down(int u){
		int mxu = max(mx[ls], mx[rs]);
		if(mx[ls] == mxu) down(ls, atag[u], btag[u], hatag[u], hbtag[u]);
		else down(ls, btag[u], btag[u], hbtag[u], hbtag[u]);
		if(mx[rs] == mxu) down(rs, atag[u], btag[u], hatag[u], hbtag[u]);
		else down(rs, btag[u], btag[u], hbtag[u], hbtag[u]);
		atag[u] = btag[u] = hatag[u] = hbtag[u] = 0;
	}

	inline void add(int ql, int qr, int x, segroot){
		if(qr < l || ql > r) return ;
		if(l >= ql && r <= qr) return down(u, x, x, x, x);
		down(u), add(ql, qr, x, lson), add(ql, qr, x, rson), up(u);
	}

	inline void mdf(int ql, int qr, int x, segroot){
		if(qr < l || ql > r || mx[u] <= x) return ;
		if(l >= ql && r <= qr && se[u] < x) return down(u, x - mx[u], 0, x - mx[u], 0);
		down(u), mdf(ql, qr, x, lson), mdf(ql, qr, x, rson), up(u);
	}

	inline LL qrysum(int ql, int qr, segroot){
		if(qr < l || ql > r) return 0;
		if(l >= ql && r <= qr) return sum[u];
		down(u); return qrysum(ql, qr, lson) + qrysum(ql, qr, rson);
	}

	inline LL qrymx(int ql, int qr, segroot){
		if(qr < l || ql > r) return -inf;
		if(l >= ql && r <= qr) return mx[u];
		down(u); return max(qrymx(ql, qr, lson), qrymx(ql, qr, rson));
	}

	inline LL qryh(int ql, int qr, segroot){
		if(qr < l || ql > r) return -inf;
		if(l >= ql && r <= qr) return hmx[u];
		down(u); return max(qryh(ql, qr, lson), qryh(ql, qr, rson));
	}
} T;
posted @ 2026-07-22 11:06  Hty111  阅读(1)  评论(0)    收藏  举报