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

最低0.47元/天 解锁文章
1389

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



