You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
python/TangDou/AcWing/BIT/788_树状数组+线段树.md

314 lines
9.0 KiB

2 years ago
##[$AcWing$ $788$. 逆序对的数量](https://www.acwing.com/problem/content/790/)
### 一、题目描述
给定一个长度为 $n$ 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 $i$ 个和第 $j$ 个元素,如果满足 $i<j$
且 $a[i]>a[j]$,则其为一个逆序对;否则不是。
**输入格式**
第一行包含整数 $n$,表示数列的长度。
第二行包含 $n$ 个整数,表示整个数列。
**输出格式**
输出一个整数,表示逆序对的个数。
**数据范围**
$1≤n≤100000$,数列中的元素的取值范围 $[1,10^9]$
**输入样例:**
```cpp {.line-numbers}
6
2 3 4 5 6 1
```
**输出样例:**
```cpp {.line-numbers}
5
```
### 二、前导知识
- **如何获取第几大+原始位置的对应关系数组?**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
int n;
int a[N], b[N];
// 输出原数组
void out() {
for (int i = 1; i <= n; i++) printf("%d ", a[i]);
puts("");
}
// 功能:排序办法
// ①、数小的在前面
// ②、如果数一样大,序号小的在前
bool cmp(int x, int y) {
if (a[x] == a[y]) return x < y;
return a[x] < a[y];
}
// 【方法1】
void func1() {
cin >> n;
// ① 初始化 b[i]=a[i]
// ② 排序(小的在前,大的在后) : a[]
// ③ 每个b[i]通过二分,在排序后的a[]中查找位置记录b[i]=找到的位置
// ④ a[]数组被重新由小到大排序
for (int i = 1; i <= n; i++) cin >> a[i], b[i] = a[i];
// 输出原始数组
out();
// 原始数组由小到大排序
// ① 由小到大排序,不用加什么greater<int>(),代码更简单
// ② 只有由小到大排序后面使用二分进行数值查询位置时才能使用lower_bound二分方法
sort(a + 1, a + n + 1);
// 离散化遍历原始数组b[i],找出它的排名即b[2]=3表示第2个输入的是排名第3的
// 这里的离散化没有用到去重如果需要去重参考788_Unique.cpp
for (int i = 1; i <= n; i++) b[i] = lower_bound(a + 1, a + n + 1, b[i]) - a;
// 倒序,由大到小(视业务调整)
for (int i = n; i; i--) printf("%d ", b[i]);
}
// 【方法2】
void func2() {
cin >> n;
// ① 初始化 b[i]=i
// ② 排序(小的在前,大的在后) : b[]
// ③ a[]数组不动
for (int i = 1; i <= n; i++) cin >> a[i], b[i] = i;
// 输出原始数组
out();
// 对b[]排序
sort(b + 1, b + n + 1, cmp);
// 倒序,由大到小(视业务调整)
for (int i = n; i; i--) printf("%d ", b[i]);
}
/*
原始数组 10 8 7 9 4
输出 1 4 2 3 5
含义:
1最大的是原始数组的a[1]=10
2次大的是原始数组的a[4]=9
3三大的是原始数组的a[2]=8
...
*/
int main() {
// 调用方法1进行测试
freopen("788.in", "r", stdin);
func1();
puts("");
// 在第二次重定向时修改一下cin的状态
cin.clear();
puts("");
// 调用方法2进行测试
freopen("788.in", "r", stdin);
func2();
puts("");
return 0;
}
```
- **为什么不喜欢使用结构体进行描述信息?**
主要是因为我们想使用一种通用的办法:**排序+离散化+二分** 来获取对应关系数组,这时朴素的数组比较方便,带结构体的数组不太好用。
### 二、树状数组解法
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
typedef long long LL;
int a[N], b[N];
int n;
int tr[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
LL ans;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], b[i] = a[i];
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++) b[i] = lower_bound(a + 1, a + 1 + n, b[i]) - a;
for (int i = 1; i <= n; i++) {
add(b[i], 1);
ans += sum(n) - sum(b[i]);
}
printf("%lld", ans);
return 0;
}
```
### 三、$d$数组的作用
数组$a[]$中的每个元素$a[i]$有三个属性:
- ① 数值 $value$
- ② 整体排名 $rank$ (由大到小排序)
- ③ 原始位置 $pos$
**当我们求逆序对时,只关心 ② 和 ③。 ① 在这里是一个临时的概念,它的存在导致可以求出整体的排名 ②**
$d[]$记录的就是 ② 整体的排名 $rank$ + ③ 原来的位置 $pos$
比如:$d[x]=y$ 表示排名第$x$位的数,原始位置是$y$
**$Q$: 排名数组$d[]$的构建步骤?**
$A$:
- 随着$a[i]$读入,记录$d[i] = i$ ,即记录输入序号,当比较数值一样时,用来判断哪个在前
- 实现比较函数 $cmp$, 如果值不一样,值大的在前;如果值一样,则输入序号大的在前
- $sort(d+1,d+1+n,cmp)$ 调用$STL$+自定义$cmp$ 对$d$数组进行排序,最终的排序结果如下面的栗子:
**输入**
$a[]$ 值 `4 2 3 5 1`
$d[]$ 序号 `1 2 3 4 5`
**结果**
$a[]$ 值 `4 2 3 5 1`
$d[]$ 序号 `4 1 3 2 5`
可以清楚看出:
- 原数组没有改变
- $d[1]=4$,表示第$1$名,值最大的,在$a[]$中第$4$个位置上,最大值$=a[d[1]]=a[4]=5$
$d[2]=1$,表示第$2$名,值次大的,在$a[]$中第$1$个位置上,次大值$=a[d[2]]=a[1]=4$
....
**算法步骤**
1、排名由高到低逐个让每个数字下场,也就是 **大的数字先入场**,占领它在原始数组中的位置
2、当某个数字下场时检查它左侧(也就是号比自己小的),现在已经有多少个数字存在,这此数字都符合一个特点:
**数比自己大,号比自己小**,也就是 **逆序对的数量**。
### 四、树状数组的作用
就是可以维护一个 **动态前缀和** ,这是我起的名字,不一定准确,但事就是这么个事。普通前缀和适合于静态数组,像这样一会加进来一个,就需要计算一下前缀和,一会又加进来一个,还想算前缀和的,树状数组的优势就来了,虽然达不到$O(1)$,但$O(logN)$也是很不错的运算速度了。
### 五、疑问
**是不是可以最大排序向左看,最小排序向右看呢?需要测试一下**
答案是一样可以$AC$!原理是一样的,从本质上说应该没有大的在前快,因为需要多计算一下$sum$,但实测效果两者基本一致。
### 六、线段树解法
```cpp {.line-numbers}
// 权值线段树解法
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5e5 + 10;
int a[N], b[N];
int n;
// 线段树区间和模板
#define ls u << 1
#define rs u << 1 | 1
struct Node {
int l, r; // 范围
int sum; // 区间内数字个数
} tr[N << 2]; // 4
// 创建
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) return;
int mid = l + r >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
}
// 向上更新统计信息
void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum;
}
// 修改点的值
void modify(int u, int x, int v) {
if (tr[u].l == x && tr[u].r == x) {
tr[u].sum += v;
return;
}
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid)
modify(ls, x, v);
else
modify(rs, x, v);
pushup(u);
}
// 查询区间和
int query(int u, int l, int r) {
if (l > r) return 0;
if (tr[u].l == l && tr[u].r == r) return tr[u].sum;
int mid = tr[u].l + tr[u].r >> 1;
if (mid < l) return query(rs, l, r);
if (r <= mid) return query(ls, l, r);
return query(ls, l, mid) + query(rs, mid + 1, r);
}
LL ans;
int main() {
cin >> n;
// 一般使用线段树都喜欢从下标1开始所以我们在处理数据时尽量让下标从1开始
// b[]在下面的代码中有两个阶段的含义目前的含义是a[i]的备份因为一会a[]由小到大排序后原始数组就丢失了位置信息就不见了需要b备份出来
for (int i = 1; i <= n; i++) cin >> a[i], b[i] = a[i];
// ① 原始数组由小到大排序
sort(a + 1, a + n + 1);
// ② 离散化遍历原始数组b[i],找出它的排名即b[2]=3表示第2个输入的是排名第3的
for (int i = 1; i <= n; i++) b[i] = lower_bound(a + 1, a + n + 1, b[i]) - a;
// 构建线段树
build(1, 1, n); // n个数字
// 边查边添加
for (int i = 1; i <= n; i++) {
modify(1, b[i], 1); // 占据b[i]这个位置
ans += query(1, b[i] + 1, n); // 出现的比我早,号还比我大的有多少个
}
// 输出
printf("%lld\n", ans);
return 0;
}
```
### 七、总结
- 树状数组讲究的是逐个下场,动态统计前缀和