固定中间点把图腾拆成左右独立的大小计数,用权值树状数组正序扫描得到每个位置左侧更小的个数,反转序列复用同一函数得到右侧计数,O(n log n)。
OJ: roj
题目 ID: 3109
难度:提高
标签:树状数组前缀和逆序对python
创建: 2026-10-01 17:40
更新: 2026-10-10 11:30
形式化题目
给定
- 若
且 ,称 构成 V 图腾( 是谷); - 若
且 ,称 构成 ∧ 图腾( 是峰)。
求 V 图腾与 ∧ 图腾的个数。
样例:3 4。
正解
思路
1. 朴素做法与瓶颈
最直接的做法是三层循环枚举
稍微聪明一点:注意 V 的判定
两种朴素做法的共同瓶颈是同一批元素被反复扫描:每个
| 做法 | 复杂度 | |
|---|---|---|
| 枚举三元组 | ||
| 固定 |
||
| 权值树状数组 |
2. 固定中间点: 与 的条件互相独立
这是整道题的枢纽。对固定的
因为
每个三元组的中间点唯一,所以按
3. 四个计数只需两个:互补量由下标总数给出
(上式用
4. 用权值树状数组把"数一遍"降到
现在唯一要回答的问题变成:已经扫过的元素里,权值比
tree是一棵权值树状数组,tree上区间的和 = 已插入元素中权值 的个数; - 查询
query(y_j-1)得到的就是; - 查询完再
add(y_j, 1),把当前元素登记进去。
因为查的是"插入之前"的状态,统计到的自然只有
5. 反向扫描复用同一个函数
右侧计数
right = prefix_less(a[::-1], n)[::-1] # 得到的结果整体倒序,下标重新对齐同一份 prefix_less 被调用两次,一次正序、一次倒序,四个计数就齐了。
6. 样例推演
把
| V 的贡献 |
∧ 的贡献 |
||||||
|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 | 4 | 0 | 0 |
| 1 | 5 | 1 | 3 | 0 | 0 | 0 | 3 |
| 2 | 3 | 1 | 1 | 1 | 1 | 1 | 1 |
| 3 | 2 | 1 | 0 | 2 | 1 | 2 | 0 |
| 4 | 4 | 3 | 0 | 1 | 0 | 0 | 0 |
| V = 3 | ∧ = 4 |
读法:3 4。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 权值树状数组扫两遍,统计「V」与「∧」两种三元组个数。
#include <cstdio>
typedef long long ll;
const ll MAXN = 200005;
ll n;
ll a[MAXN]; // 原始排列(值域 1..n)
ll rev[MAXN]; // a 的反转
ll tree[MAXN]; // 权值树状数组
ll lessL[MAXN]; // lessL[j]:j 左侧比 a[j] 小的个数
ll lessR[MAXN]; // lessR[j]:j 右侧比 a[j] 小的个数
// less[j] = seq[j] 前面(已扫过的部分)比它小的元素个数
void prefix_less(ll* seq, ll* out) {
for (ll i = 0; i <= n; i++) {
tree[i] = 0;
}
for (ll j = 0; j < n; j++) {
ll value = seq[j];
ll i = value - 1;
ll s = 0;
while (i) { // 前缀和 [1, value-1]
s += tree[i];
i -= i & -i;
}
out[j] = s;
i = value; // 把 value 插进树状数组
while (i <= n) {
tree[i]++;
i += i & -i;
}
}
}
ll rawR[MAXN];
int main() {
if (scanf("%lld", &n) != 1) return 0; // 空输入安全返回
for (ll i = 0; i < n; i++) {
scanf("%lld", &a[i]);
}
for (ll i = 0; i < n; i++) {
rev[i] = a[n - 1 - i];
}
prefix_less(a, lessL);
prefix_less(rev, rawR); // 对反转序列做同样的扫描,再翻回来
for (ll i = 0; i < n; i++) {
lessR[i] = rawR[n - 1 - i];
}
// 左侧更大的个数 = 左侧总数 j 减去左侧更小的;右侧总数是 n-1-j
ll vCount = 0, hatCount = 0;
for (ll j = 0; j < n; j++) {
vCount += (j - lessL[j]) * (n - 1 - j - lessR[j]); // V:左大右大
hatCount += lessL[j] * lessR[j]; // ∧:左小右小
}
printf("%lld %lld\n", vCount, hatCount);
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 17:40
# update_at: 2026-10-01 17:43
import sys
def prefix_less(seq: list[int], n: int) -> list[int]:
"""权值树状数组扫一遍:less[j] = seq[j] 前面(已扫过的部分)比它小的元素个数。
传入 seq[::-1] 再翻转结果,就得到每个位置右侧比它小的个数,
所以 V 与 ∧ 的四个计数只需要这一个函数。
"""
tree = [0] * (n + 1)
less: list[int] = []
for value in seq:
i = value - 1
s = 0
while i: # 前缀和 [1, value-1]:比 value 小的个数
s += tree[i]
i -= i & -i
less.append(s)
i = value # 把 value 插进树状数组
while i <= n:
tree[i] += 1
i += i & -i
return less
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
a = [next(data) for _ in range(n)]
left = prefix_less(a, n) # left[j]:j 左侧比 a[j] 小的个数
right = prefix_less(a[::-1], n)[::-1] # right[j]:j 右侧比 a[j] 小的个数
# 左侧更大的个数 = 左侧总数 j 减去左侧更小的;右侧同理,右侧总数是 n-1-j。
# 与右侧部分做笛卡尔积,就分别是 V(左大右大)和 ∧(左小右小)的数目。
print(sum((j - l) * (n - 1 - j - r) for j, (l, r) in enumerate(zip(left, right))),
sum(l * r for l, r in zip(left, right)))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
。树状数组被调用两次,每次 轮查询 + 轮插入, 每轮 ;末尾两个求和是 。与标准 C++ 正解(树状数组 / 线段树 / 归并)同阶。 - 空间复杂度:
,来自树状数组、两个计数数组和反转序列的拷贝。
实测(Apple Silicon / CPython 3.14):最大一组
总结
- 看到"数三元组"先问一句:这些条件能不能按中间点拆开。本题 V、∧ 的两个不等式分别只涉及
与 ,拆开后左右两侧独立,计数就从"枚举配对"变成"两个数相乘"。 - 拆完只需求"左边有多少个比它小"。这类动态前缀和(集合单调增长、反复问前缀)
是树状数组的标准信号;权值是
的排列则省掉了离散化。 - 互补量用下标总数减:四个计数只要求两个,少写一半代码也少一半出错机会。
- 反转序列复用函数:右侧计数就是反转后的左侧计数,位置上的对应关系靠
[::-1]还原。 这比写一个suffix_less更短,也不会出现两边边界处理不一致的 bug。 - 查询必须在插入之前做,这样统计范围天然是"已扫过的元素",不用再减掉自己。
图示解析
下面这张图串起从朴素枚举到
flowchart TD
A["枚举全部三元组 i,j,k:O(n^3),爆掉"] --> B["固定中间点 j<br/>两个不等式分别只涉及 (i,j) 与 (j,k)"]
B --> C["V:左边更大的个数 x 右边更大的个数"]
B --> D["∧:左边更小的个数 x 右边更小的个数"]
C --> E["只需左侧更小的个数 L 与右侧更小的个数 R<br/>其余两个由下标总数互补算出"]
D --> E
E --> F["权值树状数组正序扫描<br/>L[j] = 左侧比 a[j] 小的个数"]
E --> G["反转序列再调用同一函数<br/>R[j] = 右侧比 a[j] 小的个数"]
F --> H["V = sum (j - L[j]) * (n-1-j - R[j])"]
G --> H
F --> I["∧ = sum L[j] * R[j]"]
G --> I
读图时抓两条分叉:第一条说明为什么可以相乘(
再看一眼树状数组在这道题里到底"数"了什么。以样例前三个元素
| 扫描到 | 查询 |
得到 | 插入后 tree 中已登记的权值 |
|---|---|---|---|
| 0 | |||
| 1(只有 1) | |||
| 1(只有 1) |
这张表展示了"查询在插入之前"的含义:处理