CSP-S 算法复习(常用)
本文系统总结了算法竞赛中的核心数据结构和算法技巧,涵盖以下关键内容: 数据结构篇: 并查集:处理动态连通性问题,支持路径压缩和带权关系维护 树状数组:高效处理前缀和与单点修改,支持区间操作 线段树:全能区间操作,支持懒标记延迟更新 单调栈/队列:线性时间处理滑动窗口最值和特定元素查询 图论篇: 最短路径算法:包括Dijkstra、SPFA和Floyd三种经典实现 最小生成树:Kruskal算法的并
一、 数据结构
并查集
树状数组与线段树
单调栈与单调队列
二、 图论
最短路径算法
最小生成树
拓扑排序与关键路径
连通性问题
三、 字符串与搜索
KMP算法
Trie字典树
哈希
四、 高级技巧与优化思想
倍增算法:ST表、最近公共祖先
快速幂
离散化
树上前缀和与差分
一、数据结构
1. 并查集
📌 核心用途
维护动态连通性:快速查询两个元素是否属于同一个集合、快速合并两个集合;带权并查集还能维护元素间的相对关系(如距离、权值、逻辑关系)。
🎯 适用题型
- 基础:朋友圈 / 岛屿连通、判断两点是否连通
- 进阶:最小生成树(Kruskal 算法)、带权关系问题(食物链、奇偶游戏)
- 拓展:动态连通性、离线连通查询
💻 拓展用法:普通并查集 + 带权并查集(核心拓展)
#include <iostream>
#include <vector>
using namespace std;
// ===================== 1. 普通并查集(基础版) =====================
struct DSU {
vector<int> fa; // 父节点数组
// 初始化:每个元素的父节点是自己
DSU(int n) { fa.resize(n + 1); for (int i = 1; i <= n; i++) fa[i] = i; }
// 查找根节点 + 路径压缩(核心优化,让树变扁平)
int find(int x) {
if (fa[x] != x) fa[x] = find(fa[x]); // 递归找根,路径压缩
return fa[x];
}
// 合并两个集合:把x的根挂到y的根上
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx != ry) fa[rx] = ry;
}
// 判断x和y是否连通
bool isConnected(int x, int y) { return find(x) == find(y); }
};
// ===================== 2. 带权并查集(拓展:维护节点到根的权值) =====================
// 用途:解决元素间有相对关系的问题(如食物链:A吃B,B吃C)
struct WeightDSU {
vector<int> fa; // 父节点
vector<int> val; // val[x]:x到父节点fa[x]的权值
WeightDSU(int n) {
fa.resize(n + 1); val.resize(n + 1, 0);
for (int i = 1; i <= n; i++) fa[i] = i;
}
// 带权查找 + 路径压缩
int find(int x) {
if (fa[x] != x) {
int f = fa[x]; // 记录原父节点
fa[x] = find(fa[x]); // 递归更新父节点为根
val[x] += val[f]; // 更新x到根的权值(核心)
}
return fa[x];
}
};
// 测试
int main() {
DSU dsu(5);
dsu.unite(1,2); dsu.unite(2,3);
cout << dsu.isConnected(1,3); // 输出1(连通)
return 0;
}
2. 树状数组(Fenwick Tree)
📌 核心用途
高效处理前缀和 / 单点修改:时间复杂度 O(logn),代码极简、空间占用小;支持单点修改 + 区间查询、区间修改 + 单点查询、区间修改 + 区间查询。
🎯 适用题型
- 基础:单点更新 + 区间求和、逆序对统计
- 进阶:动态前缀和、区间增减 + 单点查询
- 拓展:二维前缀和、离线区间查询
💻 拓展用法:区间修改 + 区间查询(最强拓展)
#include <iostream>
#include <vector>
using namespace std;
// 树状数组:支持 区间修改 + 区间查询(核心拓展)
class FenwickTree {
private:
vector<long long> tr1, tr2; // 两个树状数组维护差分
int n;
// 单点修改
void add(vector<long long>& tr, int idx, long long val) {
for (; idx <= n; idx += idx & -idx) tr[idx] += val;
}
// 前缀和查询
long long query(vector<long long>& tr, int idx) {
long long res = 0;
for (; idx > 0; idx -= idx & -idx) res += tr[idx];
return res;
}
public:
FenwickTree(int size) : n(size) {
tr1.resize(n + 1, 0);
tr2.resize(n + 1, 0);
}
// 区间修改:[l, r] 加 val
void rangeAdd(int l, int r, long long val) {
add(tr1, l, val);
add(tr1, r + 1, -val);
add(tr2, l, val * (l - 1));
add(tr2, r + 1, -val * r);
}
// 区间查询:[1, x] 的前缀和
long long preQuery(int x) {
return x * query(tr1, x) - query(tr2, x);
}
// 区间查询:[l, r] 的和
long long rangeQuery(int l, int r) {
return preQuery(r) - preQuery(l - 1);
}
};
// 测试
int main() {
FenwickTree ft(5);
ft.rangeAdd(1,3,2); // [1,3]加2
cout << ft.rangeQuery(1,5); // 输出6(1+1+1+0+0=6)
return 0;
}
3. 线段树
📌 核心用途
全能区间操作:支持区间修改、区间查询(求和 / 最值 /gcd),通过懒标记实现延迟更新,功能覆盖树状数组,适配所有复杂区间问题。
🎯 适用题型
- 基础:区间加 / 乘 + 区间求和 / 最值
- 进阶:区间赋值、区间覆盖、动态区间查询
- 拓展:可持久化线段树、扫描线、动态开点
💻 拓展用法:区间加 + 区间求和(带懒标记,标准模板)
#include <iostream>
#include <vector>
using namespace std;
// 线段树:区间加法 + 区间求和(带懒标记)
class SegmentTree {
private:
vector<long long> sum; // 区间和
vector<long long> lazy; // 懒标记:延迟更新
int n;
// 向上更新:子节点更新父节点
void pushUp(int p) { sum[p] = sum[p<<1] + sum[p<<1|1]; }
// 向下更新:懒标记传递给子节点
void pushDown(int p, int l, int r) {
if (lazy[p] == 0) return;
int mid = (l + r) >> 1;
// 左孩子
sum[p<<1] += lazy[p] * (mid - l + 1);
lazy[p<<1] += lazy[p];
// 右孩子
sum[p<<1|1] += lazy[p] * (r - mid);
lazy[p<<1|1] += lazy[p];
// 清空当前节点懒标记
lazy[p] = 0;
}
// 建树
void build(int p, int l, int r, vector<long long>& a) {
if (l == r) { sum[p] = a[l]; return; }
int mid = (l + r) >> 1;
build(p<<1, l, mid, a);
build(p<<1|1, mid+1, r, a);
pushUp(p);
}
// 区间修改
void rangeAdd(int p, int l, int r, int ul, int ur, long long val) {
if (ul <= l && r <= ur) {
sum[p] += val * (r - l + 1);
lazy[p] += val;
return;
}
pushDown(p, l, r);
int mid = (l + r) >> 1;
if (ul <= mid) rangeAdd(p<<1, l, mid, ul, ur, val);
if (ur > mid) rangeAdd(p<<1|1, mid+1, r, ul, ur, val);
pushUp(p);
}
// 区间查询
long long rangeQuery(int p, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return sum[p];
pushDown(p, l, r);
int mid = (l + r) >> 1;
long long res = 0;
if (ql <= mid) res += rangeQuery(p<<1, l, mid, ql, qr);
if (qr > mid) res += rangeQuery(p<<1|1, mid+1, r, ql, qr);
return res;
}
public:
SegmentTree(vector<long long>& a) {
n = a.size() - 1;
sum.resize(4 * n);
lazy.resize(4 * n, 0);
build(1, 1, n, a);
}
void add(int l, int r, long long val) { rangeAdd(1, 1, n, l, r, val); }
long long query(int l, int r) { return rangeQuery(1, 1, n, l, r); }
};
// 测试
int main() {
vector<long long> a = {0,1,2,3,4,5}; // 下标从1开始
SegmentTree st(a);
st.add(1,3,2); // [1,3]加2 → 3,4,5,4,5
cout << st.query(1,5); // 输出21
return 0;
}
4. 单调栈
📌 核心用途
线性时间 \(O(n)\) 找到每个元素 左 / 右边 第一个比它大 / 小的元素,解决矩形面积、接雨水、下一个更大元素等问题。
🎯 适用题型
- 基础:下一个更大 / 更小元素、每日温度
- 进阶:柱状图最大矩形、接雨水、矩形最大面积
- 拓展:环形数组单调栈、区间最值维护
💻 拓展用法:柱状图最大矩形(经典高频题)
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
// 单调栈:柱状图中最大的矩形(核心拓展题型)
int largestRectangleArea(vector<int>& heights) {
stack<int> st; // 单调递增栈:存储下标,对应高度递增
st.push(-1); // 哨兵:避免栈空判断
int maxArea = 0, n = heights.size();
for (int i = 0; i < n; i++) {
// 当前高度 < 栈顶高度:弹出栈顶,计算以栈顶为高的矩形面积
while (st.top() != -1 && heights[i] < heights[st.top()]) {
int h = heights[st.top()]; st.pop();
int w = i - st.top() - 1; // 宽度 = 右边界 - 左边界 -1
maxArea = max(maxArea, h * w);
}
st.push(i);
}
// 处理栈中剩余元素
while (st.top() != -1) {
int h = heights[st.top()]; st.pop();
int w = n - st.top() - 1;
maxArea = max(maxArea, h * w);
}
return maxArea;
}
// 测试
int main() {
vector<int> h = {2,1,5,6,2,3};
cout << largestRectangleArea(h); // 输出10
return 0;
}
5. 单调队列
📌 核心用途
线性时间 \(O(n)\) 维护滑动窗口内的最值,是解决滑动窗口问题的最优解,也可优化动态规划。
🎯 适用题型
- 基础:滑动窗口最大值 / 最小值
- 进阶:单调队列优化 DP(多重背包、线性 DP)
💻 拓展用法:滑动窗口最大值(标准模板)
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
// 单调队列:滑动窗口最大值
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q; // 单调递减队列:存储下标,对应数值递减
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
// 1. 移除窗口外的元素(左边界出窗)
while (!q.empty() && q.front() <= i - k) q.pop_front();
// 2. 维护单调递减:移除队尾比当前元素小的元素
while (!q.empty() && nums[i] >= nums[q.back()]) q.pop_back();
q.push_back(i);
// 3. 窗口形成后,记录队首(最大值)
if (i >= k - 1) res.push_back(nums[q.front()]);
}
return res;
}
// 测试
int main() {
vector<int> nums = {1,3,-1,-3,5,3,6,7};
vector<int> ans = maxSlidingWindow(nums, 3);
for (int x : ans) cout << x << " "; // 输出3 3 5 5 6 7
return 0;
}
二、图论
1. 最短路径算法
📌 核心用途
求图中单源 / 多源最短路径长度,判断负权环;分 4 种算法:
- Dijkstra:无负权边的单源最短路
- SPFA:有负权边 / 判负环
- Floyd:全源最短路
🎯 适用题型
单源最短路、多源最短路、负权边、负环判断、最短路计数、路径还原
💻 拓展用法:堆优化 Dijkstra(最常用)+ Floyd(全源)
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
// ===================== 1. 堆优化Dijkstra(无负权单源最短路) =====================
vector<int> dijkstra(int start, int n, vector<vector<pair<int, int>>>& g) {
vector<int> dis(n + 1, INF); // 距离数组:初始无穷大
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq; // 小根堆
dis[start] = 0;
pq.emplace(0, start);
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dis[u]) continue; // 已更新过,跳过
for (auto [v, w] : g[u]) {
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
pq.emplace(dis[v], v);
}
}
}
return dis;
}
// ===================== 2. Floyd(全源最短路) =====================
void floyd(int n, vector<vector<int>>& g) {
for (int k = 1; k <= n; k++) // 中间节点
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
// 测试
int main() {
int n = 3;
vector<vector<pair<int, int>>> g(n + 1);
g[1].emplace_back(2,1); g[2].emplace_back(3,2); g[1].emplace_back(3,5);
vector<int> dis = dijkstra(1, n, g);
cout << dis[3]; // 输出3
return 0;
}
2. 最小生成树(MST)
📌 核心用途
求无向连通图中连接所有节点的总权值最小的边集,总权值最小。
🎯 适用题型
最小生成树、最小代价连通、次小生成树、最小瓶颈生成树
💻 拓展用法:Kruskal 算法(并查集实现,最常用)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 并查集(复用前文)
struct DSU {
vector<int> fa;
DSU(int n) { fa.resize(n+1); for(int i=1;i<=n;i++)fa[i]=i; }
int find(int x) { return fa[x]==x ? x : fa[x]=find(fa[x]); }
bool unite(int x,int y) {
int rx=find(x), ry=find(y);
if(rx==ry) return false;
fa[rx]=ry; return true;
}
};
// 边结构体
struct Edge { int u, v, w; };
// Kruskal算法:最小生成树
int kruskal(int n, vector<Edge>& edges) {
sort(edges.begin(), edges.end(), [](Edge& a, Edge& b){return a.w < b.w;}); // 边按权值升序
DSU dsu(n);
int total = 0, cnt = 0; // total:总权值,cnt:选中的边数
for (auto& e : edges) {
if (dsu.unite(e.u, e.v)) { // 不连通则选中
total += e.w;
cnt++;
if (cnt == n - 1) break; // 生成树有n-1条边
}
}
return total;
}
// 测试
int main() {
int n=3;
vector<Edge> e = {{1,2,1},{2,3,2},{1,3,5}};
cout << kruskal(n,e); // 输出3
return 0;
}
3. 拓扑排序
📌 核心用途
对有向无环图(DAG)进行线性排序,判断图是否有环,解决任务依赖 / 调度问题。
🎯 适用题型
课程表、任务调度、判断有向环、依赖关系排序
💻 拓展用法:Kahn 算法(入度 + 队列,标准模板)
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// 拓扑排序:返回拓扑序列,若长度≠n则有环
vector<int> topoSort(int n, vector<vector<int>>& g) {
vector<int> inDegree(n + 1, 0); // 入度数组
for (int u = 1; u <= n; u++)
for (int v : g[u]) inDegree[v]++;
queue<int> q;
// 入度为0的节点入队
for (int i = 1; i <= n; i++)
if (inDegree[i] == 0) q.push(i);
vector<int> res;
while (!q.empty()) {
int u = q.front(); q.pop();
res.push_back(u);
for (int v : g[u]) {
inDegree[v]--;
if (inDegree[v] == 0) q.push(v);
}
}
return res;
}
// 测试
int main() {
int n=3;
vector<vector<int>> g(n+1);
g[1].push_back(2); g[2].push_back(3);
vector<int> ans = topoSort(n,g);
for(int x:ans) cout<<x<<" "; // 输出1 2 3
return 0;
}
4. 关键路径
📌 核心用途
在AOE 网(带权 DAG)中找工程最短完成时间和关键活动,定位工程瓶颈。
🎯 适用题型
工程调度、关键任务、DAG 最长路径
💻 拓展用法:拓扑排序 + 关键路径计算
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
// 关键路径算法(AOE网)
void criticalPath(int n, vector<vector<pair<int, int>>>& g) {
vector<int> in(n+1,0), out(n+1,0);
vector<int> ve(n+1,0), vl(n+1,INF); // ve:最早开始时间,vl:最晚开始时间
// 1. 拓扑排序求ve(最早时间)
queue<int> q; q.push(1); ve[1]=0;
vector<int> topo;
while(!q.empty()){
int u=q.front(); q.pop(); topo.push_back(u);
for(auto [v,w]:g[u]){
in[v]--; ve[v]=max(ve[v], ve[u]+w);
if(in[v]==0) q.push(v);
}
}
// 2. 逆拓扑求vl(最晚时间)
vl[topo.back()] = ve[topo.back()];
for(int i=topo.size()-1;i>=0;i--){
int u=topo[i];
for(auto [v,w]:g[u]) vl[u]=min(vl[u], vl[v]-w);
}
// 3. 输出关键路径(ve[u]==vl[u]的节点)
cout<<"关键路径:";
for(int x:topo) if(ve[x]==vl[x]) cout<<x<<" ";
}
// 测试
int main() {
int n=4;
vector<vector<pair<int, int>>> g(n+1);
g[1].emplace_back(2,3); g[1].emplace_back(3,2); g[2].emplace_back(4,2); g[3].emplace_back(4,3);
criticalPath(n,g); // 输出1 3 4
return 0;
}
5. 连通性问题(强连通分量 SCC)
📌 核心用途
找有向图的强连通分量,缩点(将分量转为一个点,简化图为 DAG),判断割点 / 桥。
🎯 适用题型
强连通分量、缩点、2-SAT、有向图连通性、割点 / 桥
💻 拓展用法:Tarjan 算法求 SCC + 缩点
#include <iostream>
#include <vector>
#include <stack>
#include <cstring>
using namespace std;
// Tarjan算法:求强连通分量
class TarjanSCC {
public:
vector<vector<int>> g, newG; // 原图、缩点后新图
vector<int> dfn, low, belong; // dfn:时间戳,low:追溯值,belong:所属分量
stack<int> st; vector<bool> inSt;
int idx, cnt; // idx:时间戳,cnt:分量编号
TarjanSCC(int n, vector<vector<int>>& graph) : g(graph) {
dfn.resize(n+1,0); low.resize(n+1,0); belong.resize(n+1,0); inSt.resize(n+1,false);
idx = cnt = 0;
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
buildNewGraph(n); // 缩点建图
}
void tarjan(int u) {
dfn[u]=low[u]=++idx; st.push(u); inSt[u]=true;
for(int v:g[u]){
if(!dfn[v]) tarjan(v), low[u]=min(low[u], low[v]);
else if(inSt[v]) low[u]=min(low[u], dfn[v]);
}
if(dfn[u]==low[u]){ // 找到SCC
cnt++; int v;
do{ v=st.top(); st.pop(); inSt[v]=false; belong[v]=cnt; }while(v!=u);
}
}
void buildNewGraph(int n) { // 缩点
newG.resize(cnt+1);
for(int u=1;u<=n;u++)
for(int v:g[u])
if(belong[u]!=belong[v])
newG[belong[u]].push_back(belong[v]);
}
};
// 测试
int main() {
int n=3;
vector<vector<int>> g(n+1);
g[1].push_back(2); g[2].push_back(3); g[3].push_back(2);
TarjanSCC scc(n,g);
cout<<scc.cnt; // 输出2(分量:{1},{2,3})
return 0;
}
三、字符串与搜索
1. KMP 算法
📌 核心用途
线性时间字符串模式匹配:\(O(n+m)\) 找到模式串在主串中的所有位置,避免暴力回溯。
🎯 适用题型
子串匹配、重复子串、字符串周期、前缀函数应用
💻 拓展用法:KMP 匹配 + 求最小循环节
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// KMP算法:模式匹配 + 最小循环节
class KMP {
public:
vector<int> ne; // 前缀函数数组
string s, p; // 主串、模式串
KMP(string& str, string& pat) : s(str), p(pat) {
int m = p.size(); ne.resize(m,0);
// 1. 预处理前缀函数
for(int i=1,j=0;i<m;i++){
while(j && p[i]!=p[j]) j=ne[j-1];
if(p[i]==p[j]) j++;
ne[i]=j;
}
}
// 2. 模式匹配:返回所有匹配起始下标
vector<int> match() {
vector<int> res;
int n=s.size(), m=p.size();
for(int i=0,j=0;i<n;i++){
while(j && s[i]!=p[j]) j=ne[j-1];
if(s[i]==p[j]) j++;
if(j==m) { res.push_back(i-m+1); j=ne[j-1]; }
}
return res;
}
// 3. 拓展:求模式串的最小循环节
int getMinCycle() {
int m=p.size();
int len = m - ne.back();
return m%len==0 ? len : m;
}
};
// 测试
int main() {
string s="ababaabab", p="abab";
KMP kmp(s,p);
vector<int> ans = kmp.match();
for(int x:ans) cout<<x<<" "; // 输出0 4
cout<<kmp.getMinCycle(); // 输出2(ab)
return 0;
}
2. Trie 字典树
📌 核心用途
高效存储 / 查询字符串:按字符前缀存储,支持前缀匹配、单词检索、去重;01-Trie可求数字异或最值。
🎯 适用题型
前缀匹配、单词检索、字符串去重、最大异或对
💻 拓展用法:字符串 Trie + 01-Trie(求最大异或对)
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// ===================== 1. 字符串Trie =====================
class Trie {
private:
vector<vector<int>> tr;
vector<int> cnt;
int idx;
public:
Trie() : tr(1, vector<int>(26,0)), cnt(1,0), idx(0) {}
// 插入字符串
void insert(string& s) {
int p=0;
for(char c:s){
int u=c-'a';
if(!tr[p][u]) tr.emplace_back(26,0), cnt.push_back(0), tr[p][u]=++idx;
p=tr[p][u];
}
cnt[p]++;
}
// 查询字符串是否存在
int query(string& s) {
int p=0;
for(char c:s){
int u=c-'a';
if(!tr[p][u]) return 0;
p=tr[p][u];
}
return cnt[p];
}
};
// ===================== 2. 01-Trie(拓展:求最大异或对) =====================
class Trie01 {
private:
vector<vector<int>> tr;
int idx;
public:
Trie01() : tr(1, vector<int>(2,0)), idx(0) {}
void insert(int x) {
int p=0;
for(int i=31;i>=0;i--){
int u=(x>>i)&1;
if(!tr[p][u]) tr.emplace_back(2,0), tr[p][u]=++idx;
p=tr[p][u];
}
}
int query(int x) {
int p=0, res=0;
for(int i=31;i>=0;i--){
int u=(x>>i)&1;
if(tr[p][!u]) res|=(1<<i), p=tr[p][!u];
else p=tr[p][u];
}
return res;
}
};
// 测试
int main() {
// 字符串Trie
Trie t; string s1="abc",s2="ab";
t.insert(s1); t.insert(s2);
cout<<t.query("ab")<<" "; // 输出1
// 01-Trie:最大异或对
Trie01 t01;
vector<int> nums={1,2,3};
for(int x:nums) t01.insert(x);
int maxXor=0;
for(int x:nums) maxXor=max(maxXor, t01.query(x));
cout<<maxXor; // 输出3
return 0;
}
3. 字符串哈希
📌 核心用途
将字符串转为唯一哈希值,\(O(1)\) 比较字符串是否相等,快速查询子串哈希,避免 KMP 的复杂前缀函数。
🎯 适用题型
子串判等、回文串判断、字符串匹配、最长重复子串
💻 拓展用法:自然溢出哈希 + 子串查询(防冲突)
#include <iostream>
#include <vector>
#include <string>
using namespace std;
typedef unsigned long long ull;
const int base = 131; // 进制基数
// 字符串哈希:自然溢出 + 子串哈希查询
class StringHash {
public:
vector<ull> h, p; // h:哈希前缀和,p:基数幂
StringHash(string& s) {
int n=s.size();
h.resize(n+1,0); p.resize(n+1,1);
for(int i=0;i<n;i++){
h[i+1]=h[i]*base + s[i];
p[i+1]=p[i]*base;
}
}
// 查询子串s[l..r]的哈希值(下标从0开始)
ull getHash(int l, int r) {
return h[r+1] - h[l]*p[r-l+1];
}
};
// 测试
int main() {
string s="abcabc";
StringHash sh(s);
// 判断s[0..2]和s[3..5]是否相等
if(sh.getHash(0,2)==sh.getHash(3,5)) cout<<"相等"; // 输出相等
return 0;
}
四、高级技巧与优化思想
1. 倍增算法(ST 表 + LCA)
📌 核心用途
- ST 表:\(O(nlogn)\) 预处理,\(O(1)\) 查询静态区间最值(无修改)
- LCA:\(O(nlogn)\) 预处理,\(O(logn)\) 查询树上最近公共祖先
🎯 适用题型
静态区间最值、树上路径查询、LCA 求两点距离
💻 拓展用法:ST 表(区间最值)+ 倍增 LCA
#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>
using namespace std;
const int LOG = 20;
// ===================== 1. ST表:静态区间最值 =====================
class ST {
public:
vector<vector<int>> st;
vector<int> logn;
ST(vector<int>& a) {
int n=a.size()-1;
logn.resize(n+1); st.resize(LOG, vector<int>(n+1));
// 预处理log2值
logn[1]=0; for(int i=2;i<=n;i++) logn[i]=logn[i/2]+1;
// 初始化
for(int i=1;i<=n;i++) st[0][i]=a[i];
// 倍增预处理
for(int j=1;j<LOG;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
st[j][i]=max(st[j-1][i], st[j-1][i+(1<<(j-1))]);
}
// 查询区间[l,r]最大值
int query(int l, int r) {
int k=logn[r-l+1];
return max(st[k][l], st[k][r-(1<<k)+1]);
}
};
// ===================== 2. 倍增LCA:树上最近公共祖先 =====================
class LCA {
public:
vector<vector<int>> g, up;
vector<int> dep;
int n;
LCA(int size, vector<vector<int>>& graph) : n(size), g(graph) {
up.resize(LOG, vector<int>(n+1));
dep.resize(n+1,0);
dfs(1,0); // 根节点1,父节点0
}
void dfs(int u, int fa) {
up[0][u]=fa; dep[u]=dep[fa]+1;
for(int j=1;j<LOG;j++) up[j][u]=up[j-1][up[j-1][u]];
for(int v:g[u]) if(v!=fa) dfs(v,u);
}
int lca(int u, int v) {
if(dep[u]<dep[v]) swap(u,v);
// 把u提到和v同一深度
for(int j=LOG-1;j>=0;j--)
if(dep[up[j][u]]>=dep[v]) u=up[j][u];
if(u==v) return u;
// 一起向上跳
for(int j=LOG-1;j>=0;j--)
if(up[j][u]!=up[j][v]) u=up[j][u], v=up[j][v];
return up[0][u];
}
};
// 测试
int main() {
// ST表测试
vector<int> a={0,1,3,2,5,4};
ST st(a);
cout<<st.query(2,5)<<" "; // 输出5
// LCA测试
int n=5;
vector<vector<int>> g(n+1);
g[1].push_back(2); g[1].push_back(3); g[2].push_back(4); g[2].push_back(5);
LCA lca(n,g);
cout<<lca.lca(4,5); // 输出2
return 0;
}
2. 快速幂
📌 核心用途
\(O(logb)\) 快速计算 \(a^b \mod mod\),避免暴力循环;矩阵快速幂可优化线性递推(如斐波那契)。
🎯 适用题型
幂运算取模、矩阵快速幂、乘法逆元、斐波那契优化
💻 拓展用法:快速幂 + 矩阵快速幂
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int mod = 1e9+7;
// ===================== 1. 快速幂 =====================
ll qpow(ll a, ll b) {
ll res=1;
while(b) {
if(b&1) res=res*a%mod; // 二进制位为1,乘入结果
a=a*a%mod; // 底数平方
b>>=1; // 右移一位
}
return res;
}
// ===================== 2. 矩阵快速幂(拓展:斐波那契) =====================
typedef vector<vector<ll>> Matrix;
Matrix mul(Matrix& a, Matrix& b) { // 矩阵乘法
Matrix res(2, vector<ll>(2,0));
for(int i=0;i<2;i++)
for(int j=0;j<2;j++)
for(int k=0;k<2;k++)
res[i][j]=(res[i][j]+a[i][k]*b[k][j])%mod;
return res;
}
Matrix matrixQpow(Matrix a, ll n) { // 矩阵快速幂
Matrix res={{1,0},{0,1}}; // 单位矩阵
while(n) {
if(n&1) res=mul(res,a);
a=mul(a,a);
n>>=1;
}
return res;
}
// 求斐波那契第n项
ll fib(ll n) {
if(n<=2) return 1;
Matrix a={{1,1},{1,0}};
Matrix res=matrixQpow(a,n-2);
return (res[0][0]+res[0][1])%mod;
}
// 测试
int main() {
cout<<qpow(2,10)<<" "; // 输出1024
cout<<fib(10); // 输出55
return 0;
}
3. 离散化
📌 核心用途
将大范围数值(如 1e9)映射为小范围连续下标(如 1e5),节省空间,适配树状数组 / 线段树。
🎯 适用题型
数据范围大但元素少、逆序对、权值线段树、区间覆盖
💻 拓展用法:去重离散化(标准模板)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 离散化:大数值→小下标
vector<int> discretization(vector<int>& a) {
vector<int> b = a; // 备份数组
sort(b.begin(), b.end()); // 排序
b.erase(unique(b.begin(), b.end()), b.end()); // 去重
// 映射:a[i] → 离散化后的下标
for(int& x:a) x=lower_bound(b.begin(), b.end(),x)-b.begin()+1; // +1避免0下标
return a;
}
// 测试
int main() {
vector<int> a={100,20,300,20,50};
vector<int> ans=discretization(a);
for(int x:ans) cout<<x<<" "; // 输出3 1 4 1 2
return 0;
}
4. 树上前缀和与差分
📌 核心用途
树上路径修改 / 查询:差分\(O(1)\) 完成路径修改,前缀和\(O(n)\) 统计结果,简化树上复杂操作。
🎯 适用题型
树上路径加、路径求和、子树修改、树上差分优化
💻 拓展用法:树上点差分(路径修改 + 查询)
#include <iostream>
#include <vector>
using namespace std;
// 树上点差分:路径[u,v]加val + 单点查询
class TreeDiff {
public:
vector<vector<int>> g;
vector<int> val, dep, fa;
int n;
TreeDiff(int size, vector<vector<int>>& graph) : n(size), g(graph) {
val.resize(n+1,0); dep.resize(n+1,0); fa.resize(n+1,0);
dfs(1,0); // 预处理父节点+深度
}
void dfs(int u, int f) {
fa[u]=f; dep[u]=dep[f]+1;
for(int v:g[u]) if(v!=f) dfs(v,u);
}
// 倍增LCA(复用前文)
int lca(int u, int v) {
if(dep[u]<dep[v]) swap(u,v);
while(dep[u]>dep[v]) u=fa[u];
while(u!=v) u=fa[u],v=fa[v];
return u;
}
// 路径[u,v]加val
void addPath(int u, int v, int num) {
int ancestor=lca(u,v);
val[u]+=num;
val[v]+=num;
val[ancestor]-=num;
if(fa[ancestor]) val[fa[ancestor]]-=num;
}
// 回溯统计:后序遍历求和
void dfsSum(int u, int f) {
for(int v:g[u]) if(v!=f) dfsSum(v,u), val[u]+=val[v];
}
};
// 测试
int main() {
int n=5;
vector<vector<int>> g(n+1);
g[1].push_back(2); g[1].push_back(3); g[2].push_back(4); g[2].push_back(5);
TreeDiff td(n,g);
td.addPath(4,5,2); // 路径4-2-5加2
td.dfsSum(1,0);
cout<<td.val[2]; // 输出2
return 0;
}
总结
- 数据结构:并查集管连通、树状数组 / 线段树管区间、单调栈 / 队列管线性最值;
- 图论:最短路径求距离、生成树求最小连通、拓扑 / 关键路径管调度、连通性管缩点;
- 字符串:KMP 精准匹配、Trie 管前缀、哈希快速判等;
- 高级技巧:倍增提速查询、快速幂提速幂运算、离散化压缩数据、树上差分简化路径操作。
更多推荐



所有评论(0)