ExtractStars的算法竞赛模板 v4.0 (数据结构)
退役了,把之前使用的板子分享一下,没办法一下发全部,想看完整的可以去博客园,docx版本(已排版)网盘链接:https://pan.baidu.com/s/15C0G5CY2aqQykuc5sHz_RQ?pwd=jfur
二、数据结构
并查集
朴素并查集
算法介绍
并查集用父指针维护若干不交集,按秩合并与路径压缩可以在近乎常数的均摊时间内完成合并与查询。find 返回代表元并在回溯过程中压缩路径,unite 将两个集合的小树挂到大树以减少高度。赛时常配合排序或扫描线解决连通性判定与合并类问题。
常见例题
**题目:**给定 n 个点与 q 次操作,操作分为两种,合并集合和询问两个点是否连通,要求在线输出所有连通性答案。
**做法:**用并查集维护集合代表,遇到合并操作就合并,遇到询问操作就比较代表是否相同并输出答案。
代码
// 朴素并查集,按大小合并与路径压缩
// 功能:维护不交集的合并与连通性查询;支持带参数构造与延后init
// 复杂度:单次摊还近乎O(1)
struct DisjointSet
{
int n; // 元素个数
vector<int> parent; // 父指针
vector<int> compSize; // 以代表为根的集合大小
// 构造函数,若给定规模则直接完成初始化
DisjointSet(int n_ = 0)
{
if (n_) init(n_);
else n = 0;
}
// 初始化,将每个元素设为独立集合
void init(int n_)
{
n = n_;
parent.resize(n + 1);
compSize.assign(n + 1, 1);
for (int i = 1; i <= n; i++) parent[i] = i;
}
// 查询并返回x的代表元,带路径压缩
int find(int x)
{
while (x != parent[x]) x = parent[x] = parent[parent[x]];
return x;
}
// 合并a与b所在集合,返回是否发生合并
bool unite(int a, int b)
{
int x = find(a), y = find(b);
if (x == y) return false;
if (compSize[x] < compSize[y]) swap(x, y);
parent[y] = x, compSize[x] += compSize[y];
return true;
}
// 查询a与b是否连通
bool same(int a, int b)
{
return find(a) == find(b);
}
// 返回元素r所在集合大小
int size(int r)
{
return compSize[find(r)];
}
};
可撤销并查集
算法介绍
可撤销并查集通过保存每次修改的快照来支持回滚,核心思想是不做路径压缩,只按大小合并,并把被修改的父指针与大小信息压栈,回滚时弹栈恢复。它与分治或线段树分配边的框架组合,可以离线处理带删除的连边问题。
常见例题
**题目:**在一个包含 n 个点的动态图中,给定 m 条边的添加与删除序列,并给定若干时刻的询问连通块数量,要求按时间顺序输出答案。
**做法:**将每条边的生存区间分配到线段树节点,在分治遍历时把该节点负责的所有边合并进可撤销并查集,进入子区间递归,回溯时回滚到进入前的栈高度,这样每条边被合并的次数为其被覆盖的结点数,整体复杂度近似为 O((n + m) log m)。
代码
// 可撤销并查集(Rollback DSU),不做路径压缩,仅按大小合并
// 功能:支持合并、查询、快照与回滚;构造时可直接指定规模
// 复杂度:合并与回滚摊还近乎O(1)
struct RollbackDSU
{
int n; // 元素个数
vector<int> parent; // 父指针
vector<int> compSize; // 集合大小
vector<pair<int,int>> history; // 变更栈:(who, oldParent) 或 (who, -oldSize)
int comps; // 当前连通块数量
// 构造函数,若给定规模则直接初始化
RollbackDSU(int n_ = 0)
{
if (n_) init(n_);
else n = 0, comps = 0;
}
// 初始化n个独立集合
void init(int n_)
{
n = n_;
parent.resize(n + 1);
compSize.assign(n + 1, 1);
for (int i = 1; i <= n; i++) parent[i] = i;
history.clear();
comps = n;
}
// 查找代表元,不做路径压缩以便回滚
int find(int x)
{
while (x != parent[x]) x = parent[x];
return x;
}
// 保存当前历史栈高度作为快照
int snapshot()
{
return (int)history.size();
}
// 回滚到指定快照高度
void rollback(int snap)
{
while ((int)history.size() > snap)
{
auto [who, val] = history.back(); history.pop_back();
if (val >= 0) parent[who] = val;
else compSize[who] = -val;
}
}
// 合并a与b所在集合,返回是否发生合并
bool unite(int a, int b)
{
int x = find(a), y = find(b);
if (x == y) return false;
if (compSize[x] < compSize[y]) swap(x, y);
history.emplace_back(y, parent[y]); parent[y] = x;
history.emplace_back(x, -compSize[x]); compSize[x] += compSize[y];
comps--;
return true;
}
// 返回当前连通块数量
int count()
{
return comps;
}
// 判断是否连通
bool same(int a, int b)
{
return find(a) == find(b);
}
};
树状数组
一维树状数组
算法介绍
树状数组用低位元操作维护前缀信息,支持单点修改与前缀查询,区间查询通过差前缀实现。模板通常提供 add 和 sum 两个基本操作,并在 select 中利用二进制提升按权选择第 k 小位置。
常见例题
**题目:**给定长度为 n 的数组和 q 次操作,操作一为在位置 p 上加上 v,操作二为询问区间 [l, r] 的元素和。
**做法:**用一维树状数组维护前缀和,区间和由 sum® 减去 sum(l − 1) 得到,所有操作在对数时间内完成。
代码
// 一维树状数组(Fenwick),支持单点加、前缀和、区间和、按权选择
// 功能:维护加法型前缀信息;构造时可直接给定规模
// 复杂度:单次操作O(log n)
template <typename T>
struct Fenwick
{
int n; // 大小
vector<T> bit; // 树状数组,下标从1开始
// 构造函数,若给定规模则完成初始化
Fenwick(int n_ = 0)
{
if (n_) init(n_);
else n = 0;
}
// 初始化大小为n_,元素清零
void init(int n_)
{
n = n_;
bit.assign(n + 1, T{});
}
// 在位置x增加值v
void add(int x, T v)
{
for (int i = x; i <= n; i += i & -i) bit[i] = bit[i] + v;
}
// 查询前缀和sum[1..x]
T sum(int x)
{
T res{};
for (int i = x; i > 0; i -= i & -i) res = res + bit[i];
return res;
}
// 查询区间[l..r]之和
T rangeSum(int l, int r)
{
if (l > r) return T{};
return sum(r) - sum(l - 1);
}
// 选择满足前缀和<=k的最大下标,要求所有值非负
int select(T k)
{
int x = 0;
T cur{};
for (int pw = 1 << __lg(n); pw; pw >>= 1)
{
int nx = x + pw;
if (nx <= n && cur + bit[nx] <= k) x = nx, cur = cur + bit[nx];
}
return x;
}
};
二维树状数组
算法介绍
二维树状数组在两个维度上同时使用低位元操作,支持点更新与子矩形前缀查询,进而得到任意轴对齐矩形的区间和。内存为 O(nm),操作为 O(log n log m),适合中等规模的二维求和问题。
常见例题
**题目:**给定一个 n 行 m 列的整数矩阵,有 q 次操作,操作一为把位置 (x, y) 的值加上 v,操作二为询问子矩形 [x1, y1] 到 [x2, y2] 的元素和。
**做法:**用二维树状数组实现点更新与前缀矩形和的查询,答案由四个前缀的容斥组合得到。
代码
// 二维树状数组(Fenwick 2D),支持点加与子矩形求和
// 功能:维护矩阵轴对齐子矩形和;构造时可直接给定行列
// 复杂度:单次操作O(log n log m)
template <typename T>
struct Fenwick2D
{
int n, m; // 行列尺寸
vector<vector<T>> bit; // 二维树状数组,下标从1开始
// 构造函数,若给定尺寸则直接初始化
Fenwick2D(int n_ = 0, int m_ = 0)
{
if (n_ && m_) init(n_, m_);
else n = 0, m = 0;
}
// 初始化为n_行m_列
void init(int n_, int m_)
{
n = n_, m = m_;
bit.assign(n + 1, vector<T>(m + 1, T{}));
}
// 在坐标(x,y)加上v
void add(int x, int y, T v)
{
for (int i = x; i <= n; i += i & -i)
for (int j = y; j <= m; j += j & -j)
bit[i][j] = bit[i][j] + v;
}
// 查询前缀子矩形[1..x][1..y]之和
T sum(int x, int y)
{
T res{};
for (int i = x; i > 0; i -= i & -i)
for (int j = y; j > 0; j -= j & -j)
res = res + bit[i][j];
return res;
}
// 查询子矩形[x1..x2][y1..y2]之和
T rangeSum(int x1, int y1, int x2, int y2)
{
if (x1 > x2 || y1 > y2) return T{};
return sum(x2, y2) - sum(x1 - 1, y2) - sum(x2, y1 - 1) + sum(x1 - 1, y1 - 1);
}
};
线段树
朴素线段树
算法介绍
在线段树上维护区间信息,支持单点修改与区间查询。将数组建成一棵完全二叉树形结构,每个结点覆盖一个区间 [l,r],结点信息由左右儿子合并得到。查询时在 [ql,qr] 与当前结点 [l,r] 的相交关系下递归;单点修改时自顶向下找到叶子并回溯更新父结点。
常见例题
**题目:**给定长度为 n 的数组 a,q 次操作,操作一为 1 x v 表示将 a[x] 赋值为 v,操作二为 2 l r 查询区间 [l,r] 的区间和、最小值与最大值。
**做法:**用朴素线段树维护 Info = {sum, min, max}。单点修改用 modify(pos, val)。区间查询用 rangeQuery(l, r) 返回 Info,再输出 sum/min/max 即可。由于所有区间都是闭区间,递归时严格用 [l,r] 与 [ql,qr] 比较,相等或包含时直接返回整段信息,完全不相交返回空信息。
代码
// 朴素线段树
// 约定:Info 需要支持 Info()+Info 合并,且提供静态的空信息构造 empty()
// 下面附了一个示例 Info,用于区间和/最小值/最大值
template <typename T>
struct Info
{
T sum, mn, mx;
// 构造与重置
Info() : sum(0), mn(numeric_limits<T>::max()), mx(numeric_limits<T>::min()) {}
Info(T v) : sum(v), mn(v), mx(v) {}
static Info empty() { return {}; }
// 合并两个区间的信息
friend Info operator+(const Info &a, const Info &b)
{
if (a.mn == numeric_limits<T>::max())
return b;
if (b.mn == numeric_limits<T>::max())
return a;
Info c;
c.sum = a.sum + b.sum;
c.mn = min(a.mn, b.mn);
c.mx = max(a.mx, b.mx);
return c;
}
};
template <typename Info>
struct SegmentTree
{
int n; // 维护的元素个数
vector<Info> tr; // 线段树结点信息
// 构造函数:给定长度,初值为 Info()
SegmentTree(int n_ = 0) : n(0)
{
if (n_)
init(n_);
}
// 构造函数:用数组初始化
template <typename T>
SegmentTree(const vector<T> &a) { init(a); }
// 功能:按长度初始化为 n 个元素,初值为 Info()
void init(int n_)
{
n = n_;
tr.assign(4 * n + 4, Info::empty());
}
// 功能:用数组 a 初始化
template <typename T>
void init(const vector<T> &a)
{
n = (int)a.size();
tr.assign(4 * n + 4, Info::empty());
build(1, 0, n - 1, a);
}
// 功能:自底向上合并信息
void pull(int p)
{
tr[p] = tr[p << 1] + tr[p << 1 | 1];
}
// 功能:建树,覆盖区间 [l,r]
template <typename T>
void build(int p, int l, int r, const vector<T> &a)
{
if (l == r)
{
tr[p] = Info(a[l]);
return;
}
int m = (l + r) >> 1;
build(p << 1, l, m, a), build(p << 1 | 1, m + 1, r, a);
pull(p);
}
// 功能:单点赋值,将 pos 位置改成 v;覆盖区间 [l,r]
void modify(int p, int l, int r, int pos, const Info &v)
{
if (l == r)
{
tr[p] = v;
return;
}
int m = (l + r) >> 1;
if (pos <= m)
modify(p << 1, l, m, pos, v);
else
modify(p << 1 | 1, m + 1, r, pos, v);
pull(p);
}
// 外部接口:单点赋值
void modify(int pos, const Info &v) { modify(1, 0, n - 1, pos, v); }
// 功能:查询区间 [ql,qr] 的聚合信息;当前结点覆盖 [l,r]
Info rangeQuery(int p, int l, int r, int ql, int qr)
{
if (qr < l || r < ql)
return Info::empty();
if (ql <= l && r <= qr)
return tr[p];
int m = (l + r) >> 1;
return rangeQuery(p << 1, l, m, ql, qr) + rangeQuery(p << 1 | 1, m + 1, r, ql, qr);
}
// 外部接口:区间查询
Info rangeQuery(int l, int r) { return rangeQuery(1, 0, n - 1, l, r); }
// 功能:在区间 [ql,qr] 上二分找第一个使 pred(Info) 为真的位置
// 语义:pred 对“整段信息”判定是否存在可行解;若整段都不满足直接剪枝
template <typename F>
int findFirst(int p, int l, int r, int ql, int qr, F pred)
{
if (qr < l || r < ql || !pred(tr[p]))
return -1;
if (l == r)
return l;
int m = (l + r) >> 1, res = findFirst(p << 1, l, m, ql, qr, pred);
if (res != -1)
return res;
return findFirst(p << 1 | 1, m + 1, r, ql, qr, pred);
}
// 外部接口:找第一个满足条件的位置
template <typename F>
int findFirst(int l, int r, F pred) { return findFirst(1, 0, n - 1, l, r, pred); }
// 功能:在区间 [ql,qr] 上二分找最后一个使 pred(Info) 为真的位置
template <typename F>
int findLast(int p, int l, int r, int ql, int qr, F pred)
{
if (qr < l || r < ql || !pred(tr[p]))
return -1;
if (l == r)
return l;
int m = (l + r) >> 1, res = findLast(p << 1 | 1, m + 1, r, ql, qr, pred);
if (res != -1)
return res;
return findLast(p << 1, l, m, ql, qr, pred);
}
// 外部接口:找最后一个满足条件的位置
template <typename F>
int findLast(int l, int r, F pred) { return findLast(1, 0, n - 1, l, r, pred); }
};
懒标记线段树
算法介绍
当需要对整段 [l,r] 进行区间操作时(如区间加、区间赋值、区间取 min 等),在线段树上叠加懒标记。若当前整段完全被修改区间覆盖,直接对结点信息进行一次“打标应用”,并把标记累积到当前结点;下推时再把标记分发给左右儿子。查询逻辑与朴素一致,但在向下递归前要先 push 把懒标记下传,保证子树信息正确。为配合原版模板功能,保留对整段应用标签的 rangeApply、以及在区间上用结点聚合信息二分定位的 findFirst / findLast。
常见例题
**题目:**给定长度为 n 的数组 a,q 次操作,操作一为 1 l r x 令区间 [l,r] 全部加上 x,操作二为 2 l r 查询 [l,r] 的区间和。额外要求支持查询“区间 [l,r] 内从左到右第一个前缀和超过 S 的位置”。
**做法:**用懒标树,Info 维护 sum 与 len,Tag 维护 add。区间加时整段 sum 增加 add×len。区间和直接返回。要二分位置时,pred(Info) 可以判断“这段的 sum 是否 > S”,findFirst(l, r, pred) 即可在 O(log n) 内返回答案。
代码
// 懒标记线段树
// 约定:Info 需要提供 apply(Tag) 与合并 operator+;Tag 需要提供 apply(Tag) 的“自合并”
// 下面附了一个常用示例:区间加 + 区间和
template <typename T>
struct Info
{
T sum;
int len;
Info() : sum(0), len(0) {}
Info(T v, int l) : sum(v), len(l) {}
static Info empty() { return {}; }
void apply(const T &add) { sum += add * len; } // 对整段加 add
friend Info operator+(const Info &a, const Info &b)
{
if (a.len == 0)
return b;
if (b.len == 0)
return a;
return Info(a.sum + b.sum, a.len + b.len);
}
};
template <typename T>
struct Tag
{
T add;
Tag() : add(0) {}
explicit Tag(T a) : add(a) {}
void apply(const Tag &t) { add += t.add; } // 累加标记
};
template <typename Info, typename Tag>
struct LazySegmentTree
{
int n; // 维护的元素个数
vector<Info> tr; // 结点信息
vector<Tag> tg; // 懒标记
// 构造函数:给定长度,初值为 Info()
LazySegmentTree(int n_ = 0) : n(0)
{
if (n_)
init(n_);
}
// 构造函数:用数组初始化
template <typename T>
LazySegmentTree(const vector<T> &a) { init(a); }
// 功能:按长度初始化为 n 个元素,初值为 Info()
void init(int n_)
{
n = n_;
tr.assign(4 * n + 4, Info::empty());
tg.assign(4 * n + 4, Tag());
}
// 功能:用数组 a 初始化
template <typename T>
void init(const vector<T> &a)
{
n = (int)a.size();
tr.assign(4 * n + 4, Info::empty());
tg.assign(4 * n + 4, Tag());
build(1, 0, n - 1, a);
}
// 功能:自底向上合并信息
void pull(int p) { tr[p] = tr[p << 1] + tr[p << 1 | 1]; }
// 功能:把标记 t 应用到结点 p
void apply(int p, const Tag &t)
{
// Info 上的 apply 需要你自己在 Info 中定义
tr[p].apply(t.add);
tg[p].apply(t);
}
// 功能:把结点 p 的懒标记下传到两个儿子
void push(int p)
{
if (tg[p].add != 0)
{
apply(p << 1, tg[p]), apply(p << 1 | 1, tg[p]);
tg[p] = Tag();
}
}
// 功能:建树,覆盖区间 [l,r]
template <typename T>
void build(int p, int l, int r, const vector<T> &a)
{
if (l == r)
{
tr[p] = Info(a[l], 1);
return;
}
int m = (l + r) >> 1;
build(p << 1, l, m, a), build(p << 1 | 1, m + 1, r, a);
pull(p);
}
// 功能:单点赋值,将 pos 位置改成 v;覆盖区间 [l,r]
void modify(int p, int l, int r, int pos, const Info &v)
{
if (l == r)
{
tr[p] = v;
return;
}
int m = (l + r) >> 1;
push(p);
if (pos <= m)
modify(p << 1, l, m, pos, v);
else
modify(p << 1 | 1, m + 1, r, pos, v);
pull(p);
}
// 外部接口:单点赋值
void modify(int pos, const Info &v) { modify(1, 0, n - 1, pos, v); }
// 功能:对区间 [ql,qr] 应用标签 t;当前结点覆盖 [l,r]
void rangeApply(int p, int l, int r, int ql, int qr, const Tag &t)
{
if (qr < l || r < ql)
return;
if (ql <= l && r <= qr)
{
apply(p, t);
return;
}
int m = (l + r) >> 1;
push(p);
rangeApply(p << 1, l, m, ql, qr, t), rangeApply(p << 1 | 1, m + 1, r, ql, qr, t);
pull(p);
}
// 外部接口:区间打标
void rangeApply(int l, int r, const Tag &t) { rangeApply(1, 0, n - 1, l, r, t); }
// 功能:查询区间 [ql,qr] 的聚合信息;当前结点覆盖 [l,r]
Info rangeQuery(int p, int l, int r, int ql, int qr)
{
if (qr < l || r < ql)
return Info::empty();
if (ql <= l && r <= qr)
return tr[p];
int m = (l + r) >> 1;
push(p);
return rangeQuery(p << 1, l, m, ql, qr) + rangeQuery(p << 1 | 1, m + 1, r, ql, qr);
}
// 外部接口:区间查询
Info rangeQuery(int l, int r) { return rangeQuery(1, 0, n - 1, l, r); }
// 功能:在 [ql,qr] 内用整段信息二分找第一个满足 pred 的位置
template <typename F>
int findFirst(int p, int l, int r, int ql, int qr, F pred)
{
if (qr < l || r < ql || !pred(tr[p]))
return -1;
if (l == r)
return l;
int m = (l + r) >> 1;
push(p);
int res = findFirst(p << 1, l, m, ql, qr, pred);
if (res != -1)
return res;
return findFirst(p << 1 | 1, m + 1, r, ql, qr, pred);
}
// 外部接口:找第一个满足条件的位置
template <typename F>
int findFirst(int l, int r, F pred) { return findFirst(1, 0, n - 1, l, r, pred); }
// 功能:在 [ql,qr] 内用整段信息二分找最后一个满足 pred 的位置
template <typename F>
int findLast(int p, int l, int r, int ql, int qr, F pred)
{
if (qr < l || r < ql || !pred(tr[p]))
return -1;
if (l == r)
return l;
int m = (l + r) >> 1;
push(p);
int res = findLast(p << 1 | 1, m + 1, r, ql, qr, pred);
if (res != -1)
return res;
return findLast(p << 1, l, m, ql, qr, pred);
}
// 外部接口:找最后一个满足条件的位置
template <typename F>
int findLast(int l, int r, F pred) { return findLast(1, 0, n - 1, l, r, pred); }
};
动态开点线段树
算法介绍
动态开点线段树在需要访问某个子区间时才分配对应的结点,以指针或下标形式把不存在的子树延迟创建,它能在离散化困难或值域极大时以 O(k log U) 的时间与 O(k) 的空间处理 k 次有效访问,其中 U 是整个值域长度。所有区间语义统一为闭区间 [l,r],根结点覆盖外部给定的整段边界 [L,R],左右儿子分别覆盖 [l,mid] 与 [mid+1,r]。
常见例题
**题目:**给定 q 次操作,初始数组视为全零,坐标范围为 [1,10^18]。操作一是把区间 [l,r] 内每个元素加上 v,操作二是询问区间 [l,r] 的区间和。
**做法:**建立一个覆盖 [1,10^18] 的动态开点懒标树,遇到区间加时在完全覆盖的结点直接打标并累加,不完全覆盖时先 push 下传再递归左右儿子,回溯时 pull 合并;区间查询在完全覆盖时直接返回结点信息,不相交返回空信息,部分相交则先 push 再递归合并。
代码
// 动态开点线段树,示例:区间加 + 区间和
// 设计说明:Info 负责“如何合并”和“在长度len上如何应用标记”,Tag 负责“如何与另一个标记合并”与“是否为空”
// 构造函数:可指定值域 [L,R];所有函数均写明用途;单行语句尽量去花括号
struct Tag
{
ll add; // 懒标记中的增量
Tag(ll v = 0) : add(v) {}
void apply(const Tag &o) { add += o.add; }
bool isNeutral() const { return add == 0; }
};
struct Info
{
ll sum;
int len; // 区间和与区间长度
Info(ll s = 0, int l = 0) : sum(s), len(l) {}
static Info empty() { return {}; }
void apply(const Tag &t) { sum += t.add * 1LL * len; }
friend Info operator+(const Info &a, const Info &b)
{
if (a.len == 0)
return b;
if (b.len == 0)
return a;
return Info(a.sum + b.sum, a.len + b.len);
}
};
template <class Info, class Tag>
struct DynamicSegTree
{
struct Node
{
Info info;
Tag tag;
Node *ls, *rs;
// 功能:结点构造,默认空信息与空标记
Node() : info(Info::empty()), tag(Tag()), ls(nullptr), rs(nullptr) {}
};
Node *root;
ll L, R; // 根指针与全局覆盖边界
// 构造:给定整段边界 [L_,R_],根为空,按需开点
DynamicSegTree(ll L_ = 1, ll R_ = 1) : root(nullptr), L(L_), R(R_) {}
// 功能:把标记 v 作用到结点 u 表示的整段 [l,r]
void apply(Node *u, ll l, ll r, const Tag &v)
{
if (!u)
return;
Info cur = u->info;
if (cur.len == 0)
u->info.len = int(r - l + 1);
u->info.apply(v);
u->tag.apply(v);
}
// 功能:下推结点 u 的懒标记到左右儿子
void push(Node *u, ll l, ll r)
{
if (!u || u->tag.isNeutral() || l == r)
return;
ll m = (l + r) >> 1;
if (!u->ls)
u->ls = new Node();
if (!u->rs)
u->rs = new Node();
apply(u->ls, l, m, u->tag), apply(u->rs, m + 1, r, u->tag);
u->tag = Tag();
}
// 功能:自底向上合并左右儿子的区间信息
void pull(Node *u)
{
Info Lc = u->ls ? u->ls->info : Info::empty();
Info Rc = u->rs ? u->rs->info : Info::empty();
u->info = Lc + Rc;
}
// 功能:在区间 [ql,qr] 上施加标记 v(外部闭区间,需保证 ql<=qr 且在 [L,R] 内)
void rangeApply(ll ql, ll qr, const Tag &v) { rangeApply(root, L, R, ql, qr, v); }
// 功能:查询区间 [ql,qr] 的聚合信息(外部闭区间)
Info rangeQuery(ll ql, ll qr) { return rangeQuery(root, L, R, ql, qr); }
// 功能:单点赋值,把位置 x 的信息改为 infoX(常用在需要精确覆盖一个点时)
void modifyPoint(ll x, const Info &infoX) { modifyPoint(root, L, R, x, infoX); }
// 功能:清空整棵树(递归释放内存)
void clear()
{
clear(root);
root = nullptr;
}
private:
// 递归实现:对 [ql,qr] 打标 v,当前结点 u 覆盖 [l,r]
void rangeApply(Node *&u, ll l, ll r, ll ql, ll qr, const Tag &v)
{
if (qr < l || r < ql)
return;
if (!u)
u = new Node();
if (ql <= l && r <= qr)
{
apply(u, l, r, v);
return;
}
push(u, l, r);
ll m = (l + r) >> 1;
rangeApply(u->ls, l, m, ql, qr, v), rangeApply(u->rs, m + 1, r, ql, qr, v);
pull(u);
}
// 递归实现:查询 [ql,qr],当前结点 u 覆盖 [l,r]
Info rangeQuery(Node *u, ll l, ll r, ll ql, ll qr)
{
if (!u || qr < l || r < ql)
return Info::empty();
if (ql <= l && r <= qr)
return u->info;
push(u, l, r);
ll m = (l + r) >> 1;
return rangeQuery(u->ls, l, m, ql, qr) + rangeQuery(u->rs, m + 1, r, ql, qr);
}
// 递归实现:单点赋值,当前结点 u 覆盖 [l,r]
void modifyPoint(Node *&u, ll l, ll r, ll x, const Info &infoX)
{
if (!u)
u = new Node();
if (l == r)
{
u->info = infoX;
return;
}
push(u, l, r);
ll m = (l + r) >> 1;
x <= m ? modifyPoint(u->ls, l, m, x, infoX) : modifyPoint(u->rs, m + 1, r, x, infoX);
pull(u);
}
// 递归实现:释放以 u 为根的子树
void clear(Node *u)
{
if (!u)
return;
clear(u->ls), clear(u->rs), delete u;
}
};
李超线段树
算法介绍
李超线段树用线段树的形态在整个横坐标区间 [l,r] 上维护一组直线 y = kx + b 的下包(或上包),每个结点保存在该结点覆盖区间上“当前更优”的一条直线。插入一条直线时,比较其与结点已存直线在区间中点与端点处的函数值,若新线在整个子区间更优则替换并结束,若只在一半区间更优则递归下发到对应儿子,查询时在点 x 处自上而下取经过的所有直线的更优值。该结构支持在 O(log U) 时间内进行“全域直线插入”与“点查询”,扩展后可支持“区间插入直线”。常用于动态维护分段线性函数的最小值/最大值,如斜率优化、DP 转移、最优代价维护等场景。
常见例题
**题目:**给定 q 次操作,横坐标取值域为 [Xmin, Xmax]。操作一为在区间 [l,r] 上加入一条直线 y = kx + b(只在 [l,r] 有效),操作二为在点 x 处询问当前所有有效直线的最小函数值。
**做法:**建立一棵覆盖 [Xmin, Xmax] 的李超线段树,插入时若是全域插入就对根递归,如果是区间插入就只把直线递归分发到与 [l,r] 相交的结点;查询时从根走到叶,沿途用当前结点保存的直线在 x 处取更优值并与子树结果取更小值(维护最小值时比较“更小”的线,维护最大值时取反或相反的比较)。
代码
// 李超线段树(Li Chao Tree),维护“最小值”版本
// 功能:插入直线 y = kx + b(全域或区间),在点 x 查询所有已插入直线的最小值
// 复杂度:单次插入/查询 O(log(R-L+1)),按需开点,适合超大坐标范围
struct LiChaoTree
{
// 直线:y = kx + b
struct Line
{
ll k, b; // 斜率与截距
Line(ll kk = 0, ll bb = (ll)4e18) : k(kk), b(bb) {}
// 计算 y 值
ll eval(ll x) const { return k * x + b; }
};
// 结点:保存一条在当前区间更优的直线,左右儿子按需开点
struct Node
{
Line ln;
Node *ls, *rs;
Node() : ln(), ls(nullptr), rs(nullptr) {}
};
Node *root;
ll L, R; // 根指针与整体覆盖区间
// 构造函数:建立覆盖 [L_,R_] 的空树
LiChaoTree(ll L_ = 0, ll R_ = 0) : root(nullptr), L(L_), R(R_) {}
// 在整个 [L,R] 上插入一条直线 y = kx + b
void addLine(ll k, ll b)
{
Line ln(k, b);
addLine(root, L, R, ln);
}
// 只在子区间 [l,r] 上插入一条直线 y = kx + b(闭区间)
void addSegment(ll l, ll r, ll k, ll b)
{
if (l > r)
return;
Line ln(k, b);
addSegment(root, L, R, l, r, ln);
}
// 在点 x 查询所有直线的最小值;若没有插入过有效线,返回 +INF
ll query(ll x) const
{
return query(root, L, R, x);
}
// 释放整棵树
void clear()
{
clear(root);
root = nullptr;
}
private:
// 内部:确保结点存在
void ensure(Node *&u)
{
if (!u)
u = new Node();
}
// 内部:在结点 u 所覆盖的 [l,r] 上插入一条直线 ln(全域)
void addLine(Node *&u, ll l, ll r, Line ln)
{
ensure(u);
ll m = (l + r) >> 1;
bool leftBetter = ln.eval(l) < u->ln.eval(l);
bool midBetter = ln.eval(m) < u->ln.eval(m);
bool rightBetter = ln.eval(r) < u->ln.eval(r);
if (midBetter)
swap(u->ln, ln); // 保证结点处保存“当前在中点更优”的线
if (l == r)
return;
if (leftBetter != midBetter)
{
addLine(u->ls, l, m, ln);
}
else if (rightBetter != midBetter)
{
addLine(u->rs, m + 1, r, ln);
}
}
// 内部:在结点 u 所覆盖的 [l,r] 上,仅对 [ql,qr] 部分插入直线 ln(区间插入)
void addSegment(Node *&u, ll l, ll r, ll ql, ll qr, Line ln)
{
if (qr < l || r < ql)
return;
if (ql <= l && r <= qr)
{
addLine(u, l, r, ln);
return;
}
ensure(u);
ll m = (l + r) >> 1;
addSegment(u->ls, l, m, ql, qr, ln), addSegment(u->rs, m + 1, r, ql, qr, ln);
}
// 内部:在点 x 查询最小值
ll query(Node *u, ll l, ll r, ll x) const
{
if (!u)
return (ll)4e18;
ll res = u->ln.eval(x);
if (l == r)
return res;
ll m = (l + r) >> 1;
if (x <= m)
return min(res, query(u->ls, l, m, x));
return min(res, query(u->rs, m + 1, r, x));
}
// 内部:释放以 u 为根的子树
void clear(Node *u)
{
if (!u)
return;
clear(u->ls), clear(u->rs), delete u;
}
};
代码维护的是“最小值”李超树,如果需要维护“最大值”,可以把比较方向改为选更大的线,或在插入时把直线系数取相反数并在查询后取相反数。
主席树 (可持久化权值线段树)
算法介绍
主席树是在权值域上的可持久化线段树。对原数组做坐标压缩,将所有不同值映射到 [1,m] 的权值下标区间 [l,r](本文统一使用闭区间),第 i 个版本的根结点 root[i] 表示前缀 a[1…i] 在权值域上的频次数组。插入第 i 个元素时,从 root[i-1] 沿着 [1,m] 的路径复制结点并在命中的叶子把频次加一得到 root[i]。查询子数组 [L,R] 的第 k 小时,用两棵根 root[R] 和 root[L-1] 同时向下走,依据左右子树频次差决定走向;查询“≤x 的个数”时在 [1,rank(x)] 上做频次差求和。由于每次插入仅复制 O(log m) 个结点,空间 O((n+q) log m),所有操作均为 O(log m)。
常见例题
**题目:**给定长度为 n 的数组与 q 次询问。每次询问给出区间 [L,R] 与整数 k,输出子数组 a[L…R] 的第 k 小元素。
**做法:**先离散化所有值,建立 n 个前缀版本 root[i]。回答 [L,R] 的第 k 小时,从区间 [1,m] 的根结点出发比较左右子树的频次差 cntLeft = sum(left(root[R])) − sum(left(root[L-1])),若 cntLeft ≥ k 则在左子树递归,否则在右子树递归并令 k 减去 cntLeft,直到到达某个叶子下标 pos,最后用反向映射把 pos 还原成原值即为答案。
代码
// 主席树(可持久化权值线段树),闭区间 [1,m] 作为权值域
// 功能:buildPrefix 线性建立 n 个前缀版本;kth(L,R,k) 求子数组第k小;countLeq(L,R,x) 求子数组 ≤x 的个数
// 说明:内部完成坐标压缩,接口用原值;每个函数都有用途注释,括号与单行规约遵循你的格式要求
struct PersistentSeg
{
// 结点存左儿子下标、右儿子下标、该权值段出现次数之和
struct Node
{
int ls, rs, sum;
Node(int L = 0, int R = 0, int S = 0) : ls(L), rs(R), sum(S) {}
};
int n; // 原数组长度
int m; // 离散后权值域大小
vector<int> xs; // 离散值(有序去重),xs[pos-1] 是权值下标 pos 对应的原值
vector<Node> tr; // 结点池,结点0为“空结点”
vector<int> root; // root[i] 表示前缀 a[1..i] 的版本根(root[0] 为空前缀)
// 构造函数:空构造
PersistentSeg() : n(0), m(0)
{
tr.reserve(1 << 20);
tr.push_back(Node());
}
// 功能:对数组 a 进行坐标压缩,建立 xs 与映射
template <typename T>
void compress(const vector<T> &a)
{
xs.assign(a.begin(), a.end());
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(), xs.end()), xs.end());
m = (int)xs.size();
}
// 功能:把原值 v 映射为权值下标 pos(1..m)
template <typename T>
int rankOf(const T &v)
{
int pos = (int)(lower_bound(xs.begin(), xs.end(), v) - xs.begin()) + 1;
return pos;
}
// 功能:在旧根 oldRoot 的基础上,把权值下标 pos 的频次 +1,返回新根;当前覆盖 [l,r]
int addOne(int oldRoot, int l, int r, int pos)
{
int cur = clone(oldRoot);
tr[cur].sum = tr[oldRoot].sum + 1;
if (l == r)
return cur;
int mid = (l + r) >> 1;
if (pos <= mid)
tr[cur].ls = addOne(tr[oldRoot].ls, l, mid, pos);
else
tr[cur].rs = addOne(tr[oldRoot].rs, mid + 1, r, pos);
return cur;
}
// 功能:克隆一个结点并返回新结点下标
int clone(int p)
{
tr.push_back(tr[p]);
return (int)tr.size() - 1;
}
// 功能:基于数组 a 建立 n+1 个前缀版本 root,root[0]=0;区间为 [1,m]
template <typename T>
void buildPrefix(const vector<T> &a)
{
n = (int)a.size();
compress(a);
root.assign(n + 1, 0);
for (int i = 1; i <= n; i++)
{
int pos = rankOf(a[i - 1]);
root[i] = addOne(root[i - 1], 1, m, pos);
}
}
// 功能:查询子数组 [L,R] 的第 k 小,返回原值;要求 1<=L<=R<=n 且 1<=k<=R-L+1
ll kth(int L, int R, int k)
{
int u = root[R], v = root[L - 1];
int l = 1, r = m;
while (l < r)
{
int mid = (l + r) >> 1;
int cntLeft = tr[tr[u].ls].sum - tr[tr[v].ls].sum;
if (cntLeft >= k)
u = tr[u].ls, v = tr[v].ls, r = mid;
else
u = tr[u].rs, v = tr[v].rs, l = mid + 1, k -= cntLeft;
}
return xs[l - 1];
}
// 功能:统计子数组 [L,R] 中 ≤ x 的元素个数
ll countLeq(int L, int R, ll x)
{
if (!m)
return 0;
int pos = (int)(upper_bound(xs.begin(), xs.end(), x) - xs.begin());
if (pos == 0)
return 0;
return queryPrefix(root[R], 1, m, 1, pos) - queryPrefix(root[L - 1], 1, m, 1, pos);
}
// 功能:区间求和,求版本根 rt 在权值下标区间 [ql,qr] 的频次和;当前覆盖 [l,r]
ll queryPrefix(int rt, int l, int r, int ql, int qr)
{
if (!rt || qr < l || r < ql)
return 0;
if (ql <= l && r <= qr)
return tr[rt].sum;
int mid = (l + r) >> 1;
ll res = 0;
if (ql <= mid)
res += queryPrefix(tr[rt].ls, l, mid, ql, qr);
if (qr > mid)
res += queryPrefix(tr[rt].rs, mid + 1, r, ql, qr);
return res;
}
};
若题目要支持“第 k 大”,可以把 k 替换为区间长度减 k 再加一;若要支持“严格小于 x 的个数”,把 upper_bound 改成 lower_bound。该模板对值域极大或含负数都适用,因为已内置坐标压缩;如需在线插入删除,也可维护“可持久化差分”或转做可删主席树(BIT/线段树 + 上界二分),思路与本模板配合即可。
区间最值操作 & 区间历史最值
算法介绍
区间最值操作指在闭区间 [l,r] 上执行 a[i] ← min(a[i], x) 或 a[i] ← max(a[i], x),常与区间加法与区间查询(区间和、区间最大值、区间最小值)同时出现。经典做法是 Segment Tree Beats:每个结点维护当前最大值 max1、严格次大值 max2、最大值出现次数 cntMax,以及对称的 min1、min2、cntMin,再维护区间和 sum 与区间加标记 add。对区间 chmin(x) 时,若 x ≥ max1 则无需处理;若 x > max2,则可以在该结点“整段生效”,把 max1 降到 x,并用 cntMax 精确修正 sum;否则向下分裂递归并回收。整体复杂度可用势能法证明为均摊 O(log n)(区间 chmax(x) 对称)。这套维护量与操作流程与 OI Wiki 的描述一致,并能在同一棵树内同时支持 rangeAdd/rangeChmin/rangeChmax 与 rangeQuery(sum、min、max)。
“区间历史最值”问题要求在一系列区间更新后,支持查询某段内“某位置曾达到过的最大值/最小值/历史版本和”等历史信息。一个常见且实用的版本是“历史最大值 with 区间加/取 min/max”:在传播“会让值上升”的操作时,把“上升量”同步计入历史信息;对“只会下降”的操作(如 chmin)历史信息不变。实现时可在 beats 的节点上增设历史字段与“历史加标记”,在整段生效的场景下把对某一“值类”(如全为当前最大值或当前最小值的那部分)带来的提升累加进历史即可;必要时还要像维护 max1、max2 一样为历史值做“面向值类”的打标与合并。OI Wiki把这类做法称作“历史信息”,并给出“历史最大值/历史最小值/历史版本和”的建模思路及与 beats 的配合要点。
常见例题
题目: 维护一个长度为 n 的序列,支持 q 次操作:一是区间加 v;二是把 [l,r] 内所有数与 x 取 min;三是把 [l,r] 内所有数与 x 取 max;四是询问 [l,r] 的区间和与区间最大值;五是询问 [l,r] 的“历史最大值”。
做法: 用 Segment Tree Beats 维护当前量(sum、max1/max2/cntMax、min1/min2/cntMin、add),并在“整段生效”的三类操作里同步更新历史信息:对 rangeAdd(v) 若 v>0 则结点历史最大值整体加 v;对 rangeChmax(x) 若 x>min1 且 x≤min2 时只提升“等于 min1 的那一类”,可在该结点按 cntMin 精确更新历史;对 rangeChmin(x) 不提升历史。遇到无法整段生效时继续向下分裂,回收时历史量用左右儿子的历史量合并取 max/求和。有关 beats 的“整段生效条件”“为何要记次大/次小值”等细节与正确性,参见 OI Wiki 与若干教程梳理。
代码
// Segment Tree Beats:区间加 / 取min / 取max + 区间和 / 区间最值(闭区间 [l,r])
// 同时维护“历史最大值(曾达到过的最大值)”的区间查询
// 命名与封装风格对齐算竞模板:不使用 function,驼峰命名,struct + template<T>,详细注释
struct SegBeats
{
struct Node
{
ll sum; // 当前区间和
ll max1, max2; // 当前最大值、严格次大值
int cntMax; // 当前最大值的出现次数
ll min1, min2; // 当前最小值、严格次小值
int cntMin; // 当前最小值的出现次数
ll add; // 区间加法懒标记(对当前值)
ll histMax; // 区间内“历史最大值”的最大者
ll histAdd; // 历史最大值的“加标”(当整段整体上升时一并累加)
Node()
{
sum = 0;
max1 = -(1LL << 60);
max2 = -(1LL << 60);
cntMax = 0;
min1 = (1LL << 60);
min2 = (1LL << 60);
cntMin = 0;
add = 0;
histMax = -(1LL << 60);
histAdd = 0;
}
// 功能:以单个元素 x 初始化一个叶结点
static Node fromValue(ll x)
{
Node u;
u.sum = x;
u.max1 = u.min1 = x;
u.max2 = -(1LL << 60);
u.min2 = (1LL << 60);
u.cntMax = u.cntMin = 1;
u.histMax = x; // 历史最大值初始等于初值
return u;
}
};
int n; // 数组大小
vector<Node> tr; // 线段树数组,1-indexed
SegBeats(int n_ = 0) { init(n_); } // 构造函数:创建空树
template <class T>
SegBeats(const vector<T> &a) { build(a); } // 构造函数:根据数组建树
// 功能:初始化大小为 n_ 的空树
void init(int n_)
{
n = n_;
tr.assign(n * 4 + 5, Node());
}
// 功能:由数组 a 建树(下标从 1 开始)
template <class T>
void build(const vector<T> &a)
{
init((int)a.size() - 1);
auto buildDfs = [&](auto &&self, int p, int l, int r) -> void
{
if (l == r)
{
tr[p] = Node::fromValue((ll)a[l]);
return;
}
int m = (l + r) >> 1;
self(self, p << 1, l, m);
self(self, p << 1 | 1, m + 1, r);
pull(p);
};
buildDfs(buildDfs, 1, 1, n);
}
// 功能:对 [l,r] 执行区间加 v
void rangeAdd(int l, int r, ll v) { rangeAdd(1, 1, n, l, r, v); }
// 功能:对 [l,r] 执行区间取 min(x)
void rangeChmin(int l, int r, ll x) { rangeChmin(1, 1, n, l, r, x); }
// 功能:对 [l,r] 执行区间取 max(x)
void rangeChmax(int l, int r, ll x) { rangeChmax(1, 1, n, l, r, x); }
// 功能:查询 [l,r] 的区间和
ll querySum(int l, int r) { return querySum(1, 1, n, l, r); }
// 功能:查询 [l,r] 的当前最大值
ll queryMax(int l, int r) { return queryMax(1, 1, n, l, r); }
// 功能:查询 [l,r] 的当前最小值
ll queryMin(int l, int r) { return queryMin(1, 1, n, l, r); }
// 功能:查询 [l,r] 的“历史最大值”的最大者(段内每个点的历史最大值取 max,再对区间取 max)
ll queryHistMax(int l, int r) { return queryHistMax(1, 1, n, l, r); }
private:
// 功能:把整段结点 p 的“当前值整体 +v”,并同步更新历史信息(仅 v>0 会提升历史)
void applyAdd(int p, ll v, int len)
{
tr[p].sum += v * len;
tr[p].max1 += v;
tr[p].min1 += v;
if (tr[p].max2 > -(1LL << 60))
tr[p].max2 += v;
if (tr[p].min2 < (1LL << 60))
tr[p].min2 += v;
tr[p].add += v;
if (v > 0)
tr[p].histAdd += v, tr[p].histMax += v;
}
// 功能:仅对“当前最大值这一类”执行减小到 x 的整段更新(要求 x 在 (max2,max1) 内)
void applyChminOnMax(int p, ll x)
{
ll delta = tr[p].max1 - x;
tr[p].sum -= delta * tr[p].cntMax;
tr[p].max1 = x;
if (tr[p].min1 == tr[p].max1)
tr[p].min1 = x;
if (tr[p].min2 == tr[p].max1)
tr[p].min2 = x; // 两行短句合并
// 历史最大值不因下降而改变;无需动 hist*
}
// 功能:仅对“当前最小值这一类”执行增大到 x 的整段更新(要求 x 在 (min1,min2) 内)
void applyChmaxOnMin(int p, ll x)
{
ll delta = x - tr[p].min1;
tr[p].sum += delta * tr[p].cntMin;
tr[p].min1 = x;
if (tr[p].max1 == tr[p].min1)
tr[p].max1 = x;
if (tr[p].max2 == tr[p].min1)
tr[p].max2 = x;
// 这一类被整体抬升,历史最大值需要同步抬升
tr[p].histMax = max(tr[p].histMax, tr[p].max1); // 当前最大值已更新到可能更大
}
// 功能:从子结点汇总到父结点 p
void pull(int p)
{
Node &u = tr[p], &L = tr[p << 1], &R = tr[p << 1 | 1];
u.sum = L.sum + R.sum;
if (L.max1 > R.max1)
u.max1 = L.max1, u.cntMax = L.cntMax, u.max2 = max(L.max2, R.max1);
else if (L.max1 < R.max1)
u.max1 = R.max1, u.cntMax = R.cntMax, u.max2 = max(R.max2, L.max1);
else
u.max1 = L.max1, u.cntMax = L.cntMax + R.cntMax, u.max2 = max(L.max2, R.max2);
if (L.min1 < R.min1)
u.min1 = L.min1, u.cntMin = L.cntMin, u.min2 = min(L.min2, R.min1);
else if (L.min1 > R.min1)
u.min1 = R.min1, u.cntMin = R.cntMin, u.min2 = min(R.min2, L.min1);
else
u.min1 = L.min1, u.cntMin = L.cntMin + R.cntMin, u.min2 = min(L.min2, R.min2);
u.add = 0; // pull 不改变 add;add 仅在 push 时下传并在 applyAdd 中叠加
// 历史最大值是左右子区间历史最大值的最大者,同时也要不小于当前 max1
u.histMax = max({L.histMax, R.histMax, u.max1});
u.histAdd = 0;
}
// 功能:把懒标记从 p 下推到左右子
void push(int p, int l, int r)
{
int m = (l + r) >> 1;
if (tr[p].add != 0)
{
applyAdd(p << 1, tr[p].add, m - l + 1);
applyAdd(p << 1 | 1, tr[p].add, r - m);
tr[p].add = 0;
}
// 对“整段生效的 chmin/chmax”没有单独的懒标;我们在进入儿子前按需把父节点的 max/min 约束“压过去”
if (tr[p << 1].max1 > tr[p].max1)
applyChminOnMax(p << 1, tr[p].max1);
if (tr[p << 1 | 1].max1 > tr[p].max1)
applyChminOnMax(p << 1 | 1, tr[p].max1);
if (tr[p << 1].min1 < tr[p].min1)
applyChmaxOnMin(p << 1, tr[p].min1);
if (tr[p << 1 | 1].min1 < tr[p].min1)
applyChmaxOnMin(p << 1 | 1, tr[p].min1);
// 历史加标记对我们这份实现只依赖 applyAdd 中的 v>0 逻辑;这里不存在单独的“历史懒”需要下推
}
// 功能:在结点 p 覆盖的 [l,r] 上执行区间加 v
void rangeAdd(int p, int l, int r, int ql, int qr, ll v)
{
if (qr < l || r < ql)
return;
if (ql <= l && r <= qr)
{
applyAdd(p, v, r - l + 1);
return;
}
push(p, l, r);
int m = (l + r) >> 1;
rangeAdd(p << 1, l, m, ql, qr, v);
rangeAdd(p << 1 | 1, m + 1, r, ql, qr, v);
pull(p);
}
// 功能:在结点 p 覆盖的 [l,r] 上执行区间 chmin(x)
void rangeChmin(int p, int l, int r, int ql, int qr, ll x)
{
if (qr < l || r < ql || tr[p].max1 <= x)
return;
if (ql <= l && r <= qr && tr[p].max2 < x)
{
applyChminOnMax(p, x);
return;
}
push(p, l, r);
int m = (l + r) >> 1;
rangeChmin(p << 1, l, m, ql, qr, x);
rangeChmin(p << 1 | 1, m + 1, r, ql, qr, x);
pull(p);
}
// 功能:在结点 p 覆盖的 [l,r] 上执行区间 chmax(x)
void rangeChmax(int p, int l, int r, int ql, int qr, ll x)
{
if (qr < l || r < ql || tr[p].min1 >= x)
return;
if (ql <= l && r <= qr && tr[p].min2 > x)
{
applyChmaxOnMin(p, x);
return;
}
push(p, l, r);
int m = (l + r) >> 1;
rangeChmax(p << 1, l, m, ql, qr, x);
rangeChmax(p << 1 | 1, m + 1, r, ql, qr, x);
pull(p);
}
// 功能:查询 [l,r] 的区间和
ll querySum(int p, int l, int r, int ql, int qr)
{
if (qr < l || r < ql)
return 0LL;
if (ql <= l && r <= qr)
return tr[p].sum;
push(p, l, r);
int m = (l + r) >> 1;
return querySum(p << 1, l, m, ql, qr) + querySum(p << 1 | 1, m + 1, r, ql, qr);
}
// 功能:查询 [l,r] 的当前最大值
ll queryMax(int p, int l, int r, int ql, int qr)
{
if (qr < l || r < ql)
return -(1LL << 60);
if (ql <= l && r <= qr)
return tr[p].max1;
push(p, l, r);
int m = (l + r) >> 1;
ll a = queryMax(p << 1, l, m, ql, qr), b = queryMax(p << 1 | 1, m + 1, r, ql, qr);
return a > b ? a : b;
}
// 功能:查询 [l,r] 的当前最小值
ll queryMin(int p, int l, int r, int ql, int qr)
{
if (qr < l || r < ql)
return (1LL << 60);
if (ql <= l && r <= qr)
return tr[p].min1;
push(p, l, r);
int m = (l + r) >> 1;
ll a = queryMin(p << 1, l, m, ql, qr), b = queryMin(p << 1 | 1, m + 1, r, ql, qr);
return a < b ? a : b;
}
// 功能:查询 [l,r] 的“历史最大值”的最大者
ll queryHistMax(int p, int l, int r, int ql, int qr)
{
if (qr < l || r < ql)
return -(1LL << 60);
if (ql <= l && r <= qr)
return tr[p].histMax;
push(p, l, r);
int m = (l + r) >> 1;
ll a = queryHistMax(p << 1, l, m, ql, qr), b = queryHistMax(p << 1 | 1, m + 1, r, ql, qr);
return a > b ? a : b;
}
};
平衡树
朴素二叉搜索树
算法介绍
朴素二叉搜索树按中序有序地维护键值,插入与删除通过沿着大小关系走到相应位置完成,查找、前驱与后继均可在期望 O(h) 完成,h 为树高。没有随机化或自平衡手段时,最坏会退化到链,比赛中常把它作为讲解用或样例对拍基线。本文代码以指针节点写法实现插入、删除、查找、前驱、后继与按序统计的基本接口,并配合严格注释与构造函数,命名风格与 v3.2 对齐。
常见例题
**题目:**给定若干操作,插入一个整数,删除一个整数,询问一个整数是否存在,查询某数的前驱与后继。
**做法:**用朴素二叉搜索树维护键的集合,插入沿路径找到空位挂上新节点,删除分三类:无子、单子、双子(用后继或前驱替换)。查询、前驱与后继均是沿根向下比较并记录答案。
代码
// 朴素二叉搜索树
// 功能:insert / erase / count / predecessor / successor,支持重复或不重复按需调整
// 复杂度:O(h),h为树高;最坏退化为O(n)
template <typename T>
struct NaiveBST
{
struct Node
{
T key;
int cnt, sz;
Node *left, *right;
// 构造:键key,计数cnt=1,子指针为空
Node(const T &k) : key(k), cnt(1), sz(1), left(nullptr), right(nullptr) {}
};
Node *root;
NaiveBST() : root(nullptr) {}
// 维护以p为根的子树大小
static int size(Node *p) { return p ? p->sz : 0; }
static void pull(Node *p)
{
if (p)
p->sz = p->cnt + size(p->left) + size(p->right);
}
// 插入键k(可重复)
void insert(const T &k)
{
auto go = [&](auto &&self, Node *&p) -> void
{
if (!p)
{
p = new Node(k);
return;
}
if (k == p->key)
{
p->cnt++;
pull(p);
return;
}
if (k < p->key)
self(self, p->left);
else
self(self, p->right);
pull(p);
};
go(go, root);
}
// 查询键k出现次数
int count(const T &k) const
{
Node *p = root;
while (p && p->key != k)
p = k < p->key ? p->left : p->right;
return p ? p->cnt : 0;
}
// 删除一个k(存在时减一,不存在则忽略)
void erase(const T &k)
{
auto go = [&](auto &&self, Node *&p) -> void
{
if (!p)
return;
if (k < p->key)
self(self, p->left);
else if (k > p->key)
self(self, p->right);
else
{
if (p->cnt > 1)
{
p->cnt--;
pull(p);
return;
}
if (!p->left || !p->right)
{
Node *q = p->left ? p->left : p->right;
delete p;
p = q;
}
else
{
Node *s = p->right, *pre = p;
while (s->left)
pre = s, s = s->left;
p->key = s->key;
p->cnt = s->cnt;
s->cnt = 1;
if (pre == p)
self(self, p->right);
else
self(self, pre->left);
}
}
pull(p);
};
go(go, root);
}
// 前驱:严格小于k的最大值,若不存在返回false
bool predecessor(const T &k, T &res) const
{
Node *p = root;
bool ok = false;
while (p)
{
if (k <= p->key)
p = p->left;
else
{
res = p->key, ok = true;
p = p->right;
}
}
return ok;
}
// 后继:严格大于k的最小值,若不存在返回false
bool successor(const T &k, T &res) const
{
Node *p = root;
bool ok = false;
while (p)
{
if (k >= p->key)
p = p->right;
else
{
res = p->key, ok = true;
p = p->left;
}
}
return ok;
}
// 第k小(1-indexed),若不存在返回false
bool kth(int k, T &res) const
{
Node *p = root;
while (p)
{
int ls = size(p->left);
if (k <= ls)
p = p->left;
else if (k <= ls + p->cnt)
{
res = p->key;
return true;
}
else
k -= ls + p->cnt, p = p->right;
}
return false;
}
// 小于等于k的排名(元素计重)
int rankOf(const T &k) const
{
Node *p = root;
int r = 0;
while (p)
{
if (k < p->key)
p = p->left;
else
{
r += size(p->left) + (k >= p->key ? p->cnt : 0);
p = p->right;
}
}
return r;
}
};
Treap
算法介绍
旋转 Treap 在二叉搜索树键序上再叠加一个堆序随机优先级,插入与删除先按键走位,再通过旋转把堆序恢复,从而以期望 O(log n) 保持平衡。它实现简单、常数小,适合在线操作。本文 Treap 提供插入、删除、计数、前驱、后继、kth、rank 接口。
常见例题
**题目:**给定 q 次操作,包含插入 x、删除 x(若有多个仅删一个)、查询 x 是否存在、求 x 的前驱与后继、求有序第 k 小、求 x 的有序排名。
**做法:**用 Treap 维护集合,操作按标准模板实现即可,随机优先级保证期望平衡。
代码
// 旋转 Treap(BST + 堆序,支持重复计数)
// 功能:insert / erase / count / predecessor / successor / kth / rankOf
// 复杂度:期望 O(log n)
template <typename T>
struct Treap
{
struct Node
{
T key;
int pri, cnt, sz;
Node *left, *right;
Node(const T &k, int p) : key(k), pri(p), cnt(1), sz(1), left(nullptr), right(nullptr) {}
};
Node *root;
mt19937 rng;
Treap() : root(nullptr), rng(chrono::steady_clock::now().time_since_epoch().count()) {}
static int size(Node *p) { return p ? p->sz : 0; }
static void pull(Node *p)
{
if (p)
p->sz = p->cnt + size(p->left) + size(p->right);
}
// 右旋
static void rotateRight(Node *&p)
{
Node *q = p->left;
p->left = q->right;
q->right = p;
pull(p);
p = q;
pull(p);
}
// 左旋
static void rotateLeft(Node *&p)
{
Node *q = p->right;
p->right = q->left;
q->left = p;
pull(p);
p = q;
pull(p);
}
// 插入键k
void insert(const T &k)
{
auto go = [&](auto &&self, Node *&p) -> void
{
if (!p)
{
p = new Node(k, (int)rng());
return;
}
if (k == p->key)
{
p->cnt++;
pull(p);
return;
}
if (k < p->key)
{
self(self, p->left);
if (p->left->pri > p->pri)
rotateRight(p);
}
else
{
self(self, p->right);
if (p->right->pri > p->pri)
rotateLeft(p);
}
pull(p);
};
go(go, root);
}
// 删除一个k
void erase(const T &k)
{
auto go = [&](auto &&self, Node *&p) -> void
{
if (!p)
return;
if (k < p->key)
self(self, p->left);
else if (k > p->key)
self(self, p->right);
else
{
if (p->cnt > 1)
{
p->cnt--;
pull(p);
return;
}
if (!p->left || !p->right)
{
Node *q = p->left ? p->left : p->right;
delete p;
p = q;
}
else if (p->left->pri > p->right->pri)
rotateRight(p), self(self, p->right);
else
rotateLeft(p), self(self, p->left);
}
pull(p);
};
go(go, root);
}
// 查询键k的个数
int count(const T &k) const
{
Node *p = root;
while (p && p->key != k)
p = k < p->key ? p->left : p->right;
return p ? p->cnt : 0;
}
// 查询小于k的最大值
bool predecessor(const T &k, T &res) const
{
Node *p = root;
bool ok = false;
while (p)
{
if (k <= p->key)
p = p->left;
else
{
res = p->key, ok = true;
p = p->right;
}
}
return ok;
}
// 查询大于k的最小值
bool successor(const T &k, T &res) const
{
Node *p = root;
bool ok = false;
while (p)
{
if (k >= p->key)
p = p->right;
else
{
res = p->key, ok = true;
p = p->left;
}
}
return ok;
}
// 查询第k小的数
bool kth(int k, T &res) const
{
Node *p = root;
while (p)
{
int ls = size(p->left);
if (k <= ls)
p = p->left;
else if (k <= ls + p->cnt)
{
res = p->key;
return true;
}
else
k -= ls + p->cnt, p = p->right;
}
return false;
}
// 键k的排名,即小于k的数的个数加1
int rankOf(const T &k) const
{
Node *p = root;
int r = 0;
while (p)
{
if (k < p->key)
p = p->left;
else
{
r += size(p->left) + (k >= p->key ? p->cnt : 0);
p = p->right;
}
}
return r;
}
};
无旋 Treap
算法介绍
无旋 Treap(FHQ Treap)通过 split 与 merge 两个基本操作维护平衡,不需要显式旋转。按键分裂把一棵树拆为两棵满足全部键关系的树,按优先级合并时保证堆序,插入等价于先按键分裂再把新点夹在中间合并,删除等价于把等于 k 的那一段拿出来后丢掉再把剩余两段合并。由于结构天然可持久化、实现简短,它在比赛中非常常用。
常见例题
**题目:**与上一节相同的一组集合操作与序统计。
**做法:**用 split(root, ≤k) 与 merge(left, right) 组合完成插入、删除、rank、kth、前驱与后继等所有操作,所有操作期望 O(log n)。
代码
// FHQ Treap(无旋Treap),支持重复计数
// 功能:insert / erase / count / predecessor / successor / kth / rankOf
// 复杂度:期望 O(log n)
template <typename T>
struct FHQTreap
{
struct Node
{
T key;
int pri, cnt, sz;
Node *left, *right;
Node(const T &k, int p) : key(k), pri(p), cnt(1), sz(1), left(nullptr), right(nullptr) {}
};
Node *root;
mt19937 rng;
FHQTreap() : root(nullptr), rng(chrono::steady_clock::now().time_since_epoch().count()) {}
static int size(Node *p) { return p ? p->sz : 0; }
static void pull(Node *p)
{
if (p)
p->sz = p->cnt + size(p->left) + size(p->right);
}
// 合并两棵树,要求所有 left 的键 <= 所有 right 的键
static Node *merge(Node *left, Node *right)
{
if (!left || !right)
return left ? left : right;
if (left->pri > right->pri)
{
left->right = merge(left->right, right);
pull(left);
return left;
}
else
{
right->left = merge(left, right->left);
pull(right);
return right;
}
}
// 按键 k 分裂为 (<=k) 与 (>k)
static pair<Node *, Node *> split(Node *p, const T &k)
{
if (!p)
return {nullptr, nullptr};
if (p->key <= k)
{
auto t = split(p->right, k);
p->right = t.first;
pull(p);
return {p, t.second};
}
else
{
auto t = split(p->left, k);
p->left = t.second;
pull(p);
return {t.first, p};
}
}
// 插入键k
void insert(const T &k)
{
auto lr = split(root, k);
auto ll = split(lr.first, k - 1); // 注意:若T非数值类型,此处应使用另一个split逻辑:先 split(root, k-ε)
if (ll.second && ll.second->key == k)
ll.second->cnt++, pull(ll.second);
else
ll.second = merge(ll.second, new Node(k, (int)rng()));
root = merge(merge(ll.first, ll.second), lr.second);
}
// 若T不是可做 k-1 的类型,上面的“等于段”拆分方式可改用 count() 决策后直接插入到中间
// 删除一个k
void erase(const T &k)
{
auto lr = split(root, k);
auto ll = split(lr.first, k - 1);
if (ll.second)
{
if (ll.second->cnt > 1)
ll.second->cnt--, pull(ll.second);
else
{
delete ll.second;
ll.second = nullptr;
}
}
root = merge(merge(ll.first, ll.second), lr.second);
}
// 次数
int count(const T &k)
{
auto lr = split(root, k);
auto ll = split(lr.first, k - 1);
int c = ll.second ? ll.second->cnt : 0;
root = merge(merge(ll.first, ll.second), lr.second);
return c;
}
// 排名(元素计重):<=k 的个数
int rankOf(const T &k)
{
auto lr = split(root, k);
int ans = size(lr.first);
root = merge(lr.first, lr.second);
return ans;
}
// 第k小(1-indexed)
bool kth(int k, T &res)
{
Node *p = root;
while (p)
{
int ls = size(p->left);
if (k <= ls)
p = p->left;
else if (k <= ls + p->cnt)
{
res = p->key;
return true;
}
else
k -= ls + p->cnt, p = p->right;
}
return false;
}
// 前驱
bool predecessor(const T &k, T &res)
{
auto lr = split(root, k - 1);
if (!lr.first)
{
root = merge(lr.first, lr.second);
return false;
}
Node *p = lr.first;
while (p->right)
p = p->right;
res = p->key;
root = merge(lr.first, lr.second);
return true;
}
// 后继
bool successor(const T &k, T &res)
{
auto lr = split(root, k);
if (!lr.second)
{
root = merge(lr.first, lr.second);
return false;
}
Node *p = lr.second;
while (p->left)
p = p->left;
res = p->key;
root = merge(lr.first, lr.second);
return true;
}
};
说明 若键类型不是整数,k-1 的写法不可用,可把“等于段”的 split 改为先对 k 做一次 split 得到 ≤k 与 >k,然后再对 ≤k 进行“严格小于 k”的 split。实现方式是提供另一个 splitStrict(Node*, const T&),比较时用 < 与 >= 区分。
Splay 树
算法介绍
Splay 通过旋转把最近访问的节点伸展到根,从而获得良好的均摊复杂度。基本操作是单旋与双旋(zig、zig-zig、zig-zag),配合 pushUp 与必要的 pushDown。它可在不记录随机优先级的情况下完成集合所有常用操作。本文实现支持插入、删除、kth、rank、前驱与后继,并给出严格注释与构造函数,接口贴合 v3.2 的常见用法。
常见例题
同 Treap 的集合操作与序统计,要求均摊 O(log n)。做法: splay 到根并在根或相邻子树上继续操作,删除时把要删的键 splay 到根,再把根的左子树最大点伸展到根并接上原右子树。
代码
// Splay 树(集合版,支持重复计数)
// 功能:insert / erase / count / kth / rankOf / predecessor / successor
// 复杂度:均摊 O(log n)
template <typename T>
struct Splay
{
struct Node
{
T key;
int cnt, sz;
Node *fa, *ch[2];
Node(const T &k) : key(k), cnt(1), sz(1), fa(nullptr) { ch[0] = ch[1] = nullptr; }
};
Node *root;
Splay() : root(nullptr) {}
static int size(Node *p) { return p ? p->sz : 0; }
static void pull(Node *p)
{
if (p)
p->sz = p->cnt + size(p->ch[0]) + size(p->ch[1]);
}
// 判断p是否为其父的右儿子
static int dir(Node *p) { return p->fa && p == p->fa->ch[1]; }
// 连接 child 到 parent 的 d 儿子位上
static void connect(Node *child, Node *parent, int d)
{
if (parent)
parent->ch[d] = child;
if (child)
child->fa = parent;
}
// 单次旋转
static void rotate(Node *p)
{
Node *q = p->fa, *g = q->fa;
int d = dir(p), dq = dir(q);
connect(p->ch[d ^ 1], q, d);
connect(q, p, d ^ 1);
if (g)
connect(p, g, dq);
else
p->fa = nullptr;
pull(q), pull(p);
}
// 把p伸展到目标父亲 f(若f为nullptr则伸展到根)
void splay(Node *p, Node *f = nullptr)
{
while (p->fa != f)
{
Node *q = p->fa, *g = q->fa;
if (g != f)
rotate((dir(p) == dir(q)) ? q : p);
rotate(p);
}
if (!f)
root = p;
}
// 在树中查找键k,若存在把该节点伸展到根,若不存在把最后访问节点伸展到根
Node *find(const T &k)
{
Node *p = root, *last = nullptr;
while (p && p->key != k)
last = p, p = k < p->key ? p->ch[0] : p->ch[1];
splay(p ? p : last);
return p;
}
// 插入k(可重复)
void insert(const T &k)
{
if (!root)
{
root = new Node(k);
return;
}
Node *p = root, *fa = nullptr;
while (p && p->key != k)
fa = p, p = k < p->key ? p->ch[0] : p->ch[1];
if (p)
{
p->cnt++, pull(p), splay(p);
}
else
{
Node *x = new Node(k);
if (k < fa->key)
connect(x, fa, 0);
else
connect(x, fa, 1);
splay(x);
}
}
// 计数
int count(const T &k)
{
Node *p = find(k);
return p && p->key == k ? p->cnt : 0;
}
// 前驱(严格小于)
bool predecessor(const T &k, T &res)
{
find(k);
Node *p = root;
if (p && p->key < k)
{
res = p->key;
return true;
}
p = p->ch[0];
if (!p)
return false;
while (p->ch[1])
p = p->ch[1];
res = p->key;
splay(p);
return true;
}
// 后继(严格大于)
bool successor(const T &k, T &res)
{
find(k);
Node *p = root;
if (p && p->key > k)
{
res = p->key;
return true;
}
p = p->ch[1];
if (!p)
return false;
while (p->ch[0])
p = p->ch[0];
res = p->key;
splay(p);
return true;
}
// 第k小(1-indexed)
bool kth(int k, T &res)
{
Node *p = root;
if (!p || k <= 0 || k > size(p))
return false;
while (p)
{
int ls = size(p->ch[0]);
if (k <= ls)
p = p->ch[0];
else if (k <= ls + p->cnt)
{
res = p->key;
splay(p);
return true;
}
else
k -= ls + p->cnt, p = p->ch[1];
}
return false;
}
// 排名(元素计重):<=k 的个数
int rankOf(const T &k)
{
find(k);
int ls = size(root->ch[0]);
return k < root->key ? ls : ls + root->cnt;
}
// 删除一个k(若有多个仅删一个)
void erase(const T &k)
{
Node *p = find(k);
if (!p || p->key != k)
return;
if (p->cnt > 1)
{
p->cnt++;
p->cnt--;
pull(p);
return;
} // 占位以便解释:计数-1
if (!p->ch[0])
{
root = p->ch[1];
if (root)
root->fa = nullptr;
delete p;
return;
}
if (!p->ch[1])
{
root = p->ch[0];
if (root)
root->fa = nullptr;
delete p;
return;
}
Node *x = p->ch[0];
while (x->ch[1])
x = x->ch[1];
splay(x, p); // 把左子树最大点旋到p的左儿子位置
connect(p->ch[1], x, 1); // 接上原右子树
x->fa = nullptr;
root = x;
delete p;
pull(root);
}
};
可持久化数据结构
可持久化数组
算法介绍
可持久化数组以一棵静态结构的“权值存储+路径复制”线段树为载体,每次单点修改时仅复制根到叶子的路径,其他结点复用旧版本指针,因此每次版本增量空间为 O(log n),单点赋值和单点查询均为 O(log n)。所有区间与递归均采用闭区间 [l,r]。本节用 Info 作为叶子所存的元素信息,你可以把它扩展为更复杂的结构,只要定义好合并与赋值即可。
常见例题
**题目:**给定一个长度为 n 的数组以及 q 次操作,操作一为创建新版本并把位置 x 赋值为 v,操作二为在给定版本 ver 上查询位置 x 的值。
**做法:**以初始数组建出第 0 版根,之后每次赋值用路径复制构造新根保存到版本表,查询时在对应版本的根上沿闭区间 [l,r] 逐层定位到叶子读取 Info。
代码
// 可持久化数组(基于路径复制的静态线段树),所有区间为闭区间 [l,r]
// Info 用于存储单点信息与赋值;若仅需数值可用 Info{val}
template <typename T>
struct Info
{
T v;
Info(T _v = T{}) : v(_v) {}
};
// 结点:左右子指针与区间信息
template <typename Info>
struct Node
{
int ls, rs;
Info info;
Node(int l_ = 0, int r_ = 0, const Info &x = Info()) : ls(l_), rs(r_), info(x) {}
};
template <typename Info>
struct PersistentArray
{
int n; // 数组长度
vector<Node<Info>> tr; // 结点池
vector<int> root; // 各版本根
PersistentArray(int n_ = 0) : n(0)
{
if (n_)
init(n_);
}
// 初始化长度并清空版本,默认建立空数组版本0
void init(int n_)
{
n = n_;
tr.clear(), tr.reserve(n * 25);
root.clear();
root.push_back(build(0, n - 1));
}
// 用原始数组a构建版本0
template <typename U>
void buildFrom(const vector<U> &a)
{
n = (int)a.size();
tr.clear(), tr.reserve(n * 25);
root.clear();
auto buildArr = [&](auto &&self, int l, int r) -> int
{
int p = newNode();
if (l == r)
{
tr[p].info = Info(a[l]);
return p;
}
int m = (l + r) >> 1;
tr[p].ls = self(self, l, m), tr[p].rs = self(self, m + 1, r);
return p;
};
root.push_back(buildArr(buildArr, 0, n - 1));
}
// 读取版本ver在位置x的Info
Info get(int ver, int x) const
{
int p = root[ver], l = 0, r = n - 1;
while (l != r)
{
int m = (l + r) >> 1;
x <= m ? p = tr[p].ls, r = m : p = tr[p].rs, l = m + 1;
}
return tr[p].info;
}
// 基于版本ver创建新版本,在位置x赋值为v,返回新版本编号
int setPoint(int ver, int x, const Info &v)
{
int np = copyPath(root[ver], 0, n - 1, x, v);
root.push_back(np);
return (int)root.size() - 1;
}
private:
// 新建空结点
int newNode()
{
tr.emplace_back();
return (int)tr.size() - 1;
}
// 建空树
int build(int l, int r)
{
int p = newNode();
if (l == r)
return p;
int m = (l + r) >> 1;
tr[p].ls = build(l, m), tr[p].rs = build(m + 1, r);
return p;
}
// 从旧根op出发复制到叶子的路径,将位置x改为v,返回新根
int copyPath(int op, int l, int r, int x, const Info &v)
{
int p = newNode();
tr[p] = tr[op];
if (l == r)
{
tr[p].info = v;
return p;
}
int m = (l + r) >> 1;
if (x <= m)
tr[p].ls = copyPath(tr[op].ls, l, m, x, v);
else
tr[p].rs = copyPath(tr[op].rs, m + 1, r, x, v);
return p;
}
};
可持久化线段树
算法介绍
可持久化线段树在“可持久化数组”的基础上增加区间聚合能力。每次单点修改仍是复制路径,但结点信息由左右儿子通过 Info 的合并算子得到,典型是区间和或区间最大。它适合在多版本上做区间查询,例如时光倒流或离线前缀问题。每次修改与查询的时间 O(log n),每个新版本的增量空间 O(log n)。
常见例题
题目: 给定数组与若干版本,每次在上一个版本基础上将坐标 x 的值改为 v 生成新版本,然后询问某一版本 ver 的区间 [l,r] 的区间和。
做法: 定义 Info 存 sum 与合并 operator+,用 copyPath 在叶子更新值并沿路径用合并回溯得到新根,rangeQuery 在给定版本根上按闭区间 [l,r] 递归求解。
代码
// 可持久化线段树
// 特点:支持多版本的单点修改与区间查询,每次修改仅复制根到叶子的路径,其他部分复用旧版本
// 复杂度:单次修改与查询 O(log n),每次修改增量空间 O(log n)
template <typename T>
struct SegInfo
{
T sum; // 区间和,这里示例维护区间和
SegInfo(T v = T{}) : sum(v) {} // 构造函数,默认值为0
static SegInfo empty() { return SegInfo(); } // 提供一个空信息,用于越界或无效区间
friend SegInfo operator+(const SegInfo &a, const SegInfo &b)
{
// 合并两个子区间的结果
return SegInfo(a.sum + b.sum);
}
};
template <typename Info>
struct PersistentSegTree
{
// 结点定义,每个结点存储左右儿子编号与区间信息
struct Node
{
int ls, rs; // 左右儿子在结点数组中的下标
Info info; // 当前结点存储的信息
Node(int l = 0, int r = 0, const Info &x = Info()) : ls(l), rs(r), info(x) {}
};
int n; // 数组长度
vector<Node> tr; // 结点池,所有版本共享
vector<int> root; // 保存每个版本的根节点下标
// 构造函数,默认空,或者直接指定长度
PersistentSegTree(int n_ = 0) : n(0)
{
if (n_)
init(n_);
}
// 构造函数:用数组初始化
template <typename T>
PersistentSegTree(const vector<T> &a) { init(a); }
// 功能:初始化长度 n_ 的空树,版本0为全空
void init(int n_)
{
n = n_;
tr.clear();
tr.reserve(n * 25); // 预留空间,避免频繁扩容
root.clear();
root.push_back(build(0, n - 1)); // 建立初始空版本
}
// 功能:用数组 a 初始化版本0
template <typename T>
void init(const vector<T> &a)
{
n = (int)a.size();
tr.clear();
tr.reserve(n * 25);
root.clear();
auto buildArr = [&](auto &&self, int l, int r) -> int
{
int p = newNode();
if (l == r)
{
tr[p].info = Info(a[l]);
return p;
}
int m = (l + r) >> 1;
tr[p].ls = self(self, l, m), tr[p].rs = self(self, m + 1, r);
tr[p].info = tr[tr[p].ls].info + tr[tr[p].rs].info;
return p;
};
root.push_back(buildArr(buildArr, 0, n - 1));
}
// 功能:在版本 ver 上修改位置 x 的值为 v,生成一个新版本,返回新版本编号
int modify(int ver, int x, const Info &v)
{
int np = copyPath(root[ver], 0, n - 1, x, v);
root.push_back(np);
return (int)root.size() - 1;
}
// 功能:在版本 ver 上查询区间 [L,R] 的信息
Info rangeQuery(int ver, int L, int R) const
{
return query(root[ver], 0, n - 1, L, R);
}
private:
// 功能:新建一个空结点,返回编号
int newNode()
{
tr.emplace_back();
return (int)tr.size() - 1;
}
// 功能:建立空树,区间 [l,r],所有 Info 都为空
int build(int l, int r)
{
int p = newNode();
if (l == r)
return p;
int m = (l + r) >> 1;
tr[p].ls = build(l, m), tr[p].rs = build(m + 1, r);
return p;
}
// 功能:在旧版本结点 op 的基础上复制路径,将位置 x 更新为 v,返回新根
int copyPath(int op, int l, int r, int x, const Info &v)
{
int p = newNode();
tr[p] = tr[op]; // 复制当前结点
if (l == r)
{
tr[p].info = v;
return p;
} // 到达叶子,直接更新值
int m = (l + r) >> 1;
if (x <= m)
tr[p].ls = copyPath(tr[op].ls, l, m, x, v);
else
tr[p].rs = copyPath(tr[op].rs, m + 1, r, x, v);
tr[p].info = tr[tr[p].ls].info + tr[tr[p].rs].info; // 回溯更新信息
return p;
}
// 功能:在根 p 表示的区间 [l,r] 上查询 [L,R],返回区间信息
Info query(int p, int l, int r, int L, int R) const
{
if (R < l || r < L)
return Info::empty(); // 无交集返回空信息
if (L <= l && r <= R)
return tr[p].info; // 完全覆盖直接返回
int m = (l + r) >> 1;
return query(tr[p].ls, l, m, L, R) + query(tr[p].rs, m + 1, r, L, R);
}
};
可持久化 Trie
算法介绍
可持久化 Trie 通过“复制外加复用未修改分支”的方式,为每次插入生成一个新根,从而得到版本化的字典树。典型是二进制 0/1 Trie,用来支持“在某个版本内求区间最大异或”等查询。每次插入的增量空间为 O(bit),查询为 O(bit),bit 是键的位数或字符集深度。所有查询都在指定版本的根上进行。
常见例题
**题目:**给定 q 次操作,其中 insert v 创建一个新版本并在该版本中插入整数 v,query ver x 要求在版本 ver 的集合内求与 x 的最大异或值。
**做法:**采用 0/1 可持久化 Trie,版本 i 的根源于版本 i−1 的根,只在经过的位路径上复制新结点。最大异或查询时从高位到低位优先选择与 x 当前位相反的分支,如果不存在则走相同分支。
代码
// 可持久化 01-Trie,按位插入与查询最大异或,支持版本化根
struct Persistent01Trie
{
struct Node
{
int ch[2];
int cnt;
Node() : ch{0, 0}, cnt(0) {}
};
int W; // 位宽(例如31表示非负int)
vector<Node> tr; // 结点池
vector<int> root; // 各版本根
Persistent01Trie(int bitWidth = 31) : W(bitWidth) { tr.reserve(1 << 20), root.push_back(newNode()); }
// 在版本ver的基础上插入值x,返回新版本编号
int insert(int ver, int x)
{
int np = copyPath(root[ver], x);
root.push_back(np);
return (int)root.size() - 1;
}
// 在版本ver上查询与x的最大异或
int maxXor(int ver, int x) const
{
int p = root[ver], res = 0;
for (int i = W; i >= 0; i--)
{
int b = (x >> i) & 1, t = b ^ 1, nxt = tr[p].ch[t];
if (nxt && tr[nxt].cnt)
p = nxt, res |= (1 << i);
else
p = tr[p].ch[b];
}
return res;
}
private:
int newNode()
{
tr.emplace_back();
return (int)tr.size() - 1;
}
// 基于旧根op复制插入x路径,返回新根
int copyPath(int op, int x)
{
int rp = newNode(), p = rp;
tr[p] = tr[op], tr[p].cnt++;
for (int i = W; i >= 0; i--)
{
int b = (x >> i) & 1;
int old = tr[op].ch[b];
tr[p].ch[b] = newNode();
tr[tr[p].ch[b]] = old ? tr[old] : Node();
p = tr[p].ch[b], tr[p].cnt++;
op = old;
}
return rp;
}
};
莫队
朴素莫队
算法介绍
把所有区间询问离线,按照块排序规则重排后,用一条指针链在数组上滑动维护当前答案。当前区间 [L,R] 与目标区间 [l,r] 的差异通过增删元素完成状态转移,整体复杂度约为 O((n+q)√n),常数小、实现稳定,特别适合可加可删、难以在线维护的数据统计类问题。本模板与现有 v3.2 的风格保持一致,命名、注释与 I/O 习惯完全对齐。
常见例题
**题目:**给定长度为 n 的数组与 q 次询问,询问区间 [l,r] 内不同数的个数。
**做法:**在增删函数中用计数桶维护每个值出现次数,增加某个值从 0 到 1 时答案加一,减少某个值从 1 到 0 时答案减一,其他情况不变,按照莫队顺序依次处理即可。
代码
// 朴素莫队(闭区间 [l,r] 语义)
// 功能:离线重排区间询问,通过增删元素维护答案;支持任意可“增删”的统计
// 复杂度:排序 O(q log q),移动指针与增删 O((n+q) * 块长);常取块长 ≈ n/√q 或 √n
struct Mo
{
struct Query
{
int l, r, id; // 闭区间 [l,r] 与原始编号
Query() {}
Query(int l_, int r_, int id_) : l(l_), r(r_), id(id_) {}
};
int n; // 数组长度
int blk; // 分块大小
vector<int> a; // 原数组(1-indexed 更顺手,本模板用 1..n)
vector<Query> qs; // 询问列表
// 构造与初始化,支持空构造或直接给定数组
Mo() : n(0), blk(1) {}
Mo(const vector<int> &arr) { init(arr); }
// 设置数组并初始化块长
void init(const vector<int> &arr)
{
n = (int)arr.size() - 1; // 约定arr从1开始;若从0开始请在外部补一个哨兵
a = arr;
blk = max(1, (int)sqrt(max(1, n)));
qs.clear();
}
// 添加一个闭区间询问
void addQuery(int l, int r, int id) { qs.emplace_back(l, r, id); }
// 主过程,传入四个操作与一个取答函数
// addL(x):把下标 x 插入到当前区间左侧;addR(x):把下标 x 插入到当前区间右侧
// delL(x):把下标 x 从当前区间左侧删除;delR(x):把下标 x 从当前区间右侧删除
// getAns():返回当前区间的答案
template<class AddL, class AddR, class DelL, class DelR, class Get>
vector<ll> solve(AddL addL, AddR addR, DelL delL, DelR delR, Get getAns)
{
vector<int> ord(qs.size());
iota(ord.begin(), ord.end(), 0);
auto key = [&](int x) -> pair<int,int>
{
int b = qs[x].l / blk;
int t = qs[x].r;
if (b & 1) t = -t;
return {b, t};
};
sort(ord.begin(), ord.end(), [&](int i, int j)
{
auto ki = key(i), kj = key(j);
if (ki.first != kj.first) return ki.first < kj.first;
return ki.second < kj.second;
});
vector<ll> ans(qs.size());
int L = 1, R = 0;
for (int idx : ord)
{
int l = qs[idx].l, r = qs[idx].r;
while (L > l) addL(--L);
while (R < r) addR(++R);
while (L < l) delL(L++);
while (R > r) delR(R--);
ans[qs[idx].id] = getAns();
}
return ans;
}
};
// 使用示例:区间不同数个数
// 说明:cnt[val] 统计 val 在当前 [L,R] 出现次数,cur 为当前不同数个数
// 注意:请先把原数组压缩到 [1..M],避免值域过大
struct MoDistinctDemo
{
vector<int> a; // 1..n
vector<int> cnt; // 值域计数
ll cur; // 当前答案(不同数个数)
MoDistinctDemo() : cur(0) {}
void attachArray(const vector<int> &arr, int maxVal)
{
a = arr; cnt.assign(maxVal + 1, 0); cur = 0;
}
// 把下标 x 的元素加入左侧
void addL(int x)
{
int v = a[x];
if (cnt[v] == 0) cur++;
cnt[v]++;
}
// 把下标 x 的元素加入右侧
void addR(int x)
{
int v = a[x];
if (cnt[v] == 0) cur++;
cnt[v]++;
}
// 把下标 x 的元素从左侧删除
void delL(int x)
{
int v = a[x];
cnt[v]--;
if (cnt[v] == 0) cur--;
}
// 把下标 x 的元素从右侧删除
void delR(int x)
{
int v = a[x];
cnt[v]--;
if (cnt[v] == 0) cur--;
}
ll getAns() { return cur; }
};
带修莫队
算法介绍
在朴素莫队的两维移动之外再引入“时间维”,把修改操作当作第三维度的离线事件。每个询问记录它发生时刻之前已生效的修改次数 t,排序按块顺序比较 (l/blk, r/blk, t/blk)。维护当前 [L,R] 与当前时刻 T,两端指针通过增删移动,时间指针通过“应用一次修改”或“撤销一次修改”在 T 与目标 t 之间移动即可。复杂度常见取块长为 n^(2/3) 达到 O(n^(5/3)) 左右的实践效果。
常见例题
**题目:**给定数组初值,包含两类操作,类型一是把位置 x 的值改为 v,类型二是询问区间 [l,r] 内不同数的个数,要求按照输入顺序输出所有询问答案。
**做法:**将所有操作离线,询问带上当前已执行的修改数 t,排序后用三指针维护当前 L、R、T,时间前进就把修改位置的旧值替换为新值并对其是否位于 [L,R] 做一次增删,时间后退则反向应用。
代码
// 带修莫队(闭区间 [l,r] 语义;时间维支持“点修改”)
// 功能:同时维护 [L,R] 与时间 T;支持“应用修改/撤销修改”的回滚式增删
struct MoWithUpdate
{
struct Query
{
int l, r, t, id; // 询问区间与发生时的修改数 t
Query() {}
Query(int l_, int r_, int t_, int id_) : l(l_), r(r_), t(t_), id(id_) {}
};
struct Update
{
int pos, pre, now; // 把 pos 处的值从 pre 改为 now
Update() {}
Update(int pos_, int pre_, int now_) : pos(pos_), pre(pre_), now(now_) {}
};
int n; // 数组长度
int blk; // 分块大小(常取 n^(2/3))
vector<int> a; // 当前数组(1..n)
vector<Query> qs; // 询问集
vector<Update> us; // 修改集
MoWithUpdate() : n(0), blk(1) {}
MoWithUpdate(const vector<int> &arr) { init(arr); }
void init(const vector<int> &arr)
{
n = (int)arr.size() - 1;
a = arr;
blk = max(1, (int)pow(max(1, n), 2.0 / 3.0));
qs.clear(); us.clear();
}
void addQuery(int l, int r, int t, int id) { qs.emplace_back(l, r, t, id); }
void addUpdate(int pos, int pre, int now) { us.emplace_back(pos, pre, now); }
template<class Add, class Del, class Get>
vector<ll> solve(Add addPos, Del delPos, Get getAns)
{
vector<int> ord(qs.size());
iota(ord.begin(), ord.end(), 0);
auto key = [&](int i) -> tuple<int,int,int>
{
int b1 = qs[i].l / blk, b2 = qs[i].r / blk, b3 = qs[i].t / blk;
return make_tuple(b1, b2, b3);
};
sort(ord.begin(), ord.end(), [&](int i, int j){ return key(i) < key(j); });
vector<ll> ans(qs.size());
int L = 1, R = 0, T = 0;
auto apply = [&](int k, bool forward)
{
int p = us[k].pos;
int valFrom = forward ? us[k].pre : us[k].now;
int valTo = forward ? us[k].now : us[k].pre;
if (L <= p && p <= R) delPos(p); // 先移除旧值的贡献
a[p] = valTo;
if (L <= p && p <= R) addPos(p); // 再加入新值的贡献
};
for (int idx : ord)
{
int l = qs[idx].l, r = qs[idx].r, t = qs[idx].t;
while (T < t) apply(T++, true);
while (T > t) apply(--T, false);
while (L > l) addPos(--L);
while (R < r) addPos(++R);
while (L < l) delPos(L++);
while (R > r) delPos(R--);
ans[qs[idx].id] = getAns();
}
return ans;
}
};
// 使用示例:区间不同数个数(与朴素莫队相同的增删逻辑)
// 说明:对时间应用时,只在修改位置恰好处于 [L,R] 时做一次“先删后加”的切换
struct MoUpdDistinctDemo
{
vector<int> a; // 由外部 MoWithUpdate::a 引用或同步
vector<int> cnt;
ll cur;
void attachArray(const vector<int> &arr, int maxVal)
{
a = arr; cnt.assign(maxVal + 1, 0); cur = 0;
}
void addPos(int i)
{
int v = a[i];
if (cnt[v] == 0) cur++;
cnt[v]++;
}
void delPos(int i)
{
int v = a[i];
cnt[v]--;
if (cnt[v] == 0) cur--;
}
ll getAns() { return cur; }
};
树上莫队
算法介绍
把树做一次欧拉序或 dfs 序展开到序列上,再把每条路径询问映射为序列区间。令 tin[u] 表示 u 第一次被访问的时间戳,tout[u] 表示离开时间戳。常见做法是“每个节点出现两次”的欧拉序,与“访问一次即切换一次选中状态”的思想配合,路径 u 到 v 可以转化为区间 [tin[u], tin[v]] 的若干“开关”加上额外处理的最近公共祖先 lca(u,v)。当指针移动经过某个结点的时间戳时,若该结点此前未被选中则加入贡献,否则移除贡献。复杂度与朴素莫队同阶,块长通常取 √(2n) 以匹配欧拉序的长度。
常见例题
**题目:**给定一棵带颜色的树,有 q 次询问,每次询问路径 [u,v] 上不同颜色的个数。
**做法:**对树做欧拉序并预处理二进制 LCA。把每次询问规范化为 tin[u] 与 tin[v] 的区间并记录 lca,莫队增删时对经过的结点进行“开关”,当一个结点从未选中切为选中时把其颜色计数加一并可能带来答案变化,反之移除贡献;若 lca 不在区间内,最后再特判把 lca 的颜色临时加入一次统计得到答案。
代码
// 树上莫队(闭区间 [l,r] 语义,欧拉序长度为 2n)
// 功能:把树上路径询问转化为序列莫队;支持“开关式”可加可删的统计
struct TreeMo
{
int n, lg; // n 个点,lg 为 LCA 对数深度
vector<vector<int>> g; // 邻接表
vector<int> color; // 每个结点的颜色(或值),1..n
vector<int> euler, in, out, ver; // euler 为 1..2n 的时间戳序列;ver[t] = 该时间戳对应结点
vector<int> depth; // 深度
vector<vector<int>> up; // LCA 二进制跳表
// 统计部分
vector<int> vis; // 结点是否已被“选中”
vector<int> cnt; // 颜色出现次数
ll cur; // 当前答案
// 构造与初始化
TreeMo() : n(0), lg(0), cur(0) {}
TreeMo(int n_) { init(n_); }
// 初始化图结构与容器
void init(int n_)
{
n = n_;
g.assign(n + 1, {});
color.assign(n + 1, 0);
in.assign(n + 1, 0);
out.assign(n + 1, 0);
depth.assign(n + 1, 0);
ver.assign(2 * n + 2, 0);
euler.assign(2 * n + 2, 0);
lg = __lg(max(1, n)) + 1;
up.assign(lg, vector<int>(n + 1, 0));
vis.assign(n + 1, 0);
cnt.clear();
cur = 0;
}
// 加边
void addEdge(int u, int v)
{
g[u].push_back(v), g[v].push_back(u);
}
// 预处理欧拉序与 LCA,根默认 1
void build(int root = 1)
{
int timer = 0;
auto dfs = [&](auto &&self, int u, int p) -> void
{
up[0][u] = p;
for (int k = 1; k < lg; k++) up[k][u] = up[k - 1][up[k - 1][u]];
in[u] = ++timer; ver[timer] = u; euler[timer] = u;
for (int v : g[u]) if (v != p) depth[v] = depth[u] + 1, self(self, v, u);
out[u] = ++timer; ver[timer] = u; euler[timer] = u;
};
dfs(dfs, root, 0);
}
// LCA
int lca(int a, int b)
{
if (depth[a] < depth[b]) swap(a, b);
int d = depth[a] - depth[b];
for (int k = 0; k < lg; k++) if (d >> k & 1) a = up[k][a];
if (a == b) return a;
for (int k = lg - 1; k >= 0; k--) if (up[k][a] != up[k][b]) a = up[k][a], b = up[k][b];
return up[0][a];
}
// 开关一个结点 u 的贡献
void toggle(int u)
{
int c = color[u];
if (!vis[u]) { vis[u] = 1; if ((int)cnt.size() <= c) cnt.resize(c + 1, 0); if (cnt[c] == 0) cur++; cnt[c]++; }
else { vis[u] = 0; cnt[c]--; if (cnt[c] == 0) cur--; }
}
// 询问结构
struct Query
{
int l, r, id, w; // 映射到欧拉序的闭区间 [l,r];w = lca(u,v),若 w 在 [l,r] 外需特判
Query() {}
Query(int l_, int r_, int id_, int w_) : l(l_), r(r_), id(id_), w(w_) {}
};
// 把树上路径 (u,v) 转化为欧拉序区间
Query makeQuery(int u, int v, int id)
{
if (in[u] > in[v]) swap(u, v);
int w = lca(u, v);
if (w == u) return Query(in[u], in[v], id, 0);
return Query(out[u], in[v], id, w);
}
// 求解所有查询,返回答案数组
vector<ll> solve(vector<Query> qs)
{
int m = (int)qs.size();
int B = max(1, (int)sqrt(2 * n));
sort(qs.begin(), qs.end(), [&](const Query &A, const Query &Bq)
{
int blA = A.l / B, blB = Bq.l / B;
if (blA != blB) return blA < blB;
int brA = A.r / B, brB = Bq.r / B;
if (brA != brB) return brA < brB;
return A.w < Bq.w;
});
vector<ll> ans(m);
int L = 1, R = 0;
auto move = [&](int x)
{
int u = ver[x];
toggle(u);
};
for (auto &q : qs)
{
while (L > q.l) move(--L);
while (R < q.r) move(++R);
while (L < q.l) move(L++);
while (R > q.r) move(R--);
ll now = cur;
if (q.w) { toggle(q.w); now = cur; toggle(q.w); }
ans[q.id] = now;
}
return ans;
}
};
// 使用示例说明:
// 1) 读入树与颜色,调用 init(n)、addEdge、build(root);
// 2) 对每个询问(u,v) 调用 makeQuery(u,v,id) 收集为数组;
// 3) 调用 solve(queries) 获得答案数组;
// 4) 若统计目标不是“不同颜色个数”,仅需在 toggle 中按目标逻辑增删贡献即可。
杂项
跳表
算法介绍
跳表是带“多级索引”的有序链表。每个节点在若干层出现,上层稀疏、下层稠密。查找从最高层逐层下降,遇到超越目标就下移一层继续,期望时间复杂度 O(log n),插入与删除沿搜索路径回溯并按抛硬币或固定随机高度将新节点接入各层。它常被用作比赛中的有序集合备胎,在无法使用平衡树、或者需要自己掌控比较与持久化细节的题目中很便利。
常见例题
**题目:**维护一个动态集合,支持插入一个键、删除一个键、查询一个键是否存在、查询严格小于 x 的最大元素。**做法:**用跳表维护严格递增的键序列。插入时按搜索路径记录每层的“前驱”,随机高度后把新节点挂在各层前驱之后。删除时同理断链。查询前驱时从顶层右移直到下一个大于等于 x,再落到下一层反复,最终在最底层拿到严格小于 x 的位置。
代码
// 跳表(Skip List),支持 insert / erase / contains / predecessor
// 说明:模板参数K为可比较的键,严格弱序(operator<);随机层高采用几何分布
template<typename K>
struct SkipList
{
struct Node
{
K key; // 键
vector<Node*> nxt; // 各层后继指针
Node(int h, const K &k): key(k), nxt(h, nullptr) {} // 构造:给定高度与键
};
int maxLevel; // 允许的最大层数
double prob; // 上升到更高一层的概率
Node *head; // 头哨兵(-inf)
mt19937_64 rng; // 随机数
uniform_real_distribution<double> uni;
// 构造:指定最大层与晋升概率p
SkipList(int maxLevel_ = 20, double p_ = 0.5)
: maxLevel(maxLevel_), prob(p_), head(new Node(maxLevel, K{})), rng(chrono::steady_clock::now().time_since_epoch().count()), uni(0.0, 1.0) {}
// 产生一个随机高度(至少1)
int randomLevel()
{
int h = 1;
while (h < maxLevel && uni(rng) < prob) h++;
return h;
}
// 查找严格小于x的前驱结点数组,用于插入/删除
void findPrev(const K &x, vector<Node*> &prev)
{
prev.assign(maxLevel, nullptr);
Node *p = head;
for (int lvl = maxLevel - 1; lvl >= 0; lvl--)
{
while (p->nxt[lvl] && p->nxt[lvl]->key < x) p = p->nxt[lvl];
prev[lvl] = p;
}
}
// 是否包含键x
bool contains(const K &x)
{
Node *p = head;
for (int lvl = maxLevel - 1; lvl >= 0; lvl--)
while (p->nxt[lvl] && p->nxt[lvl]->key < x) p = p->nxt[lvl];
p = p->nxt[0];
return p && !(x < p->key) && !(p->key < x);
}
// 插入键x,若已存在则不重复插
bool insert(const K &x)
{
vector<Node*> prev; findPrev(x, prev);
Node *q = prev[0]->nxt[0];
if (q && !(x < q->key) && !(q->key < x)) return false;
int h = randomLevel(); Node *cur = new Node(h, x);
for (int i = 0; i < h; i++) cur->nxt[i] = prev[i]->nxt[i], prev[i]->nxt[i] = cur;
return true;
}
// 删除键x,若不存在返回false
bool erase(const K &x)
{
vector<Node*> prev; findPrev(x, prev);
Node *q = prev[0]->nxt[0];
if (!q || (x < q->key) || (q->key < x)) return false;
for (int i = 0; i < (int)q->nxt.size(); i++) prev[i]->nxt[i] = q->nxt[i];
delete q; return true;
}
// 返回严格小于x的最大元素是否存在并写入out
bool predecessor(const K &x, K &out)
{
Node *p = head;
for (int lvl = maxLevel - 1; lvl >= 0; lvl--)
while (p->nxt[lvl] && p->nxt[lvl]->key < x) p = p->nxt[lvl];
if (p == head) return false;
out = p->key; return true;
}
};
ST表
算法介绍
ST 表(Sparse Table)在静态数组上实现 O(1) RMQ 或可幂等函数的区间查询。预处理 f[i][k] 表示区间 [i,i+2^k-1] 的信息,合并时用幂等函数(如 min、max、gcd)。查询 [l,r] 时令 k=⌊log2(r−l+1)⌋,答案由 f[l][k] 与 f[r−2^k+1][k] 合并即可。由于只做一遍静态预处理,适合没有修改、查询极多的场景。
常见例题
**题目:**给定数组 a 与 q 次询问,每次询问区间 [l,r] 的最小值与最大值。**做法:**以 min 和 max 为幂等函数分别建两张 ST 表,查询时按区间长度取 k,并在两张表各做一次合并即可返回答案。
代码
// ST 表(Sparse Table)
// 说明:Info 需要提供默认构造与从值构造,以及 operator+ 作为“区间合并”
// 对于 RMQ,Info::operator+ 可写成 min/max;对于 gcd/按位与或,同理
// 复杂度:预处理 O(n log n),单次查询 O(1)
template <typename Info>
struct SparseTable
{
int n; // 原数组长度
int maxK; // 最高位 k,使得 2^k <= n
vector<int> lg; // 预存 log2
vector<vector<Info>> f; // f[k][i] 表示区间 [i, i+2^k-1] 的信息
// 构造函数:空构造
SparseTable(): n(0), maxK(0) {}
// 构造函数:从值数组 a 构建(a 为 0-based)
template <typename T>
SparseTable(const vector<T> &a) { init(a); }
// 功能:从值数组 a 初始化,完成整张稀疏表的预处理
template <typename T>
void init(const vector<T> &a)
{
n = (int)a.size();
if (!n) { maxK = 0; f.clear(); lg.clear(); return; }
maxK = __lg(n);
lg.resize(n + 1);
lg[1] = 0;
for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
f.assign(maxK + 1, vector<Info>(n));
for (int i = 0; i < n; i++) f[0][i] = Info(a[i]);
for (int k = 1; k <= maxK; k++)
{
int len = 1 << k, half = len >> 1, lim = n - len + 1;
for (int i = 0; i < lim; i++) f[k][i] = f[k - 1][i] + f[k - 1][i + half];
}
}
// 功能:区间查询,返回闭区间 [l,r] 的 Info 聚合
Info query(int l, int r)
{
assert(0 <= l && l <= r && r < n);
int k = lg[r - l + 1], len = 1 << k;
return f[k][l] + f[k][r - len + 1];
}
};
// 下面给出常用的三个 Info 示例,均以 ll 为底层值。
// 你可以按需替换底层类型或运算,并与 SparseTable 一起直接使用。
// 区间最小值
struct InfoMin
{
ll v;
InfoMin(ll x = (ll)4e18): v(x) {}
friend InfoMin operator+(const InfoMin &a, const InfoMin &b) { return InfoMin(min(a.v, b.v)); }
};
// 区间最大值
struct InfoMax
{
ll v;
InfoMax(ll x = (ll)-4e18): v(x) {}
friend InfoMax operator+(const InfoMax &a, const InfoMax &b) { return InfoMax(max(a.v, b.v)); }
};
// 区间 gcd(注意 0 的幺元语义:gcd(x,0)=x)
struct InfoGcd
{
ll v;
InfoGcd(ll x = 0): v(x) {}
static ll g(ll a, ll b)
{
while (b) a %= b, swap(a, b);
return a;
}
friend InfoGcd operator+(const InfoGcd &a, const InfoGcd &b) { return InfoGcd(g(a.v, b.v)); }
};
// 使用示例:
// vector<ll> a = {...};
// SparseTable<InfoMin> stMin(a); auto mn = stMin.query(l, r).v;
// SparseTable<InfoMax> stMax(a); auto mx = stMax.query(l, r).v;
// SparseTable<InfoGcd> stGcd(a); auto gd = stGcd.query(l, r).v;
线性基
算法介绍
线性基用于在 GF(2) 上维护一组数的异或线性独立基。插入一个数时按最高位到最低位试图“消去”已有基向量;若最终非零则作为新基放入。它能在 O(log A) 时间内支持插入与询问可得到的最大异或和;若额外维护“可取到零”的标记,还可回答是否能异或成给定值。比赛中常用于最大异或对、与图上“环异或”相关的可达值分析。
常见例题
**题目:**给一个数组,支持插入一个数与询问当前集合能得到的最大异或和。**做法:**线性基顺序插入,查询时从高位到低位尝试贪心改进答案即可。若题目要求“是否能异或得到某值”,则在插入阶段不丢弃与现有基完全消去成零的元素,并把 canZero 置真,判断时先用基消去目标,再看是否变成零或 canZero 为真。
代码
// 线性基(Binary Linear Basis),支持插入与最大异或和查询,可选支持判断可达性
template<typename T = unsigned ll, int MAXL = 63>
struct LinearBasis
{
array<T, MAXL + 1> b; // b[i] 存放最高位在 i 的基向量
bool canZero; // 是否可以异或出 0(存在冗余向量)
// 构造:清空基
LinearBasis() { clear(); }
// 清空
void clear()
{
b.fill(0);
canZero = false;
}
// 插入一个向量 x;返回是否扩大了线性空间
bool insert(T x)
{
for (int i = MAXL; i >= 0; i--)
{
if (!((x >> i) & 1)) continue;
if (!b[i]) { b[i] = x; return true; }
x ^= b[i];
}
canZero = true; return false;
}
// 查询能得到的最大异或和
T maxXor()
{
T ans = 0;
for (int i = MAXL; i >= 0; i--) if ((ans ^ b[i]) > ans) ans ^= b[i];
return ans;
}
// 判断是否能异或得到给定值 x
bool canMake(T x)
{
for (int i = MAXL; i >= 0; i--) if ((x >> i) & 1) x ^= b[i] ? b[i] : 0;
return x == 0 || canZero;
}
// 合并另一个线性基(把对方所有基向量插入本基)
void mergeFrom(const LinearBasis &o)
{
for (int i = MAXL; i >= 0; i--) if (o.b[i]) insert(o.b[i]);
if (o.canZero) canZero = true;
}
};
Sqrt Tree
算法介绍
Sqrt Tree 是一种利用“分治 + 分块 + 预处理”思想的数据结构,用来在接近 O(1) 的时间内回答 区间可结合运算 查询。其核心做法是:
- 把数组分层,每一层的块长大约是上层块长的平方根。
- 每层块内预处理所有前缀和后缀结果,用于回答跨块的子段查询。
- 在层与层之间,预处理“整块到整块”的聚合表(between),用于快速合并中间整块。
- 查询 [l,r] 时,找到合适的层,使得 [l,r] 覆盖了至少两个完整块。答案由三部分组成:左端块的后缀、右端块的前缀、中间整块的聚合。
- 更新时,修改元素所在的块并在各层递归重建相关的前缀/后缀和 between 表。
整个结构对区间的查询近似 O(1),单点修改大约 O(√n)。其优势是常数小,实现纯静态时几乎是最优的区间 RMQ/区间和解法之一。所有区间均视作闭区间 [l,r]。
常见例题
**题目:**给定长度为 n 的数组 a,q 次操作:
1 l r —— 询问区间 [l,r] 的最小值
2 x v —— 把位置 x 的元素改为 v
做法: 定义 struct Info { int val; … },operator+ 定义为取两个 Info 的最小值,Info() 的默认构造返回 +∞ 作为幺元。构建 SqrtTree 后,每次操作一直接调用 query(l,r) 得到区间最小值;操作二调用 update(x,Info(v)) 完成单点修改。由于查询接近 O(1),更新 O(√n),整体能轻松应对 q,n=10^5 级别的数据规模。
代码
// Sqrt Tree(闭区间 [l,r] 查询 + 单点修改)
struct Info
{
ll val; // 示例:区间和;若你要改成 min/max/gcd,请相应修改默认构造与 operator+
Info(ll v = 0) : val(v) {}
friend Info operator+(const Info &a, const Info &b) { return Info(a.val + b.val); }
};
struct SqrtTree
{
private:
int n; // 原数组长度
int lg; // 2^lg >= n 的最小 lg
int indexSz; // 顶层索引层大小(把每块代表拼接到尾部)
vector<Info> v; // 底层数组 + 索引层拼接后的数组,长度 n + indexSz
vector<int> clz; // 用来 O(1) 定位最高位的预处理表
vector<int> layers; // 每一层的对数规模(块大小的 log2)
vector<int> onLayer; // 把某个 log 值映射到所属层下标
vector<vector<Info>> pref; // pref[layer][i]:该层内块的 L..i 前缀聚合
vector<vector<Info>> suf; // suf[layer][i] :该层内块的 i..R 后缀聚合
vector<vector<Info>> between; // between[layer-1][ofs + block(i,j)]:整块 i..j 的聚合
// 功能:在指定层 layer 的一个块 [l,r) 上构建块内前/后缀
void buildBlock(int layer, int l, int r)
{
pref[layer][l] = v[l];
for (int i = l + 1; i < r; i++)
pref[layer][i] = pref[layer][i - 1] + v[i];
suf[layer][r - 1] = v[r - 1];
for (int i = r - 2; i >= l; i--)
suf[layer][i] = v[i] + suf[layer][i + 1];
}
// 功能:为除顶层以外的层构建“层间整块聚合”表
void buildBetween(int layer, int lBound, int rBound, int betweenOffs)
{
int bSzLog = (layers[layer] + 1) >> 1;
int bCntLog = layers[layer] >> 1;
int bSz = 1 << bSzLog;
int bCnt = (rBound - lBound + bSz - 1) >> bSzLog;
for (int i = 0; i < bCnt; i++)
{
Info ans = suf[layer][lBound + (i << bSzLog)];
between[layer - 1][betweenOffs + lBound + (i << bCntLog) + i] = ans;
for (int j = i + 1; j < bCnt; j++)
{
Info add = suf[layer][lBound + (j << bSzLog)];
ans = ans + add;
between[layer - 1][betweenOffs + lBound + (i << bCntLog) + j] = ans;
}
}
}
// 功能:为顶层构建索引层,把每个块的“块首后缀”抽到 v 尾部
void buildBetweenZero()
{
int bSzLog = (lg + 1) >> 1;
for (int i = 0; i < indexSz; i++)
v[n + i] = suf[0][i << bSzLog];
build(1, n, n + indexSz, (1 << lg) - n);
}
// 功能:当顶层某个块变更时,刷新其对应的索引元素并沿层维护
void updateBetweenZero(int bid)
{
int bSzLog = (lg + 1) >> 1;
v[n + bid] = suf[0][bid << bSzLog];
update(1, n, n + indexSz, (1 << lg) - n, n + bid);
}
// 功能:递归构建层 layer 管辖区间 [lBound,rBound),并继续对子层构建
void build(int layer, int lBound, int rBound, int betweenOffs)
{
if (layer >= (int)layers.size())
return;
int bSz = 1 << ((layers[layer] + 1) >> 1);
for (int l = lBound; l < rBound; l += bSz)
{
int r = min(l + bSz, rBound);
buildBlock(layer, l, r);
build(layer + 1, l, r, betweenOffs);
}
if (layer == 0)
buildBetweenZero();
else
buildBetween(layer, lBound, rBound, betweenOffs);
}
// 功能:递归更新,将位置 x 所在块的块内信息与层间整块聚合重建
void update(int layer, int lBound, int rBound, int betweenOffs, int x)
{
if (layer >= (int)layers.size())
return;
int bSzLog = (layers[layer] + 1) >> 1;
int bSz = 1 << bSzLog;
int blockIdx = (x - lBound) >> bSzLog;
int l = lBound + (blockIdx << bSzLog);
int r = min(l + bSz, rBound);
buildBlock(layer, l, r);
if (layer == 0)
updateBetweenZero(blockIdx);
else
buildBetween(layer, lBound, rBound, betweenOffs);
update(layer + 1, l, r, betweenOffs, x);
}
// 功能:内部查询,闭区间 [l,r],betweenOffs 为层间偏移,base 为该层基底
Info query(int l, int r, int betweenOffs, int base)
{
if (l == r)
return v[l];
if (l + 1 == r)
return v[l] + v[r];
int layer = onLayer[clz[(l - base) ^ (r - base)]];
int bSzLog = (layers[layer] + 1) >> 1;
int bCntLog = layers[layer] >> 1;
int lBound = (((l - base) >> layers[layer]) << layers[layer]) + base;
int lBlock = ((l - lBound) >> bSzLog) + 1;
int rBlock = ((r - lBound) >> bSzLog) - 1;
Info ans = suf[layer][l];
if (lBlock <= rBlock)
{
Info mid = (layer == 0)
? query(n + lBlock, n + rBlock, (1 << lg) - n, n)
: between[layer - 1][betweenOffs + lBound + (lBlock << bCntLog) + rBlock];
ans = ans + mid;
}
ans = ans + pref[layer][r];
return ans;
}
public:
// 构造函数:用底层数组构建整棵 Sqrt Tree
SqrtTree(const vector<Info> &a)
: n((int)a.size()), lg(log2Up(max(1, n))), v(a), clz(1 << lg), onLayer(lg + 1)
{
clz[0] = 0;
for (int i = 1; i < (int)clz.size(); i++)
clz[i] = clz[i >> 1] + 1;
int tlg = lg;
while (tlg > 1)
{
onLayer[tlg] = (int)layers.size();
layers.push_back(tlg);
tlg = (tlg + 1) >> 1;
}
for (int i = lg - 1; i >= 0; i--)
onLayer[i] = max(onLayer[i], onLayer[i + 1]);
int betweenLayers = max(0, (int)layers.size() - 1);
int bSzLog = (lg + 1) >> 1;
int bSz = 1 << bSzLog;
indexSz = (n + bSz - 1) >> bSzLog;
v.resize(n + indexSz);
pref.assign(layers.size(), vector<Info>(n + indexSz));
suf.assign(layers.size(), vector<Info>(n + indexSz));
between.assign(betweenLayers, vector<Info>((1 << lg) + bSz));
build(0, 0, n, 0);
}
// 功能:闭区间查询,返回 [l,r] 上的聚合
Info query(int l, int r)
{
if (l > r)
return Info();
return query(l, r, 0, 0);
}
// 功能:单点修改,把 v[x] 改为 item,并维护相关层
void update(int x, const Info &item)
{
v[x] = item;
update(0, 0, n, 0, x);
}
// 工具函数:最小 lg 使 2^lg >= m
static int log2Up(int m)
{
int res = 0;
while ((1 << res) < m)
res++;
return res;
}
};
声明
部分内容参考:
jiangly - Codeforces 在codeforces上的提交
有部分模板未经验证,可能存在错误,如果在赛场上使用导致比赛失利,概不负责,请谨慎使用该模板
docx版本(已排版)网盘链接:https://pan.baidu.com/s/15C0G5CY2aqQykuc5sHz_RQ?pwd=jfur
退役了,把使用的算法模板整理分享一下
950



被折叠的 条评论
为什么被折叠?



