洛谷P1908 逆序对

P1908 逆序对

题目地址:洛谷P1908 逆序对

题目描述

猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 a i > a j 且 i < j a_{i} > a_{j} 且 i < j ai>aji<j的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

输入输出

第一行一个数 n n n,表示序列中有 n n n个数。
第二行 n n n个数,表示给定的序列。序列中每个数字不超过 1 0 9 10^{9} 109

数据范围

n ≤ 5 × 1 0 5 n\le5\times10^{5} n5×105

输入样例

6
5 4 2 6 3 1

输出样例

11

思路

逆序对问题的正确解法就是用归并排序的思路,但是这道题也可以使用树状数组来写,在这里对两种解法思路进行讲解 逆序对问题的正确解法就是用归并排序的思路,但是这道题也可以使用树状数组来写,在这里对两种解法思路进行讲解 逆序对问题的正确解法就是用归并排序的思路,但是这道题也可以使用树状数组来写,在这里对两种解法思路进行讲解

归并排序解法思路

由题目易知, a i a_{i} ai前大于 a i a_{i} ai的数字可可以与其组成逆序对,因此本题实际上就是求出每个数前大于该数字的数的个数,然后总个数即为结果。
回想一下归并排序,这道题可以采用归并排序的思想来解决。我们知道,在归并排序思想中,是将数组分为全部为升序的子数组,通常是拆分为单个,因为单个必升序,然后两两进行合并,通过不断比较两数组中最小的数,并将较小的移到合并后的数组中,使得合并后的数组为升序排列。
在归并排序的过程中我们就可以得到 a i a_{i} ai前大于它的数字个数。按照归并排序的思路,当两个子数组合并时,如果左边数组当前最小值大于右边最小值的话,那么左边数组剩余的数字都会大于右边的这个最小值,而子数组的划分又是从左到右,那么左边数组中剩余的这几个数字都可以与右边最小的这个数字构成逆序对。
在这里插入图片描述

C++实现

#include<bits/stdc++.h> 

using namespace std;

#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
#define ll long long 
#define ull unsigned long long
#define lowbit(x) (x & -x)
#define endl '\n'
#define fi first
#define se second

typedef pair<ll, int> pir;
const int mod = 1e9 + 10;
const int INF = 0x7f7f7f7f;
const int N = 5e5 + 10;

int n, a[N], b[N];
ll res;

void msort(int l, int r) {
	if(l == r) return ;
	int mid = (l + r) >> 1;
	int x = l, y = mid + 1, z = l;
	msort(l, mid); msort(mid + 1, r);
	
	while(x <= mid && y <= r) {
		if(a[x] <= a[y]) b[z++] = a[x++];
		else {
			b[z++] = a[y++];
			//归并排序中[x - mid]和[y - r]中都是有序的,当左区间当前位数字 \
			  大于右区域当前位数字时,则左区域中剩余数组(包括当前位数字) \
			  都能与右区域当前位数字构成逆序对 
			res += mid - x + 1;
		}
	}
	while(x <= mid) b[z++] = a[x++];
	while(y <= r) b[z++] = a[y++];
	for(int i = l; i <= r; i ++ ) a[i] = b[i];
}

int main(void) {
	IOS
	
	cin >> n;
	for(int i = 1; i <= n; i ++ ) {
		cin >> a[i];
	}
	msort(1, n);
	cout << res << endl; 
	
	return 0;
}

树状数组解法思路

这道题当然也可以用树状数组来写,但是由于数据范围较大,需要加上离散化处理。
树状数组的思路就是依次将每个数 a i a_{i} ai作为下标,令 t [ a i ] t[a_{i}] t[ai]为1,然后用树状数组维护 t t t数组的前缀和,遍历 a a a数组,每次将 t [ a i ] t[a_{i}] t[ai]加1,然后求 t [ a i ] t[a_{i}] t[ai]的前缀和, t [ a i ] t[a_{i}] t[ai]的前缀和即为 a i a_{i} ai前小于 a i a_{i} ai的数字的个数,那么 i − t [ a i ] i- t[a_{i}] it[ai]即为大于 a i a_{i} ai的数字个数。然后求和即可。

离散化,将第i小的数字看作i (只需要满足数字间相对大小,不需要具体大小)按照求当前数字前比该数字大的数字个数作为逆序对的数量的这种统计方式,当出现相同的数字时,有一种情况会导致将两个相等数字判为逆序对:相等数字中靠前的一个被离散化标记为大于后边的数字所以需要按照从小到大排序,且相等时下标小的靠前(和sort排序规则一样)。

C++实现

#include<bits/stdc++.h> 

using namespace std;

#define IOS ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
#define ll long long 
#define ull unsigned long long
#define lowbit(x) (x & -x)
#define endl '\n'
#define fi first
#define se second

typedef pair<ll, int> pir;
const int mod = 1e9 + 10;
const int INF = 0x7f7f7f7f;
const int N = 5e5 + 10;

pir a[N];
int b[N], t[N];

void add(int x, int u) {
	for( ; x <= N; x += lowbit(x)) t[x] += u;
}

ll query(int x) {
	ll sum = 0;
	for( ; x; x -= lowbit(x)) sum += t[x];
	return sum;
}

int main(void) {
	IOS
	
	int n; cin >> n;
	for(int i = 1; i <= n; i ++ ) {
		cin >> a[i].fi; a[i].se = i;
	}
	sort(a + 1, a + 1 + n);
	//离散化 将第i小的数字看作i (只需要满足数字间相对大小,不需要具体大小) 
	//按照求当前数字前比该数字大的数字个数作为逆序对的数量的这种统计方式,\
	  当出现相同的数字时,有一种情况会导致将两个相等数字判为逆序对:\
	  “相等数字中靠前的一个被离散化标记为大于后边的数字 ” \
	  所以需要按照从小到大排序,且相等时下标小的靠前(和sort排序规则一样) 
	for(int i = 1; i <= n; i ++ ) b[a[i].se] = i;
	
	ll res = 0;
	for(int i = 1; i <= n; i ++ ) {
		//按照序列从左到右将数据的值对应的位置的数+1,代表又有一个数出现
		add(b[i], 1);
		//此时query()返回值为前i个数中小于b[i]的数字个数
		//第i项前大于b[i]的数字都可以与b[i]构成逆序对 
		res += i - query(b[i]);
	}
	
	cout << res << endl;
	
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值