题解:AcWing 241 楼兰图腾
【题目来源】
AcWing:241 楼兰图腾 - AcWing题库
【题目描述】
在完成了分配任务之后,西部 \(314\) 来到了楼兰古城的西部。
相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(∧),他们分别用 V 和 ∧ 的形状来代表各自部落的图腾。
西部 \(314\) 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 \(n\) 个点,经测量发现这 \(n\) 个点的水平位置和竖直位置是两两不同的。
西部 \(314\) 认为这幅壁画所包含的信息与这 \(n\) 个点的相对位置有关,因此不妨设坐标分别为 \((1,y_1),(2,y_2),\dots,(n,y_n)\),其中 \(y_1\sim y_n\) 是 \(1\) 到 \(n\) 的一个排列。
西部 \(314\) 打算研究这幅壁画中包含着多少个图腾。
如果三个点 \((i,y_i),(j,y_j),(k,y_k)\) 满足 \(1\le i\lt j\lt k\le n\) 且 \(y_i>y_j,y_j<y_k\),则称这三个点构成 V 图腾;
如果三个点 \((i,y_i),(j,y_j),(k,y_k)\) 满足 \(1\le i\lt j\lt k\le n\) 且 \(y_i<y_j,y_j>y_k\),则称这三个点构成 ∧ 图腾;
西部 \(314\) 想知道,这 \(n\) 个点中两个部落图腾的数目。
因此,你需要编写一个程序来求出 V 的个数和 ∧ 的个数。
【输入】
第一行一个数 \(n\)。
第二行是 \(n\) 个数,分别代表 \(y_1,y_2,\dots,y_n\)。
【输出】
两个数,中间用空格隔开,依次为 V 的个数和 ∧ 的个数。
【输入样例】
5
1 5 3 2 4
【输出样例】
3 4
【核心思想】
-
问题分析:给定 \(n\) 个点 \((i, y_i)\),其中 \(y\) 是 \(1 \sim n\) 的排列。需要统计
V图腾(\(y_i > y_j < y_k\),\(i < j < k\))和∧图腾(\(y_i < y_j > y_k\),\(i < j < k\))的数量。关键观察是:以中间点 \(j\) 为基准,V要求左边比 \(y_j\) 大的点数 \(\times\) 右边比 \(y_j\) 大的点数,∧要求左边比 \(y_j\) 小的点数 \(\times\) 右边比 \(y_j\) 小的点数。因此问题转化为对每个位置 \(j\),统计其左右两侧比它大/小的元素个数。 -
算法选择:
- 树状数组(Fenwick Tree):\(O(\log n)\) 维护前缀和,支持动态插入和查询"已出现且 \(\leq x\)"的元素数量
- 两次扫描:第一次从左到右统计每个位置左侧的信息,第二次从右到左统计右侧信息并计算答案
-
关键步骤:
- 第一次扫描(从左到右):
- 遍历 \(i\) 从 \(1\) 到 \(n\):
Greater[i] = query(n) - query(y):已出现且 \(> y_i\) 的元素个数(左侧比 \(y_i\) 大的数量)Lower[i] = query(y - 1):已出现且 \(< y_i\) 的元素个数(左侧比 \(y_i\) 小的数量)add(y, 1):将 \(y_i\) 加入树状数组
- 遍历 \(i\) 从 \(1\) 到 \(n\):
- 重置树状数组
- 第二次扫描(从右到左):
- 遍历 \(i\) 从 \(n\) 到 \(1\):
res1 += Greater[i] * (query(n) - query(y)):V图腾数 = 左侧比 \(y_i\) 大 \(\times\) 右侧比 \(y_i\) 大res2 += Lower[i] * query(y - 1):∧图腾数 = 左侧比 \(y_i\) 小 \(\times\) 右侧比 \(y_i\) 小add(y, 1):将 \(y_i\) 加入树状数组
- 遍历 \(i\) 从 \(n\) 到 \(1\):
- 输出:\(res1\)(
V数量)和 \(res2\)(∧数量)
- 第一次扫描(从左到右):
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),两次扫描各 \(n\) 次树状数组操作
- 空间复杂度:\(O(n)\),树状数组和辅助数组
-
树状数组统计逆序对的核心思想:
- 前缀和即计数:树状数组维护的是值域 \([1, n]\) 上的频次前缀和,
query(y)表示已出现且 \(\leq y\) 的元素数量,从而query(n) - query(y)表示已出现且 \(> y\) 的数量 - 方向分离统计:第一次从左到右只统计左侧信息,第二次从右到左只统计右侧信息,两者相乘得到以当前点为中间点的三元组数量
- 乘积原理:对于固定的中间点 \(j\),
V图腾要求左右两侧均比 \(y_j\) 大,两侧选择独立,因此总数为左侧大数个数 \(\times\) 右侧大数个数 - 值域映射:由于 \(y_i\) 是 \(1 \sim n\) 的排列,值域与定义域一致,可以直接用 \(y_i\) 作为树状数组下标,无需离散化
- 适用于三元组统计、逆序对扩展、左右两侧信息统计等场景
- 前缀和即计数:树状数组维护的是值域 \([1, n]\) 上的频次前缀和,
【解题思路】

【算法标签】
树状数组
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 使用long long类型
const int N = 200005; // 定义数组最大长度
int n; // 数组长度
int a[N]; // 存储原始数组
int tr[N]; // 树状数组
int Greater[N]; // 存储每个元素右边比它大的元素个数
int Lower[N]; // 存储每个元素右边比它小的元素个数
// 计算x的最低有效位(用于树状数组索引计算)
int lowbit(int x)
{
return x & -x;
}
// 向树状数组添加值:在位置x增加c
void add(int x, int c)
{
// 从x开始向上更新所有相关节点
for (int i = x; i <= n; i += lowbit(i))
{
tr[i] += c;
}
}
// 查询前缀和:计算前x个元素的和
int query(int x)
{
int res = 0;
// 从x开始向下累加所有相关节点
for (int i = x; i; i -= lowbit(i))
{
res += tr[i];
}
return res;
}
signed main()
{
// 输入数组长度n
cin >> n;
// 输入原始数组
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
// 第一遍扫描:从左到右计算每个元素左边的情况
for (int i = 1; i <= n; i++)
{
int y = a[i];
// 计算当前元素右边比它大的元素个数
Greater[i] = query(n) - query(y);
// 计算当前元素右边比它小的元素个数
Lower[i] = query(y - 1);
// 将当前元素加入树状数组
add(y, 1);
}
// 重置树状数组
memset(tr, 0, sizeof(tr));
int res1 = 0, res2 = 0; // res1-逆序对总数,res2-顺序对总数
// 第二遍扫描:从右到左计算结果
for (int i = n; i; i--)
{
int y = a[i];
// 计算逆序对总数
res1 += Greater[i] * (query(n) - query(y));
// 计算顺序对总数
res2 += Lower[i] * (query(y - 1));
// 将当前元素加入树状数组
add(y, 1);
}
// 输出结果:逆序对总数和顺序对总数
cout << res1 << " " << res2 << endl;
return 0;
}
【运行结果】
5
1 5 3 2 4
3 4
浙公网安备 33010602011771号