ACM竞赛常用算法详解(初学者友好版)
目录
基础数据结构
1. 并查集
基本原理: 并查集是一种用于处理"元素分组"问题的数据结构。它主要支持两种操作:
- 查询(Find):确定某个元素属于哪个组
- 合并(Union):将两个组合并成一个组
想象一下,你有一群人,你想知道谁和谁是朋友关系。每次你发现两个人是朋友,你就记录下来,而且朋友的朋友也是朋友。并查集就是用来高效管理这种关系的。
实现代码:
// MAXN是最大元素数量
int fa[MAXN];
// 初始化:每个元素开始时都是独立的集合(自己的父节点是自己)
void init(int n) {
for (int i = 1; i <= n; i++) fa[i] = i;
}
// 查找根节点(代表元素):使用路径压缩优化
// 路径压缩:在查找的过程中,将沿途的每个节点都直接连到根节点,减少后续查询的路径长度
int find(int x) {
// 如果x是根节点,返回x;否则递归查找x的父节点,并更新x的父节点为根节点
return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
// 合并两个集合:将一个集合的根节点指向另一个集合的根节点
void unite(int x, int y) {
// 先找到x和y所在集合的根节点
fa[find(x)] = find(y); // 将x的根节点指向y的根节点
}
应用场景:
- 判断图的连通性:可以确定任意两点是否连通
- Kruskal最小生成树算法:用于判断加入一条边是否会形成环
- 动态连通性问题:维护网络中的连通关系
- 好友关系网络:管理社交网络中的朋友圈
2. 线段树
基本原理: 线段树是一种二叉树形数据结构,用于高效处理区间查询和修改操作。每个节点代表一个区间,父节点的区间是子节点区间的并集。
想象你有一排数字需要频繁地进行区间求和或查找最大/最小值,线段树可以让这些操作变得高效。
实现代码(含详细注释):
struct SegTree {
int n; // 数组长度
vector<int> sum; // 存储区间和
vector<int> lazy; // 懒标记,延迟更新
// 构建线段树
// node: 当前节点编号,通常从1开始
// start, end: 当前节点表示的区间范围
// arr: 原始数组
void build(int node, int start, int end, vector<int>& arr) {
// 如果是叶子节点(表示单个元素)
if (start == end) {
sum[node] = arr[start]; // 直接赋值
return;
}
// 找到中间点,将区间分为左右两部分
int mid = (start + end) / 2;
// 递归构建左子树(左半区间)
build(node * 2, start, mid, arr);
// 递归构建右子树(右半区间)
build(node * 2 + 1, mid + 1, end, arr);
// 当前节点的值 = 左子节点的值 + 右子节点的值
sum[node] = sum[node * 2] + sum[node * 2 + 1];
}
// 区间更新操作
// node: 当前节点编号
// start, end: 当前节点的区间范围
// l, r: 要更新的区间范围
// val: 更新的值(这里是加上val)
void update(int node, int start, int end, int l, int r, int val) {
// 先处理懒标记:如果当前节点有未传递的更新
if (lazy[node] != 0) {
// 更新当前节点的值:区间长度 * 懒标记值
sum[node] += (end - start + 1) * lazy[node];
// 如果不是叶子节点,则将懒标记传递给子节点
if (start != end) {
lazy[node * 2] += lazy[node]; // 传给左子节点
lazy[node * 2 + 1] += lazy[node]; // 传给右子节点
}
// 清除当前节点的懒标记
lazy[node] = 0;
}
// 如果当前区间完全在目标区间外,直接返回
if (start > r || end < l) return;
// 如果当前区间完全包含在目标区间内
if (l <= start && end <= r) {
// 更新当前节点的值
sum[node] += (end - start + 1) * val;
// 如果不是叶子节点,设置子节点的懒标记
if (start != end) {
lazy[node * 2] += val;
lazy[node * 2 + 1] += val;
}
return;
}
// 如果区间有部分重叠,则分别处理左右子区间
int mid = (start + end) / 2;
update(node * 2, start, mid, l, r, val); // 更新左子树
update(node * 2 + 1, mid + 1, end, l, r, val); // 更新右子树
// 更新当前节点的值为子节点值的和
sum[node] = sum[node * 2] + sum[node * 2 + 1];
}
// 区间查询操作
// 参数同update,返回l到r区间的和
int query(int node, int start, int end, int l, int r) {
// 如果当前区间在查询区间外
if (start > r || end < l) return 0;
// 处理懒标记
if (lazy[node] != 0) {
sum[node] += (end - start + 1) * lazy[node];
if (start != end) {
lazy[node * 2] += lazy[node];
lazy[node * 2 + 1] += lazy[node];
}
lazy[node] = 0;
}
// 如果当前区间完全包含在查询区间内
if (l <= start && end <= r) return sum[node];
// 部分重叠,递归查询左右子区间
int mid = (start + end) / 2;
int p1 = query(node * 2, start, mid, l, r); // 查询左子树
int p2 = query(node * 2 + 1, mid + 1, end, l, r); // 查询右子树
return p1 + p2; // 返回左右子树查询结果的和
}
};
应用场景:
- 区间求和:快速计算一段区间内所有元素的和
- 区间最大/最小值:查询区间内的最值
- 区间更新:对一段区间内的所有元素进行同一操作(如加上一个值)
- 动态维护数组:当数组频繁变动且需要频繁查询区间信息时
3. 单调队列
基本原理: 单调队列是一种特殊的队列,其中的元素保持单调递增或递减顺序。它主要用于解决滑动窗口类问题,特别是需要找出窗口内最大/最小值的问题。
实现代码:
// 以求滑动窗口最大值为例
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
deque<int> q; // 存储下标,而不是值
for (int i = 0; i < nums.size(); i++) {
// 移除队首超出窗口范围的元素(下标小于i-k+1的)
while (!q.empty() && q.front() < i - k + 1) {
q.pop_front();
}
// 从队尾移除所有小于当前元素的值(维持单调递减)
while (!q.empty() && nums[q.back()] < nums[i]) {
q.pop_back();
}
// 将当前元素的下标加入队列
q.push_back(i);
// 当窗口形成后,记录当前窗口的最大值(队首元素)
if (i >= k - 1) {
result.push_back(nums[q.front()]);
}
}
return result;
}
应用场景:
- 滑动窗口最大/最小值:O(n)时间复杂度内求解
- 单调性优化问题:如凸包问题中的单调栈应用
- 某些DP优化:利用单调性减少遍历范围
4. 栈与队列的基础应用
栈的应用:后进先出(LIFO)
// 检查括号是否匹配
bool isValid(string s) {
stack<char> st;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') {
// 左括号直接入栈
st.push(c);
} else {
// 右括号需要检查是否匹配
if (st.empty()) return false; // 栈空但出现右括号,不匹配
char top = st.top();
if ((c == ')' && top == '(') ||
(c == ']' && top == '[') ||
(c == '}' && top == '{')) {
st.pop(); // 匹配成功,弹出左括号
} else {
return false; // 不匹配
}
}
}
return st.empty(); // 栈为空表示所有括号都匹配
}
队列的应用:先进先出(FIFO)
// 广度优先搜索的示例用法
void bfs(vector<vector<int>>& graph, int start) {
int n = graph.size();
vector<bool> visited(n, false);
queue<int> q;
// 初始节点
q.push(start);
visited[start] = true;
while (!q.empty()) {
int curr = q.front();
q.pop();
cout << "访问节点: " << curr << endl;
// 访问相邻节点
for (int next : graph[curr]) {
if (!visited[next]) {
q.push(next);
visited[next] = true;
}
}
}
}
图论算法
1. 最短路径
Dijkstra算法
基本原理: Dijkstra算法用于计算从一个顶点到其他所有顶点的最短路径,适用于边权为非负的有向或无向图。
该算法的核心思想是贪心:每次选择还未确定最短路径的点中,路径估计值最小的点进行扩展。
实现代码:
// 使用优先队列优化的Dijkstra算法
// graph[u] 存储顶点u的所有邻接点及边权,格式为pair<顶点,权重>
// dist数组存储从起点到各点的最短距离
void dijkstra(int start, vector<vector<pair<int, int>>>& graph, vector<int>& dist) {
int n = graph.size(); // 顶点数量
dist.assign(n, INT_MAX); // 初始化距离为无穷大
dist[start] = 0; // 起点到自身的距离为0
// 优先队列,按距离从小到大排序,存储pair<距离,顶点>
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
pq.push({0, start}); // 加入起点
while (!pq.empty()) {
int d = pq.top().first; // 当前最短距离
int u = pq.top().second; // 当前顶点
pq.pop();
// 如果已经找到更短的路径,跳过
if (d > dist[u]) continue;
// 遍历所有邻接点
for (auto& edge : graph[u]) {
int v = edge.first; // 邻接点
int weight = edge.second; // 边权
// 如果通过u到达v的路径更短,则更新距离
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.push({dist[v], v}); // 加入新的路径
}
}
}
// 最终dist数组包含从start到所有点的最短路径长度
}
应用场景:
- 导航系统:计算从起点到终点的最短路径
- 网络路由:数据包的最短路径路由
- 任意带权图的单源最短路径问题
Floyd-Warshall算法
基本原理: Floyd-Warshall算法用于计算图中所有顶点对之间的最短路径,适用于任何没有负环的图(包含负权边的情况)。
该算法通过动态规划的方式,逐步考虑是否通过中间点k来改善i到j的路径。
实现代码:
// dist[i][j]表示从顶点i到顶点j的最短路径长度
// 初始时,dist[i][j]为边(i,j)的权重,若不存在则为无穷大,dist[i][i]=0
void floyd(vector<vector<int>>& dist) {
int n = dist.size(); // 顶点数量
// k是中间点
for (int k = 0; k < n; k++) {
// i是起点
for (int i = 0; i < n; i++) {
// j是终点
for (int j = 0; j < n; j++) {
// 如果通过k能使i到j的路径更短,则更新
// 需要检查避免溢出
if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
// 最终dist[i][j]包含从i到j的最短路径长度
}
应用场景:
- 所有点对最短路径:需要计算图中任意两点间的最短距离
- 传递闭包:判断从任意点i是否能到达点j
- 检测负环:如果dist[i][i]<0,则图中存在负环
2. 最小生成树
Kruskal算法
基本原理: Kruskal算法是基于贪心策略的最小生成树算法,它从小到大遍历所有边,每次加入不会形成环的最小边。通过并查集判断是否会形成环。
实现代码:
// 边结构体
struct Edge {
int u, v, weight; // 起点,终点,权重
// 按权重升序排序
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
// Kruskal最小生成树算法
// 返回最小生成树的总权重,n为顶点数,edges为所有边
int kruskal(int n, vector<Edge>& edges) {
// 对边按权重排序
sort(edges.begin(), edges.end());
int cost = 0; // 最小生成树的总权重
vector<int> parent(n + 1); // 并查集父节点数组
// 初始化并查集
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
// 并查集的查找函数(带路径压缩)
auto find = [&](int x) -> int {
return x == parent[x] ? x : (parent[x] = find(parent[x]));
};
// 遍历所有边
for (Edge& e : edges) {
int u = find(e.u); // 边的起点所在集合
int v = find(e.v); // 边的终点所在集合
// 如果不在同一集合(不会形成环)
if (u != v) {
parent[u] = v; // 合并集合
cost += e.weight; // 将边的权重加入总成本
}
}
return cost; // 返回最小生成树的总权重
}
应用场景:
- 网络设计:最小成本连接所有节点
- 电路设计:最小化连线总长度
- 聚类:通过删除最小生成树的较长边进行聚类
Prim算法
基本原理: Prim算法也是求最小生成树的算法,但与Kruskal不同,它从一个顶点开始,每次选择一个与当前树相连的最小权重边。
实现代码:
// Prim算法求最小生成树
// graph[i] 是顶点i的所有邻接点及边权,格式为pair<顶点,权重>
int prim(vector<vector<pair<int, int>>>& graph) {
int n = graph.size();
vector<bool> visited(n, false); // 记录顶点是否在树中
vector<int> minEdge(n, INT_MAX); // 从树到每个顶点的最小边权
int totalWeight = 0; // 最小生成树的总权重
minEdge[0] = 0; // 选择顶点0作为起点
for (int i = 0; i < n; i++) {
// 找到还未加入树的顶点中,到树距离最小的顶点
int u = -1;
for (int j = 0; j < n; j++) {
if (!visited[j] && (u == -1 || minEdge[j] < minEdge[u])) {
u = j;
}
}
// 将顶点u加入树
visited[u] = true;
totalWeight += minEdge[u];
// 更新到其他顶点的最小距离
for (auto& edge : graph[u]) {
int v = edge.first;
int weight = edge.second;
if (!visited[v] && weight < minEdge[v]) {
minEdge[v] = weight; // 更新到v的最小距离
}
}
}
return totalWeight;
}
应用场景:
- 与Kruskal算法类似,但当图较稠密时更为高效
- 实时网络的构建(因为可以增量式地构建树)
3. 拓扑排序
基本原理: 拓扑排序是对有向无环图(DAG)的所有顶点进行线性排序,使得图中任何一对顶点(u,v),若存在边u->v,则排序中u必须在v之前。
实现代码:
// graph[i] 是顶点i的所有出边(后继顶点)
vector<int> topologicalSort(vector<vector<int>>& graph) {
int n = graph.size(); // 顶点数量
vector<int> inDegree(n, 0); // 记录每个顶点的入度
// 计算每个顶点的入度
for (int u = 0; u < n; u++) {
for (int v : graph[u]) {
inDegree[v]++; // u->v,所以v的入度加1
}
}
// 将所有入度为0的顶点加入队列(初始可以直接处理的顶点)
queue<int> q;
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
q.push(i);
}
}
vector<int> result; // 存储拓扑排序结果
// BFS过程
while (!q.empty()) {
int u = q.front(); // 取出一个入度为0的顶点
q.pop();
result.push_back(u); // 加入排序结果
// 将所有u的后继顶点的入度减1
for (int v : graph[u]) {
if (--inDegree[v] == 0) { // 如果v的入度变为0,则加入队列
q.push(v);
}
}
}
// 如果无法完成所有顶点的排序,说明图中有环
if (result.size() != n) {
return {}; // 返回空数组表示存在环
}
return result; // 返回拓扑排序结果
}
应用场景:
- 任务调度:确定依赖任务的执行顺序
- 编译顺序:确定代码文件的编译顺序
- 课程安排:确定先修课程和后修课程的顺序
4. 深度优先搜索 (DFS)
基本原理: 深度优先搜索是一种图遍历算法,它尽可能深地搜索树或图的分支,当无法继续前进时回溯。
实现代码:
// 图的邻接表表示:graph[i] 表示顶点i的所有邻接点
void dfs(vector<vector<int>>& graph, int start) {
int n = graph.size(); // 顶点数
vector<bool> visited(n, false); // 记录顶点是否被访问
// 递归DFS函数
function<void(int)> dfsRecursive = [&](int u) {
// 标记当前顶点为已访问
visited[u] = true;
cout << "访问顶点: " << u << endl; // 处理当前顶点
// 递归访问所有未访问的邻接顶点
for (int v : graph[u]) {
if (!visited[v]) {
dfsRecursive(v);
}
}
};
// 从起点开始DFS
dfsRecursive(start);
// 如果图不连通,处理剩余的连通分量
for (int i = 0; i < n; i++) {
if (!visited[i]) {
dfsRecursive(i);
}
}
}
// 非递归实现的DFS,使用栈
void dfsIterative(vector<vector<int>>& graph, int start) {
int n = graph.size();
vector<bool> visited(n, false);
stack<int> s;
s.push(start);
visited[start] = true;
cout << "访问顶点: " << start << endl;
while (!s.empty()) {
int u = s.top();
// 寻找未访问的邻接点
bool foundUnvisited = false;
for (int v : graph[u]) {
if (!visited[v]) {
visited[v] = true;
cout << "访问顶点: " << v << endl;
s.push(v);
foundUnvisited = true;
break;
}
}
// 如果没有未访问的邻接点,弹出栈顶
if (!foundUnvisited) {
s.pop();
}
}
}
应用场景:
- 路径搜索:寻找从一个点到另一个点的路径
- 连通分量:找出图中的所有连通分量
- 拓扑排序:通过DFS可以实现拓扑排序
- 环检测:可以用来检测图中是否存在环
5. 广度优先搜索 (BFS)
基本原理: 广度优先搜索是一种层次遍历算法,它首先访问起点的所有邻接点,然后再访问这些邻接点的所有邻接点,依此类推。
实现代码:
// 图的邻接表表示:graph[i] 表示顶点i的所有邻接点
void bfs(vector<vector<int>>& graph, int start) {
int n = graph.size(); // 顶点数
vector<bool> visited(n, false); // 记录顶点是否被访问
queue<int> q; // BFS使用队列
// 将起点加入队列并标记为已访问
q.push(start);
visited[start] = true;
// 进行BFS
while (!q.empty()) {
int u = q.front(); // 取出队首顶点
q.pop();
cout << "访问顶点: " << u << endl; // 处理当前顶点
// 访问所有未访问的邻接顶点
for (int v : graph[u]) {
if (!visited[v]) {
q.push(v); // 将顶点加入队列
visited[v] = true; // 标记为已访问
}
}
}
// 如果图不连通,处理剩余的连通分量
for (int i = 0; i < n; i++) {
if (!visited[i]) {
q.push(i);
visited[i] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
cout << "访问顶点: " << u << endl;
for (int v : graph[u]) {
if (!visited[v]) {
q.push(v);
visited[v] = true;
}
}
}
}
}
}
// 计算从起点到各点的最短路径长度(无权图)
vector<int> bfsShortestPath(vector<vector<int>>& graph, int start) {
int n = graph.size();
vector<int> distance(n, -1); // -1表示未访问
queue<int> q;
distance[start] = 0; // 起点到自己的距离为0
q.push(start);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : graph[u]) {
if (distance[v] == -1) { // 如果v未被访问
distance[v] = distance[u] + 1; // 更新距离
q.push(v); // 加入队列
}
}
}
return distance; // 返回从起点到各点的最短路径长度
}
应用场景:
- 最短路径:无权图中从起点到各点的最短路径
- 连通分量:找出图中的所有连通分量
- 层次遍历:按层次访问图中的节点
- 环检测:可以用来检测图中是否存在环
字符串算法
1. KMP算法
基本原理: KMP算法是一种高效的字符串匹配算法,通过预处理模式串,避免在匹配失败时的重复比较。
实现代码:
// 计算模式串的部分匹配表(LPS数组)
vector<int> computeLPS(string pattern) {
int m = pattern.size();
vector<int> lps(m, 0);
for (int i = 1, len = 0; i < m; ) {
if (pattern[i] == pattern[len]) {
lps[i++] = ++len;
} else if (len > 0) {
len = lps[len - 1];
} else {
lps[i++] = 0;
}
}
return lps;
}
// KMP字符串匹配算法
vector<int> kmp(string text, string pattern) {
vector<int> lps = computeLPS(pattern);
vector<int> positions;
int n = text.size(), m = pattern.size();
for (int i = 0, j = 0; i < n; ) {
if (text[i] == pattern[j]) {
i++; j++;
}
if (j == m) {
positions.push_back(i - j);
j = lps[j - 1];
} else if (i < n && text[i] != pattern[j]) {
if (j > 0) j = lps[j - 1];
else i++;
}
}
return positions;
}
应用场景:
- 字符串匹配:在文本中查找模式串出现的位置
- 文本编辑器:实现查找和替换功能
- 生物信息学:DNA序列比对
2. 字典树 (Trie)
基本原理: 字典树是一种用于高效存储和检索字符串集合的树形数据结构,每个节点代表一个字符。
实现代码:
struct TrieNode {
TrieNode* children[26];
bool isEnd;
TrieNode() {
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
isEnd = false;
}
};
class Trie {
private:
TrieNode* root;
public:
Trie() {
root = new TrieNode();
}
// 插入单词
void insert(string word) {
TrieNode* current = root;
for (char c : word) {
int index = c - 'a';
if (!current->children[index]) {
current->children[index] = new TrieNode();
}
current = current->children[index];
}
current->isEnd = true;
}
// 查找单词
bool search(string word) {
TrieNode* node = findNode(word);
return node && node->isEnd;
}
// 查找前缀
bool startsWith(string prefix) {
return findNode(prefix) != nullptr;
}
private:
TrieNode* findNode(string key) {
TrieNode* current = root;
for (char c : key) {
int index = c - 'a';
if (!current->children[index]) {
return nullptr;
}
current = current->children[index];
}
return current;
}
};
应用场景:
- 前缀匹配:查找具有相同前缀的单词
- 自动补全:实现输入法的自动补全功能
- 拼写检查:快速查找单词是否存在
动态规划
1. 经典问题及解法
背包问题
0-1背包:
vector<int> dp(capacity + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = capacity; j >= weights[i]; j--) {
dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
}
}
完全背包:
vector<int> dp(capacity + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = weights[i]; j <= capacity; j++) {
dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
}
}
最长公共子序列 (LCS)
int lcs(string s1, string s2) {
int m = s1.size(), n = s2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1[i-1] == s2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
最长递增子序列 (LIS)
int lis(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
2. 状态优化技巧
- 状态压缩: 使用二进制表示集合状态,减少维度
- 滚动数组: 优化空间复杂度
- 斜率优化: 用于优化特定形式的DP转移方程
数学方法
1. 素数筛法
埃拉托斯特尼筛法:
vector<bool> sieve(int n) {
vector<bool> isPrime(n + 1, true);
isPrime[0] = isPrime[1] = false;
for (int i = 2; i * i <= n; i++) {
if (isPrime[i]) {
for (int j = i * i; j <= n; j += i) {
isPrime[j] = false;
}
}
}
return isPrime;
}
2. 快速幂
long long fastPow(long long base, long long exponent, long long mod) {
long long result = 1;
base %= mod;
while (exponent > 0) {
if (exponent & 1) result = (result * base) % mod;
base = (base * base) % mod;
exponent >>= 1;
}
return result;
}
3. 组合数学
组合数计算:
vector<vector<long long>> combination(int n, int mod) {
vector<vector<long long>> C(n + 1, vector<long long>(n + 1, 0));
for (int i = 0; i <= n; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++) {
C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod;
}
}
return C;
}
计算几何
1. 基础概念与操作
struct Point {
double x, y;
Point(double x = 0, double y = 0) : x(x), y(y) {}
};
// 计算两点间距离
double distance(Point a, Point b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
// 计算叉积
double cross(Point o, Point a, Point b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
// 判断点是否在多边形内
bool isPointInPolygon(Point p, const vector<Point>& polygon) {
int n = polygon.size();
bool inside = false;
for (int i = 0, j = n - 1; i < n; j = i++) {
if ((polygon[i].y > p.y) != (polygon[j].y > p.y) &&
(p.x < (polygon[j].x - polygon[i].x) * (p.y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x)) {
inside = !inside;
}
}
return inside;
}
2. 凸包算法 (Graham扫描法)
vector<Point> grahamScan(vector<Point> points) {
if (points.size() <= 3) return points;
// 找到y坐标最小的点
int n = points.size();
swap(points[0], *min_element(points.begin(), points.end(), [](Point a, Point b) {
return a.y < b.y || (a.y == b.y && a.x < b.x);
}));
// 按极角排序
Point p0 = points[0];
sort(points.begin() + 1, points.end(), [&p0](Point a, Point b) {
double cross = (a.x - p0.x) * (b.y - p0.y) - (b.x - p0.x) * (a.y - p0.y);
if (fabs(cross) < 1e-9) return distance(p0, a) < distance(p0, b);
return cross > 0;
});
// Graham扫描
vector<Point> hull;
hull.push_back(points[0]);
hull.push_back(points[1]);
for (int i = 2; i < n; i++) {
while (hull.size() > 1 && cross(hull[hull.size() - 2], hull.back(), points[i]) <= 0) {
hull.pop_back();
}
hull.push_back(points[i]);
}
return hull;
}
高级数据结构
1. 树状数组 (Binary Indexed Tree)
struct BIT {
vector<int> tree;
int n;
BIT(int size) {
n = size;
tree.resize(n + 1);
}
void update(int i, int val) {
while (i <= n) {
tree[i] += val;
i += i & (-i);
}
}
int query(int i) {
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= i & (-i);
}
return sum;
}
int rangeSum(int l, int r) {
return query(r) - query(l - 1);
}
};
2. ST表 (用于RMQ问题)
vector<vector<int>> buildSparseTable(const vector<int>& arr) {
int n = arr.size();
int logN = log2(n) + 1;
vector<vector<int>> st(n, vector<int>(logN));
for (int i = 0; i < n; i++) {
st[i][0] = arr[i];
}
for (int j = 1; j < logN; j++) {
for (int i = 0; i + (1 << j) - 1 < n; i++) {
st[i][j] = min(st[i][j-1], st[i + (1 << (j-1))][j-1]);
}
}
return st;
}
int queryMin(const vector<vector<int>>& st, int L, int R) {
int j = log2(R - L + 1);
return min(st[L][j], st[R - (1 << j) + 1][j]);
}
搜索技术
1. A*搜索
typedef pair<int, int> State; // (cost + heuristic, node)
int astar(vector<vector<int>>& graph, int start, int goal, function<int(int, int)> heuristic) {
priority_queue<State, vector<State>, greater<>> pq;
vector<int> cost(graph.size(), INT_MAX);
pq.push({0 + heuristic(start, goal), start});
cost[start] = 0;
while (!pq.empty()) {
int f = pq.top().first;
int node = pq.top().second;
pq.pop();
if (node == goal) return cost[goal];
if (f - heuristic(node, goal) > cost[node]) continue;
for (int i = 0; i < graph[node].size(); i++) {
int next = graph[node][i];
int newCost = cost[node] + 1; // 假设边权为1
if (newCost < cost[next]) {
cost[next] = newCost;
pq.push({newCost + heuristic(next, goal), next});
}
}
}
return -1; // 未找到路径
}
2. 迭代加深搜索 (IDA*)
bool dfs(int node, int depth, int limit, vector<vector<int>>& graph, vector<bool>& visited) {
if (depth > limit) return false;
if (isTarget(node)) return true;
visited[node] = true;
for (int next : graph[node]) {
if (!visited[next]) {
if (dfs(next, depth + 1, limit, graph, visited)) {
return true;
}
}
}
visited[node] = false;
return false;
}
int idaStar(vector<vector<int>>& graph, int start) {
int limit = 0;
while (limit < MAX_DEPTH) {
vector<bool> visited(graph.size(), false);
if (dfs(start, 0, limit, graph, visited)) {
return limit;
}
limit++;
}
return -1;
}
实战技巧与优化
1. 位运算优化
- 使用位运算代替乘除
- 快速判断奇偶:
n & 1
- 取模2^k:
n & ((1 << k) - 1)
- 快速幂: 二进制技巧
2. 减少常数
- 减少函数调用
- 使用数组而非链表
- 预计算和缓存结果
- 使用位运算替代算术运算
3. 预处理技巧
- 离散化
- 前缀和/差分
- RMQ预处理
4. STL使用技巧
- 使用unordered_map/set代替map/set降低时间复杂度
- 使用reserve预分配空间
- 避免频繁的push_front操作
- 使用emplace_back代替push_back
总结
本文详细介绍了ACM竞赛中常用的算法及其扩展应用,从基础数据结构到高级算法技巧。熟练掌握这些算法和技术,并理解其背后的原理,将有助于解决各种复杂的编程问题。算法竞赛不仅需要扎实的理论基础,还需要大量的实战练习和经验积累。希望这份资料能够帮助程序设计爱好者提升算法设计和实现能力。