线段树
线段树
普通线段树
#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;
}
楼房重建
我们发现,若 \(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]\) 为了和 trie 、treap 对比),然后其他的就一样了。
使用动态开点线段树我们就可以做到维护一个 \(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]);
}
线段树二分
对象
- 数组本身具有单调性。
- 询问区间内第一个 \(\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\) 进行分类讨论:
- \(v \ge mx\):所有数均不超过 \(v\),则该区间无需修改。
- \(se < v < mx\):只需将最大值修改为 \(v\),其余不变,需要打标记。
- \(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
我们仿照可持久化线段树,在 \(\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;
}

浙公网安备 33010602011771号