第一次遇到最大子列和问题时,我本能地想到三重循环的暴力解法。结果在PTA上提交后直接TLE(时间超过限制),这才意识到算法效率的重要性。
常见误区:
最优解法(Kadane算法):
cpp复制int maxSubArray(vector<int>& nums) {
int max_sum = INT_MIN;
int current_sum = 0;
for (int num : nums) {
current_sum = max(num, current_sum + num);
max_sum = max(max_sum, current_sum);
}
return max_sum;
}
性能对比:
| 算法 | 时间复杂度 | 空间复杂度 | 适用数据规模 |
|---|---|---|---|
| 暴力 | O(n³) | O(1) | n ≤ 100 |
| 分治 | O(nlogn) | O(logn) | n ≤ 10⁴ |
| DP | O(n) | O(1) | n ≤ 10⁶ |
提示:当题目给出n ≤ 100000时,必须选择O(n)或O(nlogn)的算法
这道题让我深刻理解了递归思维在树问题中的应用。判断两棵树是否同构需要考虑多种情况:
易错点:
核心代码:
cpp复制bool isIsomorphic(Node* t1, Node* t2) {
if (!t1 && !t2) return true;
if (!t1 || !t2) return false;
if (t1->data != t2->data) return false;
return (isIsomorphic(t1->left, t2->left) &&
isIsomorphic(t1->right, t2->right)) ||
(isIsomorphic(t1->left, t2->right) &&
isIsomorphic(t1->right, t2->left));
}
这道题考察堆的基本操作和路径回溯。我最初犯的错误是试图先构建完整数组再调整成堆,结果发现题目要求的是插入时即时调整。
正确做法:
堆插入操作:
cpp复制void insert(int x) {
heap[++size] = x;
// 自底向上调整
for (int i = size; i > 1 && heap[i/2] > heap[i]; i /= 2) {
swap(heap[i], heap[i/2]);
}
}
路径查询示例:
code复制输入:5 (查询H[5]到根路径)
堆结构:[-,10,20,30,40,50]
输出:50 40 20 10
这道题需要计算每个节点六度范围内的节点比例,关键在于如何在BFS中记录层数信息。
实现技巧:
level数组记录每个节点的层数BFS核心代码:
cpp复制int bfs(int start) {
queue<pair<int, int>> q; // {节点, 层数}
vector<bool> visited(n+1, false);
int count = 1;
q.push({start, 0});
visited[start] = true;
while (!q.empty()) {
auto [node, level] = q.front();
q.pop();
if (level >= 6) continue;
for (int neighbor : adj[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
count++;
q.push({neighbor, level+1});
}
}
}
return count;
}
优化点:
这道题需要找到使最难变动物所需咒语最短的动物,本质上是图论中的多源最短路径问题。
算法选择:
Floyd算法实现:
cpp复制void floyd() {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
int findAnimal() {
int min_max = INF, animal = -1;
for (int i = 1; i <= n; i++) {
int max_dist = *max_element(dist[i]+1, dist[i]+n+1);
if (max_dist < min_max) {
min_max = max_dist;
animal = i;
}
}
return animal;
}
常见错误:
关键路径是AOE网中的最长路径,需要计算最早发生时间和最晚发生时间。
计算步骤:
核心代码片段:
cpp复制// 拓扑排序计算最早时间
vector<int> topoOrder;
queue<int> q;
for (int i = 1; i <= n; i++)
if (inDegree[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
topoOrder.push_back(u);
for (auto [v, time] : adj[u]) {
earliest[v] = max(earliest[v], earliest[u] + time);
if (--inDegree[v] == 0) q.push(v);
}
}
// 反向计算最晚时间
fill(latest.begin(), latest.end(), earliest[topoOrder.back()]);
for (int i = topoOrder.size()-1; i >= 0; i--) {
int u = topoOrder[i];
for (auto [v, time] : adj[u]) {
latest[u] = min(latest[u], latest[v] - time);
}
}
// 输出关键活动
for (int u = 1; u <= n; u++) {
for (auto [v, time] : adj[u]) {
if (earliest[u] == latest[v] - time) {
cout << u << "->" << v << endl;
}
}
}
PTA的排序题往往给出不同特性的测试数据,需要根据数据特点选择合适算法:
数据特征与算法选择:
| 数据类型 | 推荐算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 小规模随机数据 | 插入排序 | O(n²) | O(1) |
| 基本有序数据 | 冒泡排序 | O(n)~O(n²) | O(1) |
| 大规模随机数据 | 快速排序 | O(nlogn) | O(logn) |
| 数据范围较小 | 计数/桶排序 | O(n+k) | O(k) |
| 需要稳定排序 | 归并排序 | O(nlogn) | O(n) |
快速排序优化要点:
cpp复制void quickSort(vector<int>& arr, int left, int right) {
if (left >= right) return;
// 三数取中
int mid = left + (right - left) / 2;
if (arr[mid] > arr[right]) swap(arr[mid], arr[right]);
if (arr[left] > arr[right]) swap(arr[left], arr[right]);
if (arr[mid] > arr[left]) swap(arr[mid], arr[left]);
int pivot = partition(arr, left, right);
quickSort(arr, left, pivot - 1);
quickSort(arr, pivot + 1, right);
}
朋友圈问题典型的并查集应用,需要掌握路径压缩和按秩合并两种优化。
并查集模板:
cpp复制vector<int> parent;
vector<int> rank;
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]);
}
void unite(int x, int y) {
x = find(x);
y = find(y);
if (x == y) return;
if (rank[x] < rank[y]) {
parent[x] = y;
} else {
parent[y] = x;
if (rank[x] == rank[y]) rank[x]++;
}
}
解题步骤:
性能对比:
| 优化方式 | find时间复杂度 | union时间复杂度 |
|---|---|---|
| 无优化 | O(n) | O(n) |
| 路径压缩 | O(α(n)) | O(α(n)) |
| 按秩合并 | O(logn) | O(logn) |
| 两种优化结合 | O(α(n)) | O(α(n)) |
α(n)是反阿克曼函数,增长极其缓慢,可视为常数
在PTA题目中,最短路径是常考题型,需要根据问题特点选择合适算法:
Dijkstra算法(单源最短路径):
Floyd算法(多源最短路径):
Dijkstra核心实现:
cpp复制void dijkstra(int start) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
vector<int> dist(n+1, INF);
pq.push({0, start});
dist[start] = 0;
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (auto [v, w] : adj[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
算法选择指南:
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 单源正权图 | Dijkstra+堆优化 | 效率高 |
| 多源正权图 | Floyd | 代码简单 |
| 带负权边 | Bellman-Ford | 能检测负权回路 |
| 需要检测负权回路 | SPFA | 平均效率高于Bellman-Ford |
修理牧场问题本质上是构建哈夫曼树,每次合并最小的两块木头。
实现要点:
核心代码:
cpp复制int minCost(vector<int>& planks) {
priority_queue<int, vector<int>, greater<int>> pq(planks.begin(), planks.end());
int cost = 0;
while (pq.size() > 1) {
int a = pq.top(); pq.pop();
int b = pq.top(); pq.pop();
cost += a + b;
pq.push(a + b);
}
return cost;
}
复杂度分析:
与普通排序对比:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 排序+贪心 | O(n²) | O(1) | 小规模数据 |
| 优先队列 | O(nlogn) | O(n) | 大规模数据 |
| 线性构造法 | O(n) | O(n) | 数据已部分有序 |
汉诺塔的非递归实现需要使用栈来模拟递归调用栈,这是理解递归本质的好例子。
递归与非递归对比:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 递归 | 代码简洁,易理解 | 栈溢出风险,效率略低 |
| 非递归 | 可控栈空间,效率高 | 代码复杂,难维护 |
非递归实现核心:
cpp复制void hanoi(int n) {
stack<tuple<int, char, char, char>> st;
st.push({n, 'A', 'B', 'C'});
while (!st.empty()) {
auto [num, from, via, to] = st.top(); st.pop();
if (num == 1) {
printf("%c -> %c\n", from, to);
} else {
// 注意入栈顺序与递归调用顺序相反
st.push({num-1, via, from, to});
st.push({1, from, via, to});
st.push({num-1, from, to, via});
}
}
}
性能测试数据:
| 盘子数量 | 递归时间(ms) | 非递归时间(ms) | 移动次数 |
|---|---|---|---|
| 10 | 1.2 | 0.8 | 1023 |
| 20 | 1200 | 850 | 1048575 |
| 30 | 超时 | 可运行 | 巨大 |
验证任务调度是否合理本质上是检测有向图是否有环,可以用拓扑排序实现。
实现方法:
核心代码:
cpp复制bool isDAG(int n, vector<vector<int>>& edges) {
vector<int> inDegree(n+1, 0);
vector<vector<int>> adj(n+1);
// 构建邻接表和入度数组
for (auto& e : edges) {
adj[e[0]].push_back(e[1]);
inDegree[e[1]]++;
}
queue<int> q;
for (int i = 1; i <= n; i++)
if (inDegree[i] == 0) q.push(i);
int count = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
count++;
for (int v : adj[u]) {
if (--inDegree[v] == 0) {
q.push(v);
}
}
}
return count == n;
}
复杂度分析:
常见应用场景:
旅游规划问题需要在最短路径的基础上考虑第二权重(费用),属于双权重最短路径问题。
解法思路:
Dijkstra双权重实现:
cpp复制void dijkstra(int start, int end) {
vector<int> dist(n, INF), cost(n, INF);
dist[start] = 0;
cost[start] = 0;
priority_queue<tuple<int, int, int>,
vector<tuple<int, int, int>>,
greater<tuple<int, int, int>>> pq;
pq.push({0, 0, start});
while (!pq.empty()) {
auto [d, c, u] = pq.top(); pq.pop();
if (u == end) break;
if (d > dist[u]) continue;
for (auto [v, length, price] : adj[u]) {
if (dist[v] > dist[u] + length) {
dist[v] = dist[u] + length;
cost[v] = cost[u] + price;
pq.push({dist[v], cost[v], v});
} else if (dist[v] == dist[u] + length &&
cost[v] > cost[u] + price) {
cost[v] = cost[u] + price;
pq.push({dist[v], cost[v], v});
}
}
}
cout << dist[end] << " " << cost[end] << endl;
}
测试用例分析:
| 测试用例 | 最短路径 | 最低费用 | 说明 |
|---|---|---|---|
| 常规情况 | 3 | 40 | 唯一最短路径 |
| 多路径 | 5 | 30 | 选择费用更低的等长路径 |
| 无解 | INF | INF | 起点终点不连通 |
最小生成树问题有两种经典算法:Prim和Kruskal,需要根据图的特点选择。
Prim算法:
Kruskal算法:
Kruskal实现示例:
cpp复制struct Edge {
int u, v, w;
bool operator<(const Edge& other) const {
return w < other.w;
}
};
int kruskal(vector<Edge>& edges, int n) {
sort(edges.begin(), edges.end());
initUnionFind(n);
int total = 0, count = 0;
for (auto& e : edges) {
if (find(e.u) != find(e.v)) {
unite(e.u, e.v);
total += e.w;
if (++count == n-1) break;
}
}
return count == n-1 ? total : -1;
}
性能对比:
| 算法 | 存储结构 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| Prim | 邻接矩阵 | O(V²) | 稠密图 |
| Prim+堆 | 邻接表 | O(ElogV) | 稀疏图 |
| Kruskal | 边列表 | O(ElogE) | 稀疏图 |
| Boruvka | 混合 | O(ElogV) | 并行计算 |
判断两个插入序列是否生成相同的BST,不需要实际构建树,可以递归比较:
递归判断方法:
优化实现:
cpp复制bool isSameBST(vector<int>& a, vector<int>& b) {
if (a.size() != b.size()) return false;
if (a.empty()) return true;
if (a[0] != b[0]) return false;
vector<int> a_left, a_right, b_left, b_right;
for (int i = 1; i < a.size(); i++) {
if (a[i] < a[0]) a_left.push_back(a[i]);
else a_right.push_back(a[i]);
if (b[i] < b[0]) b_left.push_back(b[i]);
else b_right.push_back(b[i]);
}
return isSameBST(a_left, b_left) && isSameBST(a_right, b_right);
}
测试用例设计:
| 测试用例 | 预期结果 | 说明 |
|---|---|---|
| [2,1,3], [2,3,1] | Yes | 不同插入顺序相同BST |
| [3,1,2], [3,2,1] | No | 结构不同 |
| [1], [1] | Yes | 单节点情况 |
| [1,2], [1,2] | Yes | 只有右子树 |
PTA中图遍历问题常需要同时实现DFS和BFS,如"列出连通集"题目。
实现对比:
| 特性 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈(递归或显式栈) | 队列 |
| 空间复杂度 | O(h) | O(w) |
| 适用场景 | 寻找所有解、拓扑排序 | 最短路径、层序遍历 |
非递归DFS实现:
cpp复制void dfs(int start) {
stack<int> st;
vector<bool> visited(n, false);
st.push(start);
visited[start] = true;
while (!st.empty()) {
int u = st.top(); st.pop();
cout << u << " ";
// 注意邻接点逆序入栈以保证顺序
for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
int v = *it;
if (!visited[v]) {
visited[v] = true;
st.push(v);
}
}
}
}
BFS实现:
cpp复制void bfs(int start) {
queue<int> q;
vector<bool> visited(n, false);
q.push(start);
visited[start] = true;
while (!q.empty()) {
int u = q.front(); q.pop();
cout << u << " ";
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
性能考虑:
"堆中的路径"和"寻找大富翁"都是堆结构的典型应用,需要灵活掌握最大堆和最小堆。
堆的实现技巧:
最大堆插入操作:
cpp复制void insert(int x) {
heap[++size] = x;
// 自底向上调整
for (int i = size; i > 1 && heap[i/2] < heap[i]; i /= 2) {
swap(heap[i], heap[i/2]);
}
}
TopK问题解法:
cpp复制vector<int> topK(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> pq;
for (int num : nums) {
if (pq.size() < k) {
pq.push(num);
} else if (num > pq.top()) {
pq.pop();
pq.push(num);
}
}
vector<int> result;
while (!pq.empty()) {
result.push_back(pq.top());
pq.pop();
}
reverse(result.begin(), result.end());
return result;
}
复杂度分析:
魔法优惠券问题需要将正负优惠券与正负商品价值合理匹配以获得最大收益。
解题策略:
实现代码:
cpp复制int maxProfit(vector<int>& coupons, vector<int>& products) {
sort(coupons.begin(), coupons.end());
sort(products.begin(), products.end());
int i = 0, j = 0, sum = 0;
// 匹配负优惠券和负商品(小的乘小的)
while (i < coupons.size() && j < products.size() &&
coupons[i] < 0 && products[j] < 0) {
sum += coupons[i++] * products[j++];
}
// 匹配正优惠券和正商品(大的乘大的)
i = coupons.size()-1, j = products.size()-1;
while (i >= 0 && j >= 0 &&
coupons[i] > 0 && products[j] > 0) {
sum += coupons[i--] * products[j--];
}
return sum;
}
匹配策略分析:
| 优惠券类型 | 商品类型 | 处理方式 | 结果符号 |
|---|---|---|---|
| 正 | 正 | 最大×最大 | 正 |
| 负 | 负 | 最小×最小(绝对值最大) | 正 |
| 正 | 负 | 避免匹配 | 负 |
| 负 | 正 | 避免匹配 | 负 |
PTA中多处考察STL容器的使用,特别是map和unordered_map的选择。
map vs unordered_map:
| 特性 | map | unordered_map |
|---|---|---|
| 底层实现 | 红黑树 | 哈希表 |
| 查找效率 | O(logn) | O(1) |
| 元素顺序 | 按键排序 | 无特定顺序 |
| 内存占用 | 较低 | 较高 |
| 适用场景 | 需要有序遍历 | 快速查找 |
电话聊天狂人实现:
cpp复制string findMaxCaller(vector<pair<string, string>>& calls) {
map<string, int> count;
for (auto& call : calls) {
count[call.first]++;
count[call.second]++;
}
string maxCaller;
int maxCount = 0, sameCount = 1;
for (auto& [caller, cnt] : count) {
if (cnt > maxCount) {
maxCount = cnt;
maxCaller = caller;
sameCount = 1;
} else if (cnt == maxCount) {
sameCount++;
if (caller < maxCaller) {
maxCaller = caller;
}
}
}
cout << maxCaller << " " << maxCount;
if (sameCount > 1) cout << " " << sameCount;
return maxCaller;
}
性能优化技巧:
判断图是否存在欧拉回路是图论经典问题,需要掌握判定条件:
无向图欧拉回路条件:
有向图欧拉回路条件:
实现代码:
cpp复制bool hasEulerCircuit(vector<vector<int>>& graph) {
// 检查连通性(使用DFS/BFS)
if (!isConnected(graph)) return false;
// 检查所有顶点度数为偶数
for (int i = 0; i < graph.size(); i++) {
if (graph[i].size() % 2 != 0) {
return false;
}
}
return true;
}
应用变体:
二叉树非递归遍历是常见考点,需要掌握三种遍历方式的栈实现。
中序遍历栈实现:
cpp复制vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* curr = root;
while (curr || !st.empty()) {
while (curr) {
st.push(curr);
curr = curr->left;
}
curr = st.top(); st.pop();
result.push_back(curr->val);
curr = curr->right;
}
return result;
}
Morris遍历(O(1)空间):
cpp复制vector<int> morrisInorder(TreeNode* root) {
vector<int> result;
TreeNode *curr = root, *pre;
while (curr) {
if (!curr->left) {
result.push_back(curr->val);
curr = curr->right;
} else {
pre = curr->left;
while (pre->right && pre->right != curr) {
pre = pre->right;
}
if (!pre->right) {
pre->right = curr;
curr = curr->left;
} else {
pre->right = nullptr;
result.push_back(curr->val);
curr = curr->right;
}
}
}
return result;
}
性能对比:
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 递归 | O(n) | O(h) | 代码简洁 | 栈溢出风险 |
| 显式栈 | O(n) | O(h) | 避免递归 | 仍需额外空间 |
| Morris遍历 | O(n) | O(1) | 常数空间 | 修改树结构 |
| 线索二叉树 | O(n) | O(1) | 不修改结构 | 需要预处理 |
并查集在朋友圈、连通分量等问题中有高效表现,需要掌握两种优化技术。
优化实现:
cpp复制class UnionFind {
vector<int> parent;
vector<int> rank;
public:
UnionFind(int n) : parent(n), rank(n, 0) {
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]);
}
void unite(int x, int y) {
x = find(x);
y = find(y);
if (x == y) return;
if (rank[x] < rank[y]) {
parent[x] = y;
} else {
parent[y] = x;
if (rank[x] == rank[y]) rank[x]++;
}
}
bool connected(int x, int y) {
return find(x) == find(y);
}
};
优化效果对比:
| 优化方式 | find时间复杂度 | union时间复杂度 | 实际效率 |
|---|---|---|---|
| 无优化 | O(n) | O(n) | 最差 |
| 仅路径压缩 | O(α(n)) | O(α(n)) | 很好 |
| 仅按秩合并 | O(logn) | O(logn) | 较好 |
| 两者结合 | O(α(n)) | O(α(n)) | 最优 |
α(n)是反阿克曼函数,对于任何实际应用中n的值,α(n) ≤ 5
PTA中常考察多关键字排序的实现,需要熟练使用sort函数的自定义比较。
实现方法:
核心代码:
cpp复制struct Student {
string id;
string name;
int score;
};
bool cmp1(const Student& a, const Student& b) {
return a.id < b.id;
}
bool cmp2(const Student& a, const Student& b) {
return a.name != b.name ? a.name < b.name : a.id < b.id;
}
bool cmp3(const Student& a, const Student& b) {
return a.score != b.score ? a.score < b.score : a.id < b.id;
}
void sortStudents(vector<Student>& students, int C) {
switch (C) {
case 1: sort(students.begin(), students.end(), cmp1); break;
case 2: sort(students.begin(), students.end(), cmp2); break;
case 3: sort(students.begin(), students.end(), cmp3); break;
}
}
性能考虑: