3193 字
16 分钟
竞赛算法(7) 数据结构
2025-11-29
无标签

并查集#

struct UfSet {
int n;
vector<int> pa, siz;
void init(int _n = 0) {
n = _n;
pa.assign(_n, 0);
iota(pa.begin(), pa.end(), 0);
siz.assign(_n, 1);
}
UfSet(int _n = 0) { init(_n); }
int find(int x) {
while (x != pa[x]) {
x = pa[x] = pa[pa[x]];
}
return x;
}
bool uni(int x, int y) {
x = find(x), y = find(y);
if (x == y) return false;
if (siz[x] < siz[y]) swap(x, y);
pa[y] = x;
siz[x] += siz[y];
return true;
}
};

树状数组#

Fenwick tree 或者叫 Binary Indexed Tree, 功能类似于线段树, 只不过在差分操作的辅助下可以将空间压到O(n)O(n), 并且代码简短, 常数小. 树状数组的索引通常从1开始. 树状数组只能维护满足结合律且可差分的数据和运算.

树状数组维护的运算必须满足群的性质.

树状数组和线段树作用的两个区别:

  1. 如果需要区间查询操作, 树状数组只能维护可差分信息(这一点可以通过维护两个树状数组来解决, 但时间复杂度升高)
  2. 树状数组修改操作使用的运算必须与维护信息的运算相同(或者说, 构成群结构), 而线段树不需要.(本质区别)
树状数组的性质

以下以c[i]代指i的管辖区间.

  1. 对于任意索引x, c[x][x - lowbit(x) + 1, x];
  2. 对于任意x <= y, 要么c[x]c[y]不相交, 要么c[x]包含于c[y].
  3. 任意c[x]包含于c[x + lowbit(x)], 并且对于(x, x + lowbit(x))之间任意索引y, 都有c[y]c[x]不相交.

在了解这些性质之后, 我们可以得到树状数组的单点修改和区间查询操作.

树状数组的区间查询操作

以下f[l, r]代表目标函数从lr的区间和.

  1. 要查询f[l, r]时, 只需查询f[1, r] - f[1, l - 1];
  2. 要查询f[1, r]时: 初始令i = r, 然后每次让i -= lowbit(i)直到i == 0. 叠加所有的f[c[i]]即可.
树状数组的单点修改操作

假如要修改f[x].

  1. 修改f[c[x]].
  2. x = x + lowbit(x), 若x > n说明修改完毕, 否则重复步骤1.
inline int lowbit(int x) { return x & -x; }
template<typename S>
struct FwkTree {
int n;
vector<S> d;
void init(int _n) { n = _n; d.assign(_n + 1, 0); }
void init(const vector<S>& arr) {
init(arr.size());
for (int i = 1; i <= n; i++) {
d[i] = arr[i - 1];
}
for (int i = 1; i <= n; i++) {
int pa = i + lowbit(i);
if (pa <= n) d[pa] += d[i];
}
}
FwkTree(int n = 0) { init(n); }
FwkTree(const vector<S>& arr) { init(arr); }
void add(int x, S val) {
for (int i = x + 1; i <= n; i += lowbit(i)) {
d[i] += val;
}
}
S sum(int x) {
S ret = 0;
for (int i = x + 1; i >= 1; i -= lowbit(i)) {
ret += d[i];
}
return ret;
}
S sum(int l, int r) {
// [l, r)
return sum(r - 1) - sum(l - 1);
}
};

权值树状数组#

其实就是把cnt数组存在树状数组里, 起了个高大上的名字.

这时其实我们就能查询大小在区间[L, R]里的数字个数了.

权值树状数组的核心用法: 查询索引在i左边/右边的数字中, 大小在某一区间内的数字个数. 也就是说, 假如数组是a[1], a[2], ..., a[n], 权值树状数组可以在O(nlogn)O(n\log n)内建树, 在O(logn)O(\log n)内查询i < q/i > pa[i] >= L && a[i] <= Ra[i]数量.

有人就要问了, 既然需要O(nlogn)O(n\log n)的时间建树, 那我为什么不直接暴力O(n)O(n)查找呢? 这是因为很多题目的查询是离线的, 这时我们可以把所有需要查询的i < q的问题按q的大小排好序, 这样建树的时候只需要从前往后建, 建到q_j的时候查询q_j处的问题, 只需要一次建树就能解决. 分摊复杂度O(logn)O(\log n).

SPOJ K-query(说白了就是查询[L, R]处满足>K的数字个数)

// 待补

树状数组上二分(倍增)#

// 树状数组上二分
namespace fwt_ext {
// 查找权值树状数组上第k小元素的索引
// 若总数不满k, 返回n
// O(log n)
template <class fwtree>
int kth(const fwtree& fwk, ll k) {
int n = fwk.n;
assert(n > 0 && k > 0);
if (n <= 0 || k <= 0) return -1;
int pos = 0;
int max_pow = 1 << (31 - __builtin_clz(n));
for (int i = max_pow; i > 0; i >>= 1) {
if (pos + i <= n && fwk.d[pos + i] < k) {
k -= fwk.d[pos + i];
pos += i;
}
}
return pos;
}
// 采用倍增查找满足check(pref[i]) = true的最大下标i.
// check 单调谓词函数[T, ..., T, F, ..., F], 返回0-based下的最大下标.
// 若check(empty) = false, 返回-1
template <class Monoid, typename F>
int search(const FwkTree<Monoid>& fwk, F&& check) {
using S = typename Monoid::value_type;
int n = fwk.n;
assert(n > 0);
assert(check(Monoid::e()) == true);
int pos = 0;
int max_pow = 1 << (31 - __builtin_clz(n));
S cur = Monoid::e();
for (int i = max_pow; i > 0; i >>= 1) {
if (pos + i <= n) {
S next_val = Monoid::op(cur, fwk.d[pos + i]);
if (check(next_val)) {
pos += i;
cur = next_val;
}
}
}
return pos - 1;
}
}

区间加, 区间和#

显然, 树状数组可以在O(logn)O(\log n)的时间内实现单点修改和前缀和查询. 而我们发现如果用这个性质来维护差分数组的话, 单点修改其实就对应了原数组的区间修改, 前缀和就对应了原数组的单点值. 所以用树状数组来维护原数组的差分可以实现区间修改, 单点查询.

ST表#

// 稀疏表, Sparse Table.
// T为数据类型, op为一个函数指针, 默认产生区间最小值
template <typename T, T (*op)(T, T) = std::min>
struct SpTable {
int n;
int max_log;
vector<vector<T>> st;
SpTable(const vector<T>& a) : n(a.size()) {
if (n == 0) return;
max_log = 32 - __builtin_clz(n);
st.assign(max_log, vector<T>(n));
for (int i = 0; i < n; i++) st[0][i] = a[i];
for (int j = 1; j < max_log; j++) {
for (int i = 0; i + (1 << j) <= n; i++) {
st[j][i] = op(st[j - 1][i], st[j - 1][i + (1 << (j - 1))]);
}
}
}
// 查询区间 [l, r) 的结果: O(1)
T prod(int l, int r) const {
assert(0 <= l && l < r && r <= n);
int j = 31 - __builtin_clz(r - l);
return op(st[j][l], st[j][r - (1 << j)]);
}
};

哈希表#

#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
using __gnu_pbds::gp_hash_table;
using __gnu_pbds::null_type;
/*-------hashtable.hpp-------*/
// 防hack哈希表, 基于gp_hash_table, 速度快
// 支持整数类型作为键. 其他类型需先转换.
// 接口与unordered_set基本一致. 请注意最好不要使用count()
struct chash {
// 你问我这堆数字是什么, 我也不知道
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
};
template<typename K, typename V>
using HashMap = gp_hash_table<K, V, chash>;
template<typename K>
using HashSet = gp_hash_table<K, null_type, chash>;

笛卡尔树#

// 笛卡尔树 (Cartesian Tree), 可以完全代替单调栈
// 默认 Compare = less<T> -> 大根堆 (Max-Tree) -> 维护区间最大值
template <typename T, typename Compare = std::less<T>>
struct CtsTree {
int n;
int root;
vector<int> ltc, rtc; // 左右孩子, 分别代表管辖区间内左右的次大(小)值.
vector<int> L, R; // 管辖区间 [L, R] (闭区间)
CtsTree(const vector<T>& a, Compare cmp = Compare()) : n(a.size()) {
ltc.assign(n, -1);
rtc.assign(n, -1);
L.resize(n);
R.resize(n);
vector<int> stk;
for (int i = 0; i < n; ++i) {
L[i] = i;
R[i] = i;
int last_popped = -1;
while (!stk.empty() && cmp(a[stk.back()], a[i])) {
last_popped = stk.back();
stk.pop_back();
}
if (last_popped != -1) {
ltc[i] = last_popped;
}
if (!stk.empty()) {
rtc[stk.back()] = i;
}
stk.push_back(i);
}
root = stk.empty() ? -1 : stk[0];
if (root != -1) {
init_range(root); // 与建树复杂度一致
}
}
// dfs预处理区间, O(n).
void init_range(int u) {
if (ltc[u] != -1) {
init_range(ltc[u]);
L[u] = L[ltc[u]];
}
if (rtc[u] != -1) {
init_range(rtc[u]);
R[u] = R[rtc[u]];
}
}
// 返回 [l, r) 左闭右开区间 (0-based), 总贡献为((i + 1) - l)(r - i)
// 即 [l, i + 1) 和 [i, r) 区间. O(1)
pair<int, int> range(int i) const {
return {L[i], R[i] + 1};
}
};

线段树#

// 线段树(Segment Tree)
// Info 要求包含:
// 数据 = 空构造
// friend Info operator+(const Info&, const Info&)
template<class Info>
struct SegTree {
int n, size, log;
vector<Info> d;
void _pull(int x) {
d[x] = d[x * 2] + d[x * 2 + 1];
}
// 对外接口
// 重新建树
void build(const vector<Info>& arr) {
n = arr.size();
log = 0;
size = 1;
while (size < n) {
size *= 2;
log++;
}
d.assign(2 * size, Info());
for (int i = 0; i < n; i++) {
d[size + i] = arr[i];
}
for (int i = size - 1; i > 0; i--) {
_pull(i);
}
}
SegTree(const vector<Info>& arr) {
build(arr);
}
SegTree(int _n, const Info& def = Info()) {
build(vector<Info>(_n, def));
}
// 单点赋值, O(log n)
void set(int x, const Info& val) {
assert(0 <= x && x < n);
x += size;
d[x] = val;
// 自底向上更新,无需 push
x /= 2;
for (; x > 0; x /= 2) {
_pull(x);
}
}
// 区间查询[lt, rt), O(log n)
Info query(int lt, int rt) {
assert(0 <= lt && lt <= rt && rt <= n);
if (lt == rt) {
return Info();
}
if (lt + 1 == rt) {
return d[size + lt];
}
lt += size;
rt += size;
Info sumlt, sumrt;
for (; lt < rt; lt /= 2, rt /= 2) {
if (lt % 2 == 1) {
sumlt = sumlt + d[lt];
lt++;
}
if (rt % 2 == 1) {
rt--;
sumrt = d[rt] + sumrt;
}
}
return sumlt + sumrt;
}
// 扩展接口
// 查最右rt使[lt, rt)满足check, O(log n)
template <class F>
int search_right(int lt, F check) {
if (lt == n) {
return n;
}
lt += size;
Info sum;
do {
while (lt % 2 == 0) {
lt /= 2;
}
if (!check(sum + d[lt])) {
while (lt < size) {
lt *= 2;
if (check(sum + d[lt])) {
sum = sum + d[lt];
lt++;
}
}
return lt - size;
}
sum = sum + d[lt];
lt++;
} while (lowbit(lt) != lt);
return n;
}
// 查最左lt使得[lt, rt)满足check, O(log n)
template <class F>
int search_left(int rt, F check) {
if (rt == 0) {
return 0;
}
rt += size;
Info sum;
do {
rt--;
while (rt > 1 && rt % 2 == 1) {
rt /= 2;
}
if (!check(d[rt] + sum)) {
while (rt < size) {
rt = 2 * rt + 1;
if (check(d[rt] + sum)) {
sum = d[rt] + sum;
rt--;
}
}
return rt + 1 - size;
}
sum = d[rt] + sum;
} while (lowbit(rt) != rt);
return 0;
}
};

lazy tag版#

// 懒标记线段树(Lazy Segment Tree)
// Info 要求包含:
// 节点数据 = 空区间状态(零元)
// void apply(const Tag&) 标记作用于数据
// friend Info operator+(const Info&, const Info&)
// Tag 要求包含:
// 标记数据 = 叠加标记不变的状态(单位元)
// void apply(const Tag&) 标记叠加
template<class Info, class Tag>
struct SegTree_lazy {
int n, size, log;
vector<Info> d;
vector<Tag> tag;
#define anc(x) ((x) >> i)
#define ck(x) ((anc(x) << i) != x)
void _pull(int x) {
d[x] = d[x * 2] + d[x * 2 + 1];
}
void _apply(int x, const Tag& t) {
d[x].apply(t);
if (x < size) {
tag[x].apply(t);
}
}
void _push(int x) {
_apply(x * 2, tag[x]);
_apply(x * 2 + 1, tag[x]);
tag[x] = Tag();
}
// 对外接口
// 重新建树
void build(const vector<Info>& arr) {
n = arr.size();
log = 0;
size = 1;
while (size < n) {
size *= 2;
log++;
}
d.assign(2 * size, Info());
tag.assign(size, Tag());
for (int i = 0; i < n; i++) {
d[size + i] = arr[i];
}
for (int i = size - 1; i > 0; i--) {
_pull(i);
}
}
SegTree_lazy() : n(0) {}
SegTree_lazy(const vector<Info>& arr) {
build(arr);
}
SegTree_lazy(int _n, const Info& def = Info()) {
build(vector<Info>(_n, def));
}
// 单点赋值, O(log n)
void set(int x, const Info& val) {
assert(0 <= x && x < n);
x += size;
for (int i = log; i > 0; i--) {
_push(anc(x));
}
d[x] = val;
for (int i = 1; i < log + 1; i++) {
_pull(anc(x));
}
}
// 区间修改[lt, rt), O(log n)
void modify(int lt, int rt, const Tag& t) {
assert(0 <= lt && lt <= rt && rt <= n);
if (lt == rt) {
return;
}
lt += size;
rt += size;
for (int i = log; i > 0; i--) {
if (ck(lt)) {
_push(anc(lt));
}
if (ck(rt)) {
_push(anc(rt - 1));
}
}
for (int i = lt, j = rt; i < j; i /= 2, j /= 2) {
if (i % 2 == 1) {
_apply(i, t);
i++;
}
if (j % 2 == 1) {
j--;
_apply(j, t);
}
}
for (int i = 1; i < log + 1; i++) {
if (ck(lt)) {
_pull(anc(lt));
}
if (ck(rt)) {
_pull(anc(rt - 1));
}
}
}
// 区间查询[lt, rt)
Info prod(int lt, int rt) {
assert(0 <= lt && lt <= rt && rt <= n);
if (lt == rt) {
return Info();
}
lt += size;
rt += size;
for (int i = log; i > 0; i--) {
if (ck(lt)) {
_push(anc(lt));
}
if (ck(rt)) {
_push(anc(rt - 1));
}
}
Info sumlt, sumrt;
for (; lt < rt; lt /= 2, rt /= 2) {
if (lt % 2 == 1) {
sumlt = sumlt + d[lt];
lt++;
}
if (rt % 2 == 1) {
rt--;
sumrt = d[rt] + sumrt;
}
}
return sumlt + sumrt;
}
// 扩展接口
// 查最右rt使[lt, rt)满足check, O(log n)
template <class F>
int search_right(int lt, F check) {
if (lt == n) {
return n;
}
lt += size;
for (int i = log; i > 0; i--) {
_push(anc(lt));
}
Info sum;
do {
while (lt % 2 == 0) {
lt /= 2;
}
if (!check(sum + d[lt])) {
while (lt < size) {
_push(lt);
lt *= 2;
if (check(sum + d[lt])) {
sum = sum + d[lt];
lt++;
}
}
return lt - size;
}
sum = sum + d[lt];
lt++;
} while (lowbit(lt) != lt);
return n;
}
// 查最左lt使得[lt, rt)满足check
template <class F>
int search_left(int rt, F check) {
if (rt == 0) {
return 0;
}
rt += size;
for (int i = log; i > 0; i--) {
_push(anc(rt - 1));
}
Info sum;
do {
rt--;
while (rt > 1 && rt % 2 == 1) {
rt /= 2;
}
if (!check(d[rt] + sum)) {
while (rt < size) {
_push(rt);
rt = 2 * rt + 1;
if (check(d[rt] + sum)) {
sum = d[rt] + sum;
rt--;
}
}
return rt + 1 - size;
}
sum = d[rt] + sum;
} while (lowbit(rt) != rt);
return 0;
}
#undef anc
#undef ck
};
竞赛算法(7) 数据结构
https://fuwari.vercel.app/posts/竞赛算法7-数据结构/
作者
ykindred
发布于
2025-11-29
许可协议
CC BY-NC-SA 4.0