线段树

线段树

普通线段树

#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;
}	

楼房重建

P4198 楼房重建 - 洛谷

我们发现,若 \(i\) 不会被 \(j\) 遮挡,必然会有,\(\frac{y_i}{i} \ge \frac{y_j}{j}\),所以我们不妨设 \(b_i = \frac{y_i}{i}\),问题就转化成了维护 \(b_i\) 构成的单调栈,即有多少个位置是当前的前缀最大值。

我们考虑线段树的 \(\text{pushup}\) 怎么写。

对于节点 \(u\),他的左右节点分别为 \(ls,rs\)

我们维护 \(u\) 中单调栈长度和最大值,对于 \(ls\) 单调栈内的所有数必然在 \(u\) 的单调栈内,且其末尾一定是 \(\text{mx[ls]}\),对于右儿子,如果其最大值小于左儿子,贡献为 \(0\)。否则,对其继续递归二分,看它左儿子的最大值是否大于 \(u\) 的左儿子最大值,大于,那左儿子保留,递归右侧;否则,递归左侧。

使用二分计算 \(\text{pushup}\) 函数,实乃高明。

最后时间复杂度:\(\mathcal O(n\log^2n)\)

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;

int n, m;

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 {
    int len, cnt;
    double mx;
} tr[N << 2];

int cal(int u, double x) {
    int res = 0;
    if (tr[u].mx <= x)
        return 0;
    if (tr[u].len == 1)
        return tr[u].mx > x;
    if (tr[ls].mx > x) {
        res = tr[u].cnt - tr[ls].cnt + cal(ls, x);
    } else
        res = cal(rs, x);
    return res;
}

void up(int u) {
    tr[u].mx = max(tr[ls].mx, tr[rs].mx);
    int res = cal(rs, tr[ls].mx);
    tr[u].cnt = tr[ls].cnt + res;
}

void build(segroot) {
    tr[u].len = r - l + 1;
    if (l == r)
        return;
    build(lson), build(rson), up(u);
}

void mdf(int x, double v, segroot) {
    if (l > x || r < x)
        return;
    if (l == r) {
        tr[u].mx = v;
        tr[u].cnt = 1;
        return;
    }
    mdf(x, v, lson), mdf(x, v, rson), up(u);
}
}  // namespace segtree

using namespace segtree;

int main() {
    scanf("%d%d", &n, &m);
    build();
    while (m--) {
        int x, y;
        scanf("%d%d", &x, &y);
        mdf(x, y * 1.00 / x);
        printf("%d\n", tr[1].cnt);
    }
    return 0;
}

动态开点线段树

我们发现,又是线段树的很多节点都是无用的,于是我们考虑动态开点,节点按需索取,这样就可以节省一定的空间,防止出现 MLE 的情况。每个节点 \(u\),记录 \(ls[u]\)\(rs[u]\)(或者也可以是 \(son[0], son[1]\) 为了和 trietreap 对比),然后其他的就一样了。

使用动态开点线段树我们就可以做到维护一个 \(1 \sim 1e9\) 大小的序列,只开了我们需要用的节点。

int rt[N];

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);
    }
};

for(int i = 1, x; i <= n; i ++) {
	cin>>x;
	if(x) add(i, x, rt[1]);
}

线段树二分

对象

  1. 数组本身具有单调性。
  2. 询问区间内第一个 \(\ge k\) 的位置。

做法

我们先看左子节点是否满足要求,满足则向左走,不满足向右走,最终 \(l = r\) 时,就返回答案即可。

扫描线

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

线段树合并 & 线段树分裂

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

线段树合并

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

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}; 
	}
};

线段树维护凸包

静态凸包

P4758 Mountainous landscape - 洛谷

我们考虑维护一个区间内部上凸包,判断这条线段是否能交到上凸包上。

我们对于一段区间,开一个 set 或平衡树维护凸包上的点。

考虑如何合并两个儿子的信息。

就相当于我们找到两个凸包上的两个点连起来形成一个凸包。

我们考虑在平衡树上二分切线的斜率,找到公切线即可找到两个需要连上的点。

建线段树时我们直接用上述过程暴力 \(\text{pushup}\)

接下来对于一个查询,我们将其拆分成线段树上的整区间,判断是否与这些区间的前缀凸包有交。

前缀凸包的维护直接用二分斜率合并即可。

时间复杂度:\(\mathcal O(n \log^2n)\) 对于手写平衡树(或 vector), \(\mathcal O(n \log^3n)\) 对于 set

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

void _main();
int main(){
	int T; scanf("%d", &T); 
	while(T --) _main();
	return 0;
}

const int N = 1e5 + 5;
int n;
pair<int, int> p[N];
pair<int, int> s, t;

namespace segtree{
	#define ls (u << 1)
	#define rs (u << 1 | 1)

	vector<pair<int, int>> v[N << 2];
	pair<int, int> stk[N];
	int top;

	inline pair<int, int> sub(pair<int, int> a, pair<int, int> b) { 
		return {a.first - b.first, a.second - b.second};  
	} 

	inline LL judge(pair<int, int> x, pair<int, int> y) { 
		return 1ll * x.first * y.second - 1ll * x.second * y.first; 
	}

	void insert(pair<int, int> x){
		while(top > 1 && judge(sub(stk[top], stk[top - 1]), sub(x, stk[top - 1])) >= 0) 
			top --;
		stk[++ top] = x;
	}

	void up(int u){
		top = 0;
		int l = 0, r = 0;
		while(l < v[ls].size() && r < v[rs].size()){
			if(v[ls][l].first < v[rs][r].first) insert(v[ls][l ++]);
			else insert(v[rs][r ++]);
		}
		while(l < v[ls].size()) insert(v[ls][l++]);
		while(r < v[rs].size()) insert(v[rs][r++]);

		v[u].clear();
		for(int i = 1; i <= top; i ++) v[u].emplace_back(stk[i]);
	}

	void bld(int u, int l, int r){
		v[u].clear();
		if(l == r){
			v[u].emplace_back(p[l]);
			return;
		}
		int mid = (l + r) / 2;
		bld(ls, l, mid);
		bld(rs, mid+1, r);
		up(u);
	}

	bool check(int u){
		auto &vec = v[u];
		int l = 0, r = (int)vec.size() - 1;
		int ans = 0;
		while(l <= r){
			int m = (l + r) / 2;
			if(m == 0){
				ans = m;
				l = m + 1;
				continue;
			}
			LL val1 = judge(sub(t, s), sub(vec[m-1], s));
			LL val2 = judge(sub(t, s), sub(vec[m], s));
			if(val1 < val2){
				ans = m;
				l = m + 1;
			}else {
				r = m - 1;
			}
		}
		return judge(sub(t, s), sub(vec[ans], s)) > 0ll;
	}

	int qry(int x, int u, int l, int r){
		if(x <= l) {
			if(!check(u)) return 0;
			if(l == r) return l - 1;
		}
		int mid = (l + r) / 2;
		if(x <= mid) {
			int res = qry(x, ls, l, mid); 
			if(res != 0) return res;
		}
		return qry(x, rs, mid+1, r);
	}
}
using namespace segtree;

void _main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++) 
		scanf("%d%d", &p[i].first, &p[i].second);
	bld(1,1,n);
	for(int i = 1; i < n - 1; i ++){
		s = p[i], t = p[i + 1];
		printf("%d ", qry(i + 2, 1, 1, n));
	}
	puts("0");
}

动态维护(未知正确)

考虑楼房重建同款做法,我们可以拿平衡树维护一个增减纵坐标后的点。

我们考虑只维护一些局部信息,记录凸包上的哪一条边跨过了两个区间,就可以根据斜率快速判断往哪里继续递归。

我们考虑如何维护这个斜率,考虑两个儿子的情况,求出其两条向中间上升的线交的情况,那么跨这两条的线一定在他们中间,我们想内侧递归。

对于两线向中间下降,我们直接向外侧儿子递归。

对于两线同增减,公切线就是递归左(或右儿子)的全部凸包和右(或左)儿子的一部分。

那么我们就可以做到 \(\mathcal O(\log n)\) 完成单点修改。

线段树分治

实际上应该叫时间分治。

给定一个 \(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;

可持久化线段树

区间修改、区间查询当前版本、区间查询历史版本。

算法介绍

比如有一个版本 \(i\) 的线段树根是 \(rt_i\),对于下一个版本,我们只新建最新的一条树链,对剩下的节点我们直接连到前一个版本的节点上。这样我们就只会有新建 \(\log n\) 个节点。

每次访问从一个版本的根出发即可。

复杂度分析

当我们每修改一个节点,都会新增 \(\log n\) 个节点,时间复杂度不变,空间复杂度在 \(\mathcal O(n \log n)\)

具体实现

#include<bits/stdc++.h>

using namespace std;

const int N = 2e6 + 1;

int n, q;
int a[N];

struct PST{
	#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

	int ls[N * 20], rs[N * 20], sum[N * 20], rt[N], idx;

	void bld(segroot){
		if(!u) u = ++ idx;
		if(l == r) return sum[u] = a[l], void();
		bld(lson), bld(rson);
	}

	void mdf(int pos, int x, int v, segroot){
		if(l > pos || r < pos) return !u ? u = v : 0, void();
		if(!u) u = ++ idx;
		if(l == r) return sum[u] = x, void();
		mdf(pos, x, ls[v], lson), mdf(pos, x, rs[v], rson);
	}

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

int main(){
	scanf("%d%d", &n, &q);
	for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
	T.bld(T.rt[0]);
	for(int i = 1; i <= q; i ++){
		int op, x, y, z; scanf("%d%d%d", &x, &op, &y);
		if(op == 1){
			scanf("%d", &z);
			T.mdf(y, z, T.rt[x], T.rt[i]);
		} else {
			printf("%d\n", T.qry(y, T.rt[x]));
			T.rt[i] = T.rt[x];
		}
	}
	return 0;
}

主席树

这是 HJT 发明的线段树做法。

就是可持久化权值线段树。

求静态区间第 \(k\) 小。

如果没有区间限制,就是一个普通的线段树二分板子。

我们考虑在值域上建线段树,令版本 \(i\) 表示添加了第 \(1 \sim i\) 中数的值域线段树。

发现 \([l, r]\)\([v_l, v_r]\) 的个数 \(= [1, r] - [1, l - 1]\)

所以我们可以同时在两棵线段树上二分就可以了。

可持久化并查集

为什么把它放在这里呢?因为这也是可持久化数组的应用。

首先,这里的并查集不能够路径压缩,因为这样实际上使用的是均摊复杂度,在基于树形的版本操作时复杂度不对。

所以我们只适用按秩合并,最坏情况最多 \(\mathcal O(n \alpha(n))\)

具体实现就是把并查集中用到的数组换成可持久化数组即可。

这里我们就换了另一种状态树的实现方式。

#include<bits/stdc++.h>

using namespace std;

const int N = 400010;

int n, m;
int fa[N], sz[N];
vector<int> ed[N];
int ans[N];
array<int, 3> opt[N];

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

void dfs(int u){
	int la = -1, lb = -1, lfa = -1;
	if(opt[u][0] == 1){
		int a = opt[u][1], b = opt[u][2];
		int x = find(a), y = find(b);
		if(x != y){
			if(sz[x] > sz[y]) swap(x, y);
			sz[y] += sz[x];
			la = x, lfa = fa[x], lb = y;
			fa[x] = y;
		}
	} else if(opt[u][0] == 3) {
		int a = opt[u][1], b = opt[u][2];
		ans[u] = (find(a) == find(b));
	}
	for(auto v : ed[u]) dfs(v);
	if(la != -1){
		fa[la] = lfa, sz[lb] -= sz[la];
	}
}

int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i ++) fa[i] = i, sz[i] = 1;
	for(int i = 1; i <= m; i ++){
		int op, x, y; scanf("%d%d", &op, &x);
		if(op == 2){
			ed[x].push_back(i);
			opt[i] = {op, x, -1};
		} else scanf("%d", &y), ed[i - 1].push_back(i), opt[i] = {op, x, y};
	}
	dfs(0);
	for(int i = 1; i <= m; i ++) if(opt[i][0] == 3) printf("%d\n", ans[i]);
	return 0;
}

可持久化 01 Trie

P4735 最大异或和 - 洛谷

我们仿照可持久化线段树,在 \(\text{Trie}\) 上我们将新点新建,旧点连边,然后我们就有了可持久化 \(01 \; \text{Trie}\) 的数据结构,空间尽量开大点,时间不变。

#include<bits/stdc++.h>

using namespace std;

const int N = 3e5 + 5;

int n, m;
int nxt[N * 25][2], idx, cnt[N * 25];
int rt[N * 2], sum[N * 2];

void add(int u, int v, int w, int x){
	if(w < 0) return ;
	int c = (x >> w) & 1;
	nxt[u][c ^ 1] = nxt[v][c ^ 1];
	nxt[u][c] = ++ idx;
	cnt[nxt[u][c]] = cnt[nxt[v][c]] + 1;
	add(nxt[u][c], nxt[v][c], w - 1, x);
}

int qry(int u, int v, int x, int w){
	if(w < 0) return 0;
	int c = (x >> w) & 1;
	if(cnt[nxt[v][c ^ 1]] > cnt[nxt[u][c ^ 1]]) return qry(nxt[u][c ^ 1], nxt[v][c ^ 1], x, w - 1) ^ (1 << w);
	return qry(nxt[u][c], nxt[v][c], x, w - 1);
}

int main(){
	scanf("%d%d", &n, &m);
	add(rt[0] = ++ idx, 0, 24, 0);
	for(int i = 1, x; i <= n; i ++){
		scanf("%d", &x);
		sum[i] = sum[i - 1] ^ x;
		add(rt[i] = ++ idx, rt[i - 1], 24, sum[i]);
	}
	while(m --){
		char op; int x, y, z; scanf("\n%c%d", &op, &x);
		if(op == 'A'){
			n ++;
			sum[n] = sum[n - 1] ^ x;
			add(rt[n] = ++ idx, rt[n - 1], 24, sum[n]);
		} else {
			scanf("%d%d", &y, &z);
			x --, y --;
			if(x == 0) printf("%d\n", qry(0, rt[y], sum[n] ^ z, 24));
			else printf("%d\n", qry(rt[x - 1], rt[y], sum[n] ^ z, 24));
		}
	}
	return 0;
}	
posted @ 2026-07-22 11:06  Hty111  阅读(10)  评论(0)    收藏  举报