目录
一.题目
题目描述
设A[1..n]是一个包含N个数的数组。如果在i〈 j的情况下,有A[i] 〉a[j],则(i,j)就称为A中的一个逆序对。 例如,数组(3,1,4,5,2)的“逆序对”有 <3,1>,<3,2>,<4,2>,<<5,2> 共4个。 使用 归并排序 可以用O(nlogn)的时间解决统计逆序对个数的问题 。
输入
第1行:1个整数N表示排序元素的个数。(1≤N≤100000) 第2行:N个用空格分开的整数,每个数在小于100000。
输出
1行:仅一个数,即序列中包含的逆序对的个数。
样例输入
3
1 3 2
样例输出
1
题解
这是一道十分简单的树状数组引入题目。相信大家一定都做过逆序对吧,首先来看二路归并的代码:
二路归并
十分简单,只要会二分排序,那么就一定会这道题:
代码如下:
#include <cstring>
#include <cstdio>
#define M 100005
int n, a[M], b[M], c[M];
long long ans;
inline void bing(int l, int mid, int r){
int k = l, k1 = mid + 1, k2 = l;
while(k <= mid && k1 <= r){
if(a[k] <= a[k1])
b[k2 ++] = a[k ++];
else{
ans = ans + mid - k + 1;
b[k2 ++] = a[k1 ++];
}
}
while(k <= mid)
b[k2 ++] = a[k ++];
while(k1 <= r)
b[k2 ++] = a[k1 ++];
for(int i = l; i <= r; i ++)
a[i] = b[i];
}
inline void fen(int l, int r){
int mid = (l + r) / 2;
if(l >= r)
return ;
fen(l, mid);
fen(mid + 1, r);
bing(l, mid, r);
}
int main (){
scanf("%d", &n);
for(int i = 1; i <= n; i ++)
scanf("%d", &a[i]);
fen(1, n);
printf("%lld", ans);
return 0;
}
但有时这种方法也许处理不了大数据,我们接下来看另一种新方法:
树状数组
我们的主要思想就是:每次往后找到比这个数更小的数,且使那一个数的BIT加1,最后再一个数一个数地算前缀和即可。
但是,如果这样的话,有的数有可能会很大,那么我们的数组就会爆掉,于是,引入离散化:
这是我的个人解释:
当一个数列中,数字十分的大但是我们只需要这些数字在数列中所在位置时,就可以用离散化。如图所示,就是一个离散化后的结果:
注:原数组的下表是那个数本身的位置,离散化后是从小到大的每个数的位置。
那么,怎么做到呢?主要思想就是:先存下每个数的原始位置,然后将它们排序,又用另一个数组存下每个数排序后的位置,离散化就完成了。(注意去重)
有两种方法:
1.数组
显而易见,就用以上方法:
代码如下:
for(int i = 1; i <= n; i ++){
scanf("%d", &a[i].val);
a[i].id = i;
}
sort(a + 1, a + n + 1); //定义结构体时按val从小到大重载
for(int i = 1; i <= n; i ++)
b[a[i].id] = i; //将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值
2.STL+二分
总体思想差不多,只是用了更高级的函数而已。
代码如下:
#include<algorithm> // 需要的头文件
//n原数组大小 num原数组中的元素 lsh离散化的数组 cnt离散化后的数组大小
int lsh[MAXN] , cnt , num[MAXN] , n;
for(int i=1; i<=n; i++)
{
scanf("%d",&num[i]);
lsh[i] = num[i]; //复制一份原数组
}
sort(lsh+1 , lsh+n+1); //排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
//cnt就是排序去重之后的长度
cnt = unique(lsh+1 , lsh+n+1) - lsh - 1; //unique返回去重之后最后一位后一位地址 - 数组首地址 - 1
for(int i=1; i<=n; i++)
num[i] = lower_bound(lsh+1 , lsh+cnt+1 , num[i]) - lsh;
//lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址 - 数组首地址 ,从而实现离散化
要介绍几个函数:
(1)unique(起始下标,终止下标)
unique返回去重之后最后一位后一位地址 。在其后减lsh是因为unique返回的是一个指针,要减lsh才返回那一个地址;减1是因为unique返回的是最后一位后一位地址,所以要减1。
(2)lower _bound(起始下标,终止下标,查找的值)
lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址,减lsh的原因同上。
有了离散化,我们就能很方便地用树状数组求值了。首先进入一个1到n的循环,每次就是往后更新一遍树状数组再找一次前缀和即可。前缀和找的就是比这个数小的数已经放了多少个,再用i去减这些数共有多少个,就求出了当前比这个数大的数有多少个,再全部累加起来就求出了共有多少个逆序对。
如下图:
1.初始化:
2.第一个:
3.
4.
5.
6.注意再次有一个去重操作,因为有两个数离散后下标都是1,所以直接在C数组下标1的位置再加1。
7.最后一次操作:
所以代码如下:
#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;
#define M 100005
struct node{
int v, id;
}a[M];
int n, b[M], c[M];
long long ans;
bool cmp (node x, node y){
return x.v < y.v;
}
int lowbit(int i){
return i & -i;
}
void update(int k, int x){
for(int i = k; i <= n; i += lowbit(i))
c[i] += x;
}
long long SUM (int x){
int s = 0;
for(int i = x; i >= 1; i -= lowbit(i))
s += c[i];
return s;
}
int main (){
scanf("%d", &n);
for(int i = 1; i <= n; i ++){
scanf("%d", &a[i].v);
a[i].id = i;
}
sort(a + 1, a + 1 + n, cmp);
int cnt = 0;
for(int i = 1; i <= n; i ++){
if(a[i].v != a[i - 1].v)//去重操作
cnt ++;
b[a[i].id] = cnt;
}
for(int i = 1; i <= n; i ++){
update (b[i], 1);
ans += i - SUM (b[i]);
}
printf("%lld", ans);
return 0;
}
二.举一反三
上题是直接告诉你要求逆序对,如果是下题呢?
题目:冒泡排序(从小到大)
描述:将一列数用冒泡排序,问最少交换多少次。
很明显,这道题不能直接模拟过程。因为从小到大排序,那么原本有序的两个数就根本不用动,只有两个数之间是逆序关系 才会交换两数。所以他要求的就是逆序对的个数
三.总结
树状数组是十分有用的,再查讯某数的位置和某区间数的总和十分有用,所以要好好学树状数组。