3921 字
20 分钟
高级数据结构详解:从CSP-S到IOI

高级数据结构详解:从CSP-S到IOI#

在算法竞赛中,数据结构是解决复杂问题的关键工具。本文将系统讲解从CSP-S到IOI级别的高级数据结构,包括原理分析、代码实现和实战应用。

1. 线段树(Segment Tree)#

线段树是一种用于解决区间查询和修改问题的树形数据结构,支持O(log n)的查询和修改操作。

1.1 基本原理#

线段树将区间递归地二分,每个节点维护一个区间的信息(如和、最大值、最小值等)。对于长度为n的数组,线段树是一棵完全二叉树,高度为log n。

核心性质

  • 父节点区间是子节点区间的并集
  • 叶子节点对应数组的单个元素
  • 空间复杂度:O(4n)

1.2 基础实现(区间求和)#

class SegmentTree {
private:
vector<long long> tree;
vector<long long> arr;
int n;
void build(int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
return;
}
int mid = (start + end) / 2;
int leftNode = 2 * node;
int rightNode = 2 * node + 1;
build(leftNode, start, mid);
build(rightNode, mid + 1, end);
tree[node] = tree[leftNode] + tree[rightNode];
}
void updateRange(int node, int start, int end, int l, int r, long long val) {
if (start > r || end < l) return;
if (start == end) {
tree[node] += val;
return;
}
int mid = (start + end) / 2;
updateRange(2 * node, start, mid, l, r, val);
updateRange(2 * node + 1, mid + 1, end, l, r, val);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
long long queryRange(int node, int start, int end, int l, int r) {
if (start > r || end < l) return 0;
if (l <= start && end <= r) return tree[node];
int mid = (start + end) / 2;
long long leftSum = queryRange(2 * node, start, mid, l, r);
long long rightSum = queryRange(2 * node + 1, mid + 1, end, l, r);
return leftSum + rightSum;
}
public:
SegmentTree(vector<long long>& nums) {
n = nums.size();
arr = nums;
tree.resize(4 * n);
build(1, 0, n - 1);
}
void update(int l, int r, long long val) {
updateRange(1, 0, n - 1, l, r, val);
}
long long query(int l, int r) {
return queryRange(1, 0, n - 1, l, r);
}
};

1.3 懒惰标记(Lazy Propagation)#

对于区间修改操作,使用懒惰标记可以将时间复杂度从O(n log n)优化到O(log n)。

class LazySegmentTree {
private:
vector<long long> tree, lazy;
int n;
void pushDown(int node, int start, int end) {
if (lazy[node] != 0) {
tree[node] += (end - start + 1) * lazy[node];
if (start != end) {
lazy[2 * node] += lazy[node];
lazy[2 * node + 1] += lazy[node];
}
lazy[node] = 0;
}
}
void updateRange(int node, int start, int end, int l, int r, long long val) {
pushDown(node, start, end);
if (start > r || end < l) return;
if (l <= start && end <= r) {
lazy[node] += val;
pushDown(node, start, end);
return;
}
int mid = (start + end) / 2;
updateRange(2 * node, start, mid, l, r, val);
updateRange(2 * node + 1, mid + 1, end, l, r, val);
pushDown(2 * node, start, mid);
pushDown(2 * node + 1, mid + 1, end);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
long long queryRange(int node, int start, int end, int l, int r) {
if (start > r || end < l) return 0;
pushDown(node, start, end);
if (l <= start && end <= r) return tree[node];
int mid = (start + end) / 2;
return queryRange(2 * node, start, mid, l, r) +
queryRange(2 * node + 1, mid + 1, end, l, r);
}
public:
LazySegmentTree(int size) : n(size) {
tree.resize(4 * n);
lazy.resize(4 * n);
}
void update(int l, int r, long long val) {
updateRange(1, 0, n - 1, l, r, val);
}
long long query(int l, int r) {
return queryRange(1, 0, n - 1, l, r);
}
};

复杂度分析

  • 建树:O(n)
  • 单点/区间修改:O(log n)
  • 区间查询:O(log n)
  • 空间:O(4n)

适用场景:区间和、区间最值、区间GCD、区间加、区间乘等。

2. 树状数组(Binary Indexed Tree / Fenwick Tree)#

树状数组是一种代码简洁、常数小的数据结构,主要用于高效地进行前缀和查询和单点修改。

2.1 基本原理#

树状数组利用二进制的性质,每个位置存储一段区间的和。核心函数:

  • lowbit(x) = x & (-x):获取x的最低位1

性质

  • tree[i] 管理区间 [i - lowbit(i) + 1, i]
  • 空间复杂度:O(n)

2.2 基础实现#

class BIT {
private:
vector<long long> tree;
int n;
int lowbit(int x) {
return x & (-x);
}
public:
BIT(int size) : n(size) {
tree.resize(n + 1);
}
// 单点修改:位置 pos 增加 val
void update(int pos, long long val) {
for (int i = pos; i <= n; i += lowbit(i)) {
tree[i] += val;
}
}
// 查询前缀和 [1, pos]
long long query(int pos) {
long long sum = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
sum += tree[i];
}
return sum;
}
// 查询区间和 [l, r]
long long queryRange(int l, int r) {
return query(r) - query(l - 1);
}
};

2.3 高级应用#

1. 区间修改,单点查询(差分数组)

class RangeUpdateBIT {
private:
BIT diff;
int n;
public:
RangeUpdateBIT(int size) : n(size), diff(size) {}
// 区间 [l, r] 增加 val
void updateRange(int l, int r, long long val) {
diff.update(l, val);
diff.update(r + 1, -val);
}
// 查询单点值
long long query(int pos) {
return diff.query(pos);
}
};

2. 区间修改,区间查询(双树状数组)

class RangeUpdateRangeQuery {
private:
BIT tree1, tree2; // tree1存d[i], tree2存i*d[i]
int n;
public:
RangeUpdateRangeQuery(int size) : n(size), tree1(size), tree2(size) {}
void updateRange(int l, int r, long long val) {
tree1.update(l, val);
tree1.update(r + 1, -val);
tree2.update(l, val * (l - 1));
tree2.update(r + 1, -val * r);
}
long long query(int pos) {
return pos * tree1.query(pos) - tree2.query(pos);
}
long long queryRange(int l, int r) {
return query(r) - query(l - 1);
}
};

复杂度分析

  • 修改/查询:O(log n)
  • 空间:O(n)

适用场景:前缀和、逆序对、动态中位数、二维区间和。

3. 并查集(Union-Find / Disjoint Set Union)#

并查集是一种用于处理不相交集合合并和查询问题的数据结构。

3.1 优化实现#

class UnionFind {
private:
vector<int> parent, rank;
int count; // 连通分量个数
public:
UnionFind(int n) : count(n) {
parent.resize(n);
rank.resize(n, 1);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 路径压缩
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 按秩合并
bool unite(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return false;
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
count--;
return true;
}
bool connected(int x, int y) {
return find(x) == find(y);
}
int getCount() {
return count;
}
};

3.2 带权并查集#

class WeightedUnionFind {
private:
vector<int> parent;
vector<long long> weight; // 到父节点的权值
public:
WeightedUnionFind(int n) {
parent.resize(n);
weight.resize(n, 0);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
int root = find(parent[x]);
weight[x] += weight[parent[x]];
parent[x] = root;
}
return parent[x];
}
bool unite(int x, int y, long long w) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return false;
parent[rootX] = rootY;
weight[rootX] = weight[y] - weight[x] + w;
return true;
}
long long getWeight(int x, int y) {
if (find(x) != find(y)) return LLONG_MAX;
return weight[x] - weight[y];
}
};

复杂度分析

  • find/unite:O(α(n)),α为阿克曼函数的反函数,近似O(1)
  • 空间:O(n)

适用场景:连通性判断、最小生成树(Kruskal)、动态连通性。

4. 单调栈和单调队列#

4.1 单调栈#

单调栈用于解决”下一个更大元素”类问题。

// 找每个元素右边第一个比它大的元素
vector<int> nextGreaterElement(vector<int>& nums) {
int n = nums.size();
vector<int> result(n, -1);
stack<int> st; // 存储下标,单调递减
for (int i = 0; i < n; i++) {
while (!st.empty() && nums[st.top()] < nums[i]) {
result[st.top()] = i;
st.pop();
}
st.push(i);
}
return result;
}
// 最大矩形面积(经典题)
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
heights.push_back(0);
int maxArea = 0;
for (int i = 0; i < heights.size(); i++) {
while (!st.empty() && heights[st.top()] > heights[i]) {
int h = heights[st.top()];
st.pop();
int w = st.empty() ? i : i - st.top() - 1;
maxArea = max(maxArea, h * w);
}
st.push(i);
}
return maxArea;
}

4.2 单调队列#

用于滑动窗口最值问题。

class MonotonicQueue {
private:
deque<int> dq; // 存储下标
vector<int>& arr;
public:
MonotonicQueue(vector<int>& nums) : arr(nums) {}
void push(int idx) {
while (!dq.empty() && arr[dq.back()] < arr[idx]) {
dq.pop_back();
}
dq.push_back(idx);
}
void pop(int idx) {
if (!dq.empty() && dq.front() == idx) {
dq.pop_front();
}
}
int getMax() {
return arr[dq.front()];
}
};
// 滑动窗口最大值
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MonotonicQueue mq(nums);
vector<int> result;
for (int i = 0; i < nums.size(); i++) {
mq.push(i);
if (i >= k - 1) {
result.push_back(mq.getMax());
mq.pop(i - k + 1);
}
}
return result;
}

复杂度:O(n),每个元素最多入队出队一次。

5. ST表(Sparse Table)#

ST表用于解决RMQ(区间最值查询)问题,支持O(1)查询,但不支持修改。

5.1 实现#

class SparseTable {
private:
vector<vector<int>> st;
vector<int> lg;
int n;
public:
SparseTable(vector<int>& arr) {
n = arr.size();
lg.resize(n + 1);
lg[1] = 0;
for (int i = 2; i <= n; i++) {
lg[i] = lg[i / 2] + 1;
}
int maxLog = lg[n] + 1;
st.assign(n, vector<int>(maxLog));
// 初始化
for (int i = 0; i < n; i++) {
st[i][0] = arr[i];
}
// 动态规划
for (int j = 1; j < maxLog; j++) {
for (int i = 0; i + (1 << j) <= n; i++) {
st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
}
}
// 查询区间 [l, r] 最大值
int query(int l, int r) {
int j = lg[r - l + 1];
return max(st[l][j], st[r - (1 << j) + 1][j]);
}
};

复杂度分析

  • 预处理:O(n log n)
  • 查询:O(1)
  • 空间:O(n log n)

适用场景:静态RMQ、LCA(最近公共祖先)。

6. 主席树(可持久化线段树)#

主席树可以查询历史版本的数据,是可持久化数据结构的代表。

6.1 静态区间第k小#

struct Node {
int lson, rson;
int sum;
};
class PersistentSegmentTree {
private:
vector<Node> tree;
vector<int> roots;
int cnt;
int build(int l, int r) {
int root = cnt++;
tree[root].sum = 0;
if (l == r) return root;
int mid = (l + r) / 2;
tree[root].lson = build(l, mid);
tree[root].rson = build(mid + 1, r);
return root;
}
int update(int pre, int l, int r, int pos) {
int root = cnt++;
tree[root] = tree[pre];
tree[root].sum++;
if (l == r) return root;
int mid = (l + r) / 2;
if (pos <= mid) {
tree[root].lson = update(tree[pre].lson, l, mid, pos);
} else {
tree[root].rson = update(tree[pre].rson, mid + 1, r, pos);
}
return root;
}
int query(int u, int v, int l, int r, int k) {
if (l == r) return l;
int mid = (l + r) / 2;
int leftSum = tree[tree[v].lson].sum - tree[tree[u].lson].sum;
if (k <= leftSum) {
return query(tree[u].lson, tree[v].lson, l, mid, k);
} else {
return query(tree[u].rson, tree[v].rson, mid + 1, r, k - leftSum);
}
}
public:
PersistentSegmentTree(int maxSize) {
tree.resize(maxSize * 40);
cnt = 0;
}
void buildTree(vector<int>& arr, vector<int>& sorted) {
int n = arr.size();
int m = sorted.size();
roots.resize(n + 1);
roots[0] = build(0, m - 1);
for (int i = 0; i < n; i++) {
int pos = lower_bound(sorted.begin(), sorted.end(), arr[i]) - sorted.begin();
roots[i + 1] = update(roots[i], 0, m - 1, pos);
}
}
int kthSmallest(int l, int r, int k, vector<int>& sorted) {
int idx = query(roots[l - 1], roots[r], 0, sorted.size() - 1, k);
return sorted[idx];
}
};

复杂度分析

  • 建树:O(n log n)
  • 查询:O(log n)
  • 空间:O(n log n)

7. 平衡树#

7.1 Treap(树堆)#

struct TreapNode {
int val, priority;
int size;
TreapNode *left, *right;
TreapNode(int v) : val(v), priority(rand()), size(1), left(nullptr), right(nullptr) {}
};
class Treap {
private:
TreapNode* root;
int getSize(TreapNode* node) {
return node ? node->size : 0;
}
void updateSize(TreapNode* node) {
if (node) {
node->size = getSize(node->left) + getSize(node->right) + 1;
}
}
TreapNode* rotateLeft(TreapNode* node) {
TreapNode* right = node->right;
node->right = right->left;
right->left = node;
updateSize(node);
updateSize(right);
return right;
}
TreapNode* rotateRight(TreapNode* node) {
TreapNode* left = node->left;
node->left = left->right;
left->right = node;
updateSize(node);
updateSize(left);
return left;
}
TreapNode* insert(TreapNode* node, int val) {
if (!node) return new TreapNode(val);
if (val < node->val) {
node->left = insert(node->left, val);
if (node->left->priority > node->priority) {
node = rotateRight(node);
}
} else {
node->right = insert(node->right, val);
if (node->right->priority > node->priority) {
node = rotateLeft(node);
}
}
updateSize(node);
return node;
}
int getRank(TreapNode* node, int val) {
if (!node) return 0;
if (val <= node->val) {
return getRank(node->left, val);
} else {
return getSize(node->left) + 1 + getRank(node->right, val);
}
}
public:
Treap() : root(nullptr) {}
void insert(int val) {
root = insert(root, val);
}
int rank(int val) {
return getRank(root, val) + 1;
}
};

复杂度:期望O(log n),最坏O(n)。

8. 分块#

分块是一种暴力优化思想,将数组分成√n块。

class BlockArray {
private:
vector<int> arr;
vector<long long> blockSum;
int n, blockSize, blockCount;
public:
BlockArray(vector<int>& nums) {
arr = nums;
n = arr.size();
blockSize = sqrt(n) + 1;
blockCount = (n + blockSize - 1) / blockSize;
blockSum.resize(blockCount);
for (int i = 0; i < n; i++) {
blockSum[i / blockSize] += arr[i];
}
}
void update(int pos, int val) {
blockSum[pos / blockSize] += val - arr[pos];
arr[pos] = val;
}
long long query(int l, int r) {
long long sum = 0;
int blockL = l / blockSize;
int blockR = r / blockSize;
if (blockL == blockR) {
for (int i = l; i <= r; i++) {
sum += arr[i];
}
} else {
for (int i = l; i < (blockL + 1) * blockSize; i++) {
sum += arr[i];
}
for (int i = blockL + 1; i < blockR; i++) {
sum += blockSum[i];
}
for (int i = blockR * blockSize; i <= r; i++) {
sum += arr[i];
}
}
return sum;
}
};

复杂度:修改O(1),查询O(√n)。

9. 树链剖分#

树链剖分将树分解为若干条链,结合线段树实现树上路径查询和修改。

class HeavyLightDecomposition {
private:
vector<vector<int>> adj;
vector<int> parent, depth, heavy, head, pos;
LazySegmentTree seg;
int currentPos;
int dfs(int u) {
int size = 1, maxSubtree = 0;
for (int v : adj[u]) {
if (v == parent[u]) continue;
parent[v] = u;
depth[v] = depth[u] + 1;
int subtree = dfs(v);
size += subtree;
if (subtree > maxSubtree) {
maxSubtree = subtree;
heavy[u] = v;
}
}
return size;
}
void decompose(int u, int h) {
head[u] = h;
pos[u] = currentPos++;
if (heavy[u] != -1) {
decompose(heavy[u], h);
}
for (int v : adj[u]) {
if (v != parent[u] && v != heavy[u]) {
decompose(v, v);
}
}
}
public:
HeavyLightDecomposition(int n) : seg(n) {
adj.resize(n);
parent.resize(n);
depth.resize(n);
heavy.assign(n, -1);
head.resize(n);
pos.resize(n);
currentPos = 0;
}
void addEdge(int u, int v) {
adj[u].push_back(v);
adj[v].push_back(u);
}
void build(int root = 0) {
dfs(root);
decompose(root, root);
}
void updatePath(int u, int v, long long val) {
while (head[u] != head[v]) {
if (depth[head[u]] < depth[head[v]]) swap(u, v);
seg.update(pos[head[u]], pos[u], val);
u = parent[head[u]];
}
if (depth[u] > depth[v]) swap(u, v);
seg.update(pos[u], pos[v], val);
}
long long queryPath(int u, int v) {
long long res = 0;
while (head[u] != head[v]) {
if (depth[head[u]] < depth[head[v]]) swap(u, v);
res += seg.query(pos[head[u]], pos[u]);
u = parent[head[u]];
}
if (depth[u] > depth[v]) swap(u, v);
res += seg.query(pos[u], pos[v]);
return res;
}
};

复杂度:修改/查询O(log² n)。

LCT是动态树结构,支持动态加边删边和路径查询。

struct LCTNode {
LCTNode *ch[2], *fa;
int val, sum;
bool rev;
LCTNode(int v = 0) : val(v), sum(v), rev(false) {
ch[0] = ch[1] = fa = nullptr;
}
bool isRoot() {
return !fa || (fa->ch[0] != this && fa->ch[1] != this);
}
void pushDown() {
if (rev) {
swap(ch[0], ch[1]);
if (ch[0]) ch[0]->rev ^= 1;
if (ch[1]) ch[1]->rev ^= 1;
rev = false;
}
}
void update() {
sum = val;
if (ch[0]) sum += ch[0]->sum;
if (ch[1]) sum += ch[1]->sum;
}
};
class LinkCutTree {
private:
void rotate(LCTNode* x) {
LCTNode* y = x->fa;
LCTNode* z = y->fa;
int k = y->ch[1] == x;
if (!y->isRoot()) z->ch[z->ch[1] == y] = x;
x->fa = z;
y->ch[k] = x->ch[k ^ 1];
if (x->ch[k ^ 1]) x->ch[k ^ 1]->fa = y;
x->ch[k ^ 1] = y;
y->fa = x;
y->update();
x->update();
}
void splay(LCTNode* x) {
while (!x->isRoot()) {
LCTNode* y = x->fa;
if (!y->isRoot()) {
(y->fa->ch[0] == y) ^ (y->ch[0] == x) ? rotate(x) : rotate(y);
}
rotate(x);
}
}
void access(LCTNode* x) {
for (LCTNode* y = nullptr; x; y = x, x = x->fa) {
splay(x);
x->ch[1] = y;
x->update();
}
}
void makeRoot(LCTNode* x) {
access(x);
splay(x);
x->rev ^= 1;
}
public:
void link(LCTNode* x, LCTNode* y) {
makeRoot(x);
x->fa = y;
}
void cut(LCTNode* x, LCTNode* y) {
makeRoot(x);
access(y);
splay(y);
y->ch[0]->fa = nullptr;
y->ch[0] = nullptr;
y->update();
}
int query(LCTNode* x, LCTNode* y) {
makeRoot(x);
access(y);
splay(y);
return y->sum;
}
};

复杂度:均摊O(log n)。

11. 经典竞赛应用#

11.1 线段树应用#

例题1:区间修改区间求和

  • 洛谷 P3372
  • 使用懒惰标记的线段树

例题2:区间最大子段和

  • 维护区间最大值、最大前缀、最大后缀、总和

11.2 树状数组应用#

例题:逆序对

long long countInversions(vector<int>& arr) {
vector<int> sorted = arr;
sort(sorted.begin(), sorted.end());
sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
BIT bit(sorted.size());
long long inversions = 0;
for (int i = arr.size() - 1; i >= 0; i--) {
int pos = lower_bound(sorted.begin(), sorted.end(), arr[i]) - sorted.begin() + 1;
inversions += bit.query(pos - 1);
bit.update(pos, 1);
}
return inversions;
}

11.3 主席树应用#

例题:区间第k小

  • POJ 2104
  • 使用可持久化线段树

11.4 树链剖分应用#

例题:树上路径和

  • 支持修改节点权值
  • 查询路径和

12. 总结与进阶#

12.1 数据结构选择指南#

问题类型推荐数据结构复杂度
区间和查询,单点修改树状数组O(log n)
区间和查询,区间修改线段树(懒标记)O(log n)
静态区间最值ST表O(1)
动态区间最值线段树O(log n)
区间第k小主席树O(log n)
动态维护有序集合平衡树O(log n)
树上路径操作树链剖分/LCTO(log² n) / O(log n)
连通性维护并查集O(α(n))

12.2 学习路径建议#

CSP-S级别

  1. 树状数组基础
  2. 线段树基础
  3. 并查集
  4. 单调栈/队列
  5. ST表

省选/NOI级别

  1. 线段树进阶(懒标记、动态开点)
  2. 主席树
  3. 树链剖分
  4. 基础平衡树

IOI/WC级别

  1. LCT
  2. 可持久化数据结构
  3. 高级平衡树(Splay)
  4. 分块进阶

12.3 练习建议#

  1. 基础巩固:Codeforces Div2 C/D, 洛谷普及+/提高
  2. 专项训练:按数据结构类型刷专题
  3. 综合应用:NOI/IOI真题
  4. 码量训练:手写常用模板至少5遍

掌握这些高级数据结构,是在算法竞赛中取得好成绩的关键。建议从基础开始,逐步进阶,多做题多总结,形成自己的代码模板库。


推荐资源

  • 《算法竞赛进阶指南》
  • OI Wiki
  • Codeforces
  • 洛谷题库
  • USACO Guide
高级数据结构详解:从CSP-S到IOI
https://myblog-590.pages.dev/posts/algorithm-data-structures/
作者
谢星宇
发布于
2026-01-21
许可协议
CC BY-NC-SA 4.0