刷PTA数据结构题时,我踩过的那些坑和高效解法(附C++代码)

诗语情柔

刷PTA数据结构题时,我踩过的那些坑和高效解法(附C++代码)

1. 最大子列和问题:从暴力到动态规划的思维跃迁

第一次遇到最大子列和问题时,我本能地想到三重循环的暴力解法。结果在PTA上提交后直接TLE(时间超过限制),这才意识到算法效率的重要性。

常见误区:

  • 边界条件处理不当(全负数情况)
  • 数组越界访问
  • 使用O(n³)暴力解法导致超时

最优解法(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)的算法

2. 树的同构判断:递归思维与特殊情况的处理

这道题让我深刻理解了递归思维在树问题中的应用。判断两棵树是否同构需要考虑多种情况:

  1. 两棵树都为空 → 同构
  2. 一棵为空一棵不为空 → 不同构
  3. 根节点值不同 → 不同构
  4. 左子树同构且右子树同构 → 同构
  5. 左右子树交换后同构 → 同构

易错点:

  • 忽略了空树情况
  • 没有考虑节点值相同但结构不同的情况
  • 递归终止条件不完整

核心代码:

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));
}

3. 堆中的路径:最小堆构建与路径追踪

这道题考察堆的基本操作和路径回溯。我最初犯的错误是试图先构建完整数组再调整成堆,结果发现题目要求的是插入时即时调整。

正确做法:

  1. 从数组的第二个位置开始存储(索引1为根节点)
  2. 每次插入新元素后,自底向上调整
  3. 查询路径时不断除以2回溯到根节点

堆插入操作:

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

4. 六度空间理论验证:BFS的层数控制

这道题需要计算每个节点六度范围内的节点比例,关键在于如何在BFS中记录层数信息。

实现技巧:

  • 使用level数组记录每个节点的层数
  • 在BFS队列中存储节点和当前层数
  • 当层数超过6时停止搜索

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;
}

优化点:

  • 使用邻接表而非邻接矩阵存储图
  • 提前终止超过6层的搜索
  • 避免重复计算已访问节点

5. 哈利·波特的考试:多源最短路径的Floyd应用

这道题需要找到使最难变动物所需咒语最短的动物,本质上是图论中的多源最短路径问题。

算法选择:

  • Floyd算法:O(n³),适合n≤100的情况
  • Dijkstra调用n次:O(n³),稀疏图时更优

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;
}

常见错误:

  • 未初始化对角线为0
  • 未处理不可达情况
  • 混淆最大值和最小值的选择标准

6. 关键路径分析:拓扑排序与动态规划的结合

关键路径是AOE网中的最长路径,需要计算最早发生时间和最晚发生时间。

计算步骤:

  1. 拓扑排序确定事件顺序
  2. 正向计算最早发生时间
  3. 反向计算最晚发生时间
  4. 关键活动判定(最早时间=最晚时间)

核心代码片段:

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;
        }
    }
}

7. 排序算法选择:从理论到PTA实战

PTA的排序题往往给出不同特性的测试数据,需要根据数据特点选择合适算法:

数据特征与算法选择:

数据类型 推荐算法 时间复杂度 空间复杂度
小规模随机数据 插入排序 O(n²) O(1)
基本有序数据 冒泡排序 O(n)~O(n²) O(1)
大规模随机数据 快速排序 O(nlogn) O(logn)
数据范围较小 计数/桶排序 O(n+k) O(k)
需要稳定排序 归并排序 O(nlogn) O(n)

快速排序优化要点:

  • 三数取中法选择pivot
  • 小规模时切换为插入排序
  • 使用尾递归减少栈空间
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);
}

8. 并查集实战:朋友圈问题的高效解法

朋友圈问题典型的并查集应用,需要掌握路径压缩和按秩合并两种优化。

并查集模板:

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]++;
    }
}

解题步骤:

  1. 初始化每个元素的父节点为自身
  2. 对每对关系执行union操作
  3. 最后统计每个根节点的出现次数,最大值即为最大朋友圈

性能对比:

优化方式 find时间复杂度 union时间复杂度
无优化 O(n) O(n)
路径压缩 O(α(n)) O(α(n))
按秩合并 O(logn) O(logn)
两种优化结合 O(α(n)) O(α(n))

α(n)是反阿克曼函数,增长极其缓慢,可视为常数

9. 最短路径算法对比:Dijkstra与Floyd的选择

在PTA题目中,最短路径是常考题型,需要根据问题特点选择合适算法:

Dijkstra算法(单源最短路径):

  • 适合正权图
  • 使用优先队列优化后复杂度O(E + VlogV)
  • 不能处理负权边

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

10. 哈夫曼编码:优先队列的实际应用

修理牧场问题本质上是构建哈夫曼树,每次合并最小的两块木头。

实现要点:

  • 使用最小堆(优先队列)
  • 每次取出两个最小元素合并
  • 将合并后的值重新放入堆中

核心代码:

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(logn)
  • 共进行n-1次合并
  • 总时间复杂度O(nlogn)

与普通排序对比:

方法 时间复杂度 空间复杂度 适用场景
排序+贪心 O(n²) O(1) 小规模数据
优先队列 O(nlogn) O(n) 大规模数据
线性构造法 O(n) O(n) 数据已部分有序

11. 非递归遍历:栈模拟递归过程

汉诺塔的非递归实现需要使用栈来模拟递归调用栈,这是理解递归本质的好例子。

递归与非递归对比:

方式 优点 缺点
递归 代码简洁,易理解 栈溢出风险,效率略低
非递归 可控栈空间,效率高 代码复杂,难维护

非递归实现核心:

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 超时 可运行 巨大

12. 拓扑排序应用:任务调度合理性验证

验证任务调度是否合理本质上是检测有向图是否有环,可以用拓扑排序实现。

实现方法:

  1. 计算每个节点的入度
  2. 将入度为0的节点加入队列
  3. 不断移除队列中的节点并减少相邻节点入度
  4. 如果最后所有节点都被移除,则无环

核心代码:

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;
}

复杂度分析:

  • 时间复杂度:O(V + E)
  • 空间复杂度:O(V + E)

常见应用场景:

  • 课程安排合理性检查
  • 任务调度依赖关系验证
  • 编译过程中的依赖解析

13. 最短路径变体:旅游规划中的多权重问题

旅游规划问题需要在最短路径的基础上考虑第二权重(费用),属于双权重最短路径问题。

解法思路:

  1. 优先保证路径最短
  2. 在路径长度相同的情况下选择费用更低的路径
  3. 使用Dijkstra算法,但需要同时维护距离和费用

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 起点终点不连通

14. 最小生成树实战:公路村村通问题

最小生成树问题有两种经典算法:Prim和Kruskal,需要根据图的特点选择。

Prim算法:

  • 适合稠密图
  • 时间复杂度O(V²)
  • 需要邻接矩阵存储

Kruskal算法:

  • 适合稀疏图
  • 时间复杂度O(ElogE)
  • 需要并查集数据结构

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) 并行计算

15. 二叉搜索树验证:同构BST判断技巧

判断两个插入序列是否生成相同的BST,不需要实际构建树,可以递归比较:

递归判断方法:

  1. 第一个元素必须相同(根节点)
  2. 左子树元素集合必须相同
  3. 右子树元素集合必须相同
  4. 递归判断左右子树

优化实现:

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 只有右子树

16. 图遍历技巧:DFS与BFS的选择与实现

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);
            }
        }
    }
}

性能考虑:

  • 稠密图使用邻接矩阵
  • 稀疏图使用邻接表
  • 大规模图注意避免递归深度过大

17. 堆的应用:动态中位数与TopK问题

"堆中的路径"和"寻找大富翁"都是堆结构的典型应用,需要灵活掌握最大堆和最小堆。

堆的实现技巧:

  • 数组存储完全二叉树
  • 从1开始索引方便计算父子节点
  • 插入时自底向上调整
  • 删除时自顶向下调整

最大堆插入操作:

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问题解法:

  1. 维护一个大小为K的最小堆
  2. 遍历所有元素,比堆顶大的就替换
  3. 最后堆中就是最大的K个元素
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;
}

复杂度分析:

  • 建堆:O(n)
  • 每次插入/删除:O(logk)
  • 总复杂度:O(nlogk)

18. 贪心算法实战:魔法优惠券的最佳匹配

魔法优惠券问题需要将正负优惠券与正负商品价值合理匹配以获得最大收益。

解题策略:

  1. 将正优惠券和正商品降序排序
  2. 将负优惠券和负商品升序排序(即绝对值降序)
  3. 分别匹配正对正、负对负
  4. 避免正负相乘的情况

实现代码:

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;
}

匹配策略分析:

优惠券类型 商品类型 处理方式 结果符号
最大×最大
最小×最小(绝对值最大)
避免匹配
避免匹配

19. 哈希表应用:电话聊天狂人与QQ账户管理

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;
}

性能优化技巧:

  • 对于大规模数据,使用unordered_map提高查找速度
  • 需要字典序输出时,可以先用unordered_map统计,再转入map排序
  • 自定义哈希函数处理复杂键类型

20. 欧拉回路判断:七桥问题的图论解法

判断图是否存在欧拉回路是图论经典问题,需要掌握判定条件:

无向图欧拉回路条件:

  1. 图是连通的
  2. 所有顶点的度数都是偶数

有向图欧拉回路条件:

  1. 图是强连通的
  2. 每个顶点入度等于出度

实现代码:

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;
}

应用变体:

  • 欧拉通路(允许两个奇数度顶点)
  • 中国邮路问题(加权图的最短欧拉回路)
  • 哈密尔顿回路与欧拉回路的关系

21. 非递归二叉树遍历:栈与Morris算法

二叉树非递归遍历是常见考点,需要掌握三种遍历方式的栈实现。

中序遍历栈实现:

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) 不修改结构 需要预处理

22. 并查集优化:路径压缩与按秩合并

并查集在朋友圈、连通分量等问题中有高效表现,需要掌握两种优化技术。

优化实现:

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

23. 多关键字排序:模拟Excel排序功能

PTA中常考察多关键字排序的实现,需要熟练使用sort函数的自定义比较。

实现方法:

  1. 定义结构体存储学生信息
  2. 根据排序要求实现不同比较函数
  3. 使用stable_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;
    }
}

性能考虑:

  • 对于大规模数据,避免在比较函数中进行复杂计算
  • 可以预先计算需要比较的键值
  • 考虑使用指针排序减少数据移动

24

内容推荐

从特洛伊咖啡壶到华为LiteOS:一个文科生也能看懂的物联网发展简史
本文以特洛伊咖啡壶为起点,生动讲述了物联网从概念到现实的发展历程,重点解析了华为LiteOS的轻量化设计及其在物联网中的关键作用。文章还提供了HCIA物联网认证的实用备考建议,帮助读者理解物联网的核心技术和应用场景。
openEuler 22.03 LTS安装GNOME 41桌面踩坑实录:我遇到的5个问题及解决方法
本文详细记录了在openEuler 22.03 LTS上安装GNOME 41桌面环境时遇到的五个典型问题及解决方案,包括包冲突与依赖缺失、首次启动黑屏、中文语言包设置、网络管理器与蓝牙服务异常以及清理安装残留。通过具体命令和步骤,帮助用户顺利实现图形化桌面环境的部署。
告别synchronized!用Disruptor无锁框架重构你的Java高并发服务(附性能对比)
本文深入探讨了如何利用Disruptor无锁框架重构Java高并发服务,显著提升系统性能。通过对比传统synchronized方案与Disruptor在TPS、延迟和CPU利用率等方面的表现,展示了Disruptor在高并发场景下的巨大优势。文章包含核心原理解析、实战重构示例和性能调优建议,帮助开发者掌握这一高性能并发框架。
Python解包错误:从“too many values to unpack”到优雅处理数据不匹配
本文深入解析Python中常见的'too many values to unpack'错误,探讨其本质及解决方案。从基础的数量匹配到进阶的星号解包技巧,再到实战中的数据不匹配处理,帮助开发者优雅应对ValueError异常。文章特别介绍了unpack机制在API响应、文件解析等场景中的应用,提升代码健壮性。
C/C++宏函数避坑指南:从SQUARE(8+2)=26说起,手把手教你正确加括号
本文深入解析C/C++宏函数常见陷阱,以SQUARE(8+2)=26为例揭示宏定义缺陷,提供防御性编程四原则(括号防御、多语句封装、副作用防护、类型安全),并对比现代C++替代方案。通过Linux内核和Redis源码案例,展示宏函数最佳实践与调试技巧,帮助开发者规避潜在风险。
别再问OA运维难不难了!从B/S到C/S,手把手教你搞定Windows服务器上的OA系统部署
本文详细解析了OA系统在Windows服务器上的部署流程,涵盖B/S和C/S架构的配置要点。从环境准备到安全加固,提供完整的运维指南,帮助解决OA系统部署中的常见问题,提升运维效率。特别针对OA运维中的难点给出实用解决方案。
保姆级教程:用Python脚本一键搞定CrowdHuman数据集转YOLOv5格式(含只保留person类别的代码)
本文提供了一份详细的Python脚本教程,帮助用户将CrowdHuman数据集从ODGT格式转换为YOLOv5格式,特别包含只保留person类别的代码实现。通过环境准备、数据集解析、核心代码实现和自动化处理流水线搭建,大幅提升目标检测任务的效率。
你的ROS小车能动吗?给URDF模型加上Gazebo物理属性和键盘控制的完整流程
本文详细介绍了如何为ROS小车的URDF模型添加Gazebo物理属性和键盘控制功能,解决模型在仿真中无法移动的问题。通过定义质量、惯性矩阵、碰撞属性和传动系统,使小车具备真实物理行为,并实现Python键盘控制节点,帮助开发者快速完成从静态模型到动态仿真的转变。
实战笔记:STM32G4 HRTIM高分辨率定时器的PWM波形生成与调试
本文详细介绍了STM32G4 HRTIM高分辨率定时器在PWM波形生成与调试中的实战应用。从基础入门到高级功能配置,包括死区时间设置、故障保护等,提供了完整的项目环境搭建和调试技巧,帮助工程师实现高精度PWM控制,适用于电机驱动、电源转换等场景。
别再只用next()了!Python生成器send()方法实战:手把手教你构建动态数据管道
本文深入解析Python生成器的`send()`方法,教你如何突破`next()`的单向限制,构建动态数据管道。通过实战案例展示如何实现生成器与外部环境的双向交互,包括动态日志处理器和可配置API模拟器,提升数据处理灵活性和效率。掌握这一技巧可广泛应用于实时监控、数据清洗等场景。
手把手教你用Vivado和SDK在ZCU102上玩转PS端SPI控制器(EMIO扩展版)
本文详细介绍了如何在ZCU102评估板上使用Vivado和SDK实现PS端SPI控制器的EMIO扩展。从Vivado工程创建、IP配置到SDK应用程序开发,提供完整的SPI通信系统构建指南,帮助开发者快速掌握ZYNQ平台的SPI扩展技术,提升嵌入式系统开发效率。
C++取整函数全攻略:round、ceil、floor怎么选?结合实例一次讲清
本文全面解析C++中的取整函数round、ceil和floor的应用场景与性能对比,结合电商分页、游戏伤害计算等实战案例,帮助开发者精准选择取整策略。特别探讨了保留小数位的高精度处理技巧和跨平台一致性挑战,为工程实践提供避坑指南。
从原始数据到精准分析:ENVI5.3驱动下的高分二号影像全流程预处理实战
本文详细介绍了使用ENVI5.3对高分二号(GF-2)遥感影像进行全流程预处理的方法,包括辐射定标、大气校正、正射校正和影像融合等关键步骤。通过实战案例和避坑指南,帮助用户掌握从原始数据到精准分析的技术要点,提升遥感影像处理效率和数据质量。
麒麟&UOS系统下vlc-qt开发环境搭建与实战指南
本文详细介绍了在麒麟和UOS国产操作系统下搭建vlc-qt开发环境的完整流程,包括环境准备、依赖安装、编译优化及Qt项目集成实战。特别针对ARM架构与X86架构的差异提供了解决方案,并分享了性能优化与常见问题排查技巧,帮助开发者高效实现音视频应用开发。
【Python】pyecharts 模块 ② ( 虚拟环境安装与配置 | 多版本Python环境下的模块部署 )
本文详细介绍了在Python多版本环境下使用虚拟环境安装和配置pyecharts模块的方法。通过venv和conda两种工具创建隔离环境,解决版本冲突问题,并提供了PyCharm中的多环境配置技巧。文章还涵盖了复杂环境下的排错指南、虚拟环境的高级应用以及企业级部署实践,帮助开发者高效管理Python项目依赖。
保姆级避坑指南:微信小程序调用百度OCR识别身份证,从配置到上线的完整流程
本文提供微信小程序集成百度OCR身份证识别的完整流程,从百度AI平台配置到微信小程序上线,涵盖关键步骤和常见避坑指南。详细讲解Access Token获取、图片处理、OCR接口调用等核心技术点,帮助开发者高效实现身份证扫描识别功能,提升实名认证流程的用户体验。
禾川HCQ0-1100-D PLC固件升级与库版本避坑指南:从1.04版Web可视化说起
本文详细解析禾川HCQ0-1100-D PLC固件升级与库版本兼容性问题,从1.04版Web可视化功能切入,提供完整的版本管理解决方案。涵盖固件升级流程、库函数版本冲突处理、Web可视化配置及多总线协议集成实践,帮助工程师规避常见版本陷阱,提升工业自动化项目开发效率。
VCS门级仿真避坑指南:从Pre-Gate到Post-Gate的完整配置与调试实战
本文详细解析了VCS门级仿真从Pre-Gate到Post-Gate的完整配置与调试实战,涵盖关键编译选项、典型问题解决方案和高效调试方法论。通过对比Pre-Gate和Post-Gate仿真的核心差异,帮助工程师优化验证流程,提升芯片设计效率。特别针对跨时钟域处理和X态溯源等常见挑战,提供了实用的调试技巧和最佳实践。
告别delay()!用Arduino Uno定时器中断实现精准多任务(附TimerOne库实战)
本文详细介绍了如何利用Arduino Uno的定时器中断和TimerOne库实现精准多任务处理,告别传统的delay()函数。通过实战案例和高级技巧,帮助开发者解决时序失控、响应迟钝等问题,提升项目效率和精度。
Qt信号管理三板斧:connect、disconnect、blockSignals在动态界面中的实战配合
本文深入探讨Qt信号管理中的connect、disconnect和blockSignals三种方法在动态界面开发中的实战应用。通过对比分析它们的本质区别、适用场景及性能影响,帮助开发者高效管理信号与槽的连接,构建更健壮的交互界面。特别针对表单验证、监控面板和插件系统等典型场景,提供了最佳实践方案。
已经到底了哦
精选内容
热门内容
最新内容
DDR5 SDRAM 信号完整性实战:深入解析占空比调节器(DCA)的校准策略与系统补偿
本文深入解析DDR5 SDRAM中占空比调节器(DCA)的校准策略与系统补偿,探讨其在高速内存应用中的核心价值与工程挑战。通过实战案例详细介绍了DCA寄存器配置、四相时钟系统处理及读取训练中的协同优化,帮助工程师提升信号完整性并实现系统稳定性。
保姆级拆解:GameFramework资源加载如何用任务池和对象池搞定高并发?
本文深入解析GameFramework在高并发场景下的资源加载优化方案,重点介绍任务池和对象池的协同设计。通过优先级调度、智能代理分配及引用计数管理,有效解决移动游戏开发中的性能瓶颈问题,提升资源加载效率并降低内存占用。
从‘自用’到‘共享’:我是如何把一个日常工具脚本打包成PyPI可安装包的
本文分享了如何将日常Python脚本打包成PyPI可安装包的完整过程,重点探讨了从自用到共享的思维转变。通过项目结构规范化、配置管理优化、文档撰写和自动化测试等关键步骤,帮助开发者将私人工具转化为可复用的开源包,提升代码价值并扩大技术影响力。
用STM32G431和ADS1118搭建一个简易四通道电压监测仪(附完整工程)
本文详细介绍了如何利用STM32G431微控制器和ADS1118 ADC芯片构建一个高精度四通道电压监测仪。通过模拟SPI通信实现多通道电压采集,提供完整的硬件设计、软件实现及优化策略,适用于电子系统调试、电源监测等多种场景。项目包含详细代码示例和常见问题解决方案,助力开发者快速搭建可靠的电压监测系统。
告别‘一视同仁’:聊聊3D点云检测中FocalsConv如何像人眼一样聚焦关键区域
本文探讨了Focal Sparse Convolutional Networks(FocalsConv)在3D点云检测中的创新应用,通过模拟人眼的选择性关注机制,动态聚焦关键区域。该技术有效解决了传统3D卷积神经网络在处理非均匀点云数据时的效率问题,显著提升了小目标检测精度和实时性能,特别适用于自动驾驶等场景。
稀疏贝叶斯学习:从高维噪声中识别关键信号的智能框架
本文深入探讨了稀疏贝叶斯学习(Sparse Bayesian Learning)在高维噪声数据中识别关键信号的智能框架。通过先验分布和变分推断等核心技术,稀疏贝叶斯学习能够有效压缩特征维度并提升模型可解释性。文章结合医疗影像、金融风控等实战案例,展示了其在特征选择和降维方面的卓越性能,并提供了避坑指南和前沿进展,为处理高维数据提供了高效解决方案。
图像频域处理入门:用MATLAB的FFT/FFT2函数看懂频谱图与滤波
本文介绍了图像频域处理的基础知识,重点讲解如何使用MATLAB的FFT/FFT2函数进行频谱图分析和滤波操作。通过实际代码示例,帮助读者理解傅里叶变换在数字图像处理中的应用,包括频谱图解读、频域滤波技术及优化技巧,适合初学者快速入门频域图像处理。
避开这3个坑,你的CellProfiler病理图像分析流程才算真正跑通
本文深入探讨了CellProfiler在病理图像分析中的三个常见陷阱及解决方案,包括颜色解混、对象识别阈值策略和数据整合。通过实战案例和参数优化建议,帮助研究者避免系统性偏差,提升分析结果的准确性和可靠性。
从零到一:K210上Mx_yolov3模型训练与部署避坑指南
本文详细介绍了在K210开发板上训练与部署Mx_yolov3模型的完整流程,包括环境搭建、CUDA配置、数据集准备、模型训练与调优、模型转换及部署方案。特别针对常见问题如zlibwapi.dll缺失、内存不足等提供了实用解决方案,帮助开发者高效完成AI模型在边缘设备上的落地应用。
PLL IP核:从原理到实战的时钟管理指南
本文深入解析PLL IP核在数字系统中的关键作用,从软核、固核到硬核的三种形态对比,到Quartus中的实战配置与调试技巧。通过详细案例展示如何生成多时钟信号,优化高级参数,并解决常见问题,帮助工程师高效管理FPGA时钟系统。特别涵盖动态重配置等进阶应用,提升系统灵活性与性能。