一、问题背景与算法选择
题目要求计算n个人按照特定顺序排队时发生的握手次数,本质上是计算序列中逆序对的数量。树状数组(Fenwick Tree)因其高效的区间查询和单点更新能力(O(logn))成为解决此类问题的理想选择。
二、完整代码实现(带详细注释)
#include <iostream>
#include <vector>
using namespace std;
// 树状数组实现类
class FenwickTree {
private:
vector tree; // 存储树状数组
int size; // 数组大小
public:
// 构造函数,初始化大小为n的树状数组
FenwickTree(int n) : size(n), tree(n + 1, 0) {}
// 更新操作:在index位置增加delta
void update(int index, int delta) {
// 典型的树状数组更新方式
for(; index <= size; index += index & -index)
tree[index] += delta;
}
// 查询操作:求前index个元素的和
int query(int index) {
int res = 0;
// 典型的树状数组查询方式
for(; index > 0; index -= index & -index)
res += tree[index];
return res;
}
};
int main() {
// 优化输入输出
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<int> order(N);
for(int i = 0; i < N; i++) {
cin >> order[i];
order[i]++; // 转换为1-based索引
}
// 初始化树状数组
FenwickTree ft(N);
long long handshakes = 0;
// 核心计算逻辑
for(int i = 0; i < N; i++) {
int current = order[i];
// 查询比current小的已存在数的个数
handshakes += ft.query(current - 1);
// 将当前数加入树状数组
ft.update(current, 1);
}
cout << handshakes << endl;
return 0;
}
三、算法核心解析
- 树状数组原理:
- 1-based转换:
- 将输入值+1转换为1-based索引
- 避免处理0索引带来的边界问题
- 逆序对计算:
- 按顺序处理每个人时
- 查询已处理人中编号比当前小的数量
- 累加得到总握手次数
四、复杂度分析与优化
- 时间复杂度:
- 预处理:O(n)
- n次查询和更新:O(nlogn)
- 总复杂度:O(nlogn)
- 空间复杂度:
- 树状数组:O(n)
- 输入存储:O(n)
- 优化方向:
- 离散化处理可减少空间使用
- 并行计算可优化大规模数据
五、常见问题解答
Q:为什么选择树状数组而不是线段树? A:树状数组代码更简洁且在解决前缀和问题上效率相当。
Q:1-based转换是否必要? A:不是绝对必要但能简化代码逻辑,避免处理0索引。
Q:如何处理数值很大的情况? A:可以通过离散化将大范围数值映射到小范围。
来源:竞赛学习