求解逆序对
什么是逆序对
逆序对就是如果\(i>j\&\& a[i] ,这两个数就是一个逆序对。其实换句话说,找到排在自己前面比自己大的数的个数。
给定一个\(n\)个数的序列,求解序列中逆序对的个数\(n \leq 5 \times 10^5,a[i]\leq 10^9\)。
输入样例
6
5 4 2 6 3 1
输出样例
11
求解逆序对
方法一:暴力求解
使用两个循环去跑。
#include
using namespace std;
typedef long long ll;
#define endl '\n'
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define _for(i, a, b) for (int i=(a); i<=(b); i++)
const int INF = 0x7fffffff;
const int MAXN = 1e6 + 10;
int n, arr[MAXN];
ll res;
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> arr[i];
}
for(int i = 1; i <= n; i++){
for(int j = 1; j < i; j++){
if(arr[j] > arr[i]) res++;
}
}
cout << res << endl;
return 0;
}
方法二:对并排序解法
首先我们要知道什么是归并排序。(如果你知道可以点)然后,我们可以这样思考:如果我们将一个序列排成从小到大的有序序列,那么我们每次划分合并后的左右序列都是从小到大的有序序列,我们只需要记录右区间每个数分别会与左区间产生多少逆序对即可。
如果不理解的话,我们可以看个例子:
首先,我们对输入数组一直对半拆分,直到区间内只有一个元素的时候,一个元素肯定是有序数组,然后我们在依次合并两个有序数组,计算逆序对的个数就发生在合并的过程中,一直重复整个过程,直到整个数组有序。(整个过程大致如下)
相信大家也都看懂了(迷之自信^ _ ^)
由于归并排序没有什么坑,正常的执行并统计即可,注意答案可能会爆\(int\),所以我们需要使用\(long\quad long\)来存储。时间复杂度和普通的归并排序一样\(O(Nlog_2N)\)。
#include
using namespace std;
typedef long long ll;
#define endl '\n'
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define _for(i, a, b) for (int i=(a); i<=(b); i++)
const int INF = 0x7fffffff;
const int MAXN = 1e6 + 10;
int n, a[MAXN], b[MAXN];
ll res;
void msort(int l, int r){//归并排序
if(l == r) return ;
int mid = l + (r - l) / 2;//这里是二分的一个坑,如果l,r两个数很大,可能导致l+r出错。
int i = l, j = mid + 1, k = l;
msort(l,mid);
msort(mid+1,r);
while(i <= mid && j <= r){
if(a[i] <= a[j]) b[k++] = a[i++];
else{
b[k++] = a[j++];
res += mid - i + 1; //记录答案
}
}
while(i <= mid){
b[k++] = a[i++];
}
while(j <= r){
b[k++] = a[j++];
}
for(int t = l; t <= r; t++){
a[t] = b[t];
}
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
}
msort(1,n);
cout << res << endl;
return 0;
}
方法三:树状数组求解
-
首先,我们要思考一个问题,我们要怎样记录第\(i\)个数与\(1\dots i-1\)个数构成的逆序对呢?
我们都知道,初始化的树状数组全为\(0\),现在我们按照序列从左到右将数值对应的位置加一,表示有一个数出现。因此,在循环到第\(i\)个数的时候,前\(i-1\)个数也已经加入树状数组中,树状数组内比\(a_i\)大的都会与\(a_i\)构成逆序对,因为它们肯定会比\(a_i\)出现的早,所以产生的逆序对数量为\(i-getsum(a_i)\)。
注意:\(getsum(a_i)\)是在树状数组中询问\(1\dots a_i\)项的前缀和。 -
接下来,我们来思考新的问题,如果\(a_i\)的数值过大,树状数组的内存空间不够怎么办?
我们先来看一个例题:
我们都知道1000 5 6这三个数的逆序对是2
同样的3 1 2这三个数的逆序对也是2
所以说,上面的两个序列是等价的,因为无论3还是1000,都大于第二项和第三项,因此我们知道,我们只需要数据之间的相对大小,只需要满足大于或小于本身,与大多少无关。
这就启发我们对数据进行。先将数据进行排序,再用\(1\)到\(n\)分别对应\(n\)个数表示它们的相对大小,对新的序列建树状数组空间就够了。
-
下面,我们思考最后一个问题,相等的元素是否会导致求解错误?
不处理话会出现错误,问题的关键在于是否有和\(a_i\)相等的元素在\(a_i\)之前被加入且相对大小标记更大。出现这种情况就会误将两个相等的元素判为逆序对。那么我们要怎样解决呢?其实我们只要将所有与\(a_i\)相等的元素,先出现的标记更小就可以了。
同时再次注意答案可能会爆\(int\),所以我们需要使用\(long\quad long\)来存储。\(O(Nlog_2N)\)。
由于我们不仅要排序,还要建树状数组,所以,树状数组相对归并排序会慢一些。但是当数值域小的时候树状数组会更快,两者均有有点,我们都需掌握。
#include
using namespace std;
typedef long long ll;
#define endl '\n'
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define _for(i, a, b) for (int i=(a); i<=(b); i++)
const int INF = 0x7fffffff;
const int MAXN = 1e6 + 10;
int n, b[MAXN];
struct node{
int val, id;
bool operator < (const node & rhs) const{
return (this->val < rhs.val) || (this->val == rhs.val && this->id < rhs.id);//从小到大排序,数值相同id小的在前 解决第三个问题
}
}arr[MAXN];
inline int lowbit(int x){
return (x & -x);
}
void add(int x, int d){
while(x <= n){
b[x] += d;
x += lowbit(x);
}
}
ll sum(int x){
ll res = 0;
while(x > 0){
res += b[x];
x -= lowbit(x);
}
return res;
}
int main(){
ll res = 0;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> arr[i].val;
arr[i].id = i;
}
sort(arr+1,arr+1+n);//离散化数值,解决第二个问题
for(int i = 1; i <= n; i++){
add(arr[i].id,1);//离散化结果—— 下标等效于数值
res += (i - sum(arr[i].id));//计算逆序对
}
cout << res << endl;
return 0;
}