【蓝桥杯AcWing】专题———逆序数(重点)

最初接触到逆序数是在离散数学中,逆序数的概念如下:

给出一个有N个数的序列,如果一对数中前面的数大于后面的数,那么它们就称为一个逆序。一个序列中逆序的总数就称为这个排列的逆序数。
如序列 2 4 3 1 的逆序数为4(2和1,4和3,4和1,3和1)

逆序数的解法有多种,这里介绍归并排序、树状数组,时间复杂度均为O(n log n)、
***注:短期内归并排序的方法更易掌握,文末提供了两道题目可以练练手~

法一:归并求解逆序数

算法思想

  • 从问题的源头出发,如果前面大于后面,就构成一组逆序,而这就是归并排序中交换两个数的条件;
  • 结构体a[].val存储值, a[].num存储逆序组数
  • 因为只计算逆序数,对于一组逆序只用标记1次即可,这里将对较大的数进行记录。

如对于 2 4 3 1 只标记较大数,逆序记录为 1 2 1 0
归并后的结果:
a[].val 1 2 3 4
a[].num 0 1 1 2

————————————开始归并————————————

#include <iostream>

using namespace std;

const int maxn = 100005;
struct note{
	int val;
	int num;//逆序 
};
struct note a[maxn], b[maxn];
void Merge(int l, int mid, int r){
	int i = l, j = mid+1;
	int k = l;
	while(i <= mid && j <= r){
		if(a[i].val <= a[j].val){
			b[k++] = a[i++];
			b[k-1].num += (j-mid-1);
		}else{
			b[k++] = a[j++];
			//b[k-1].num += (mid-i+1);	
		}
	}
	while(i <= mid){
		b[k++] = a[i++];
		b[k-1].num += (r-mid);
	}
	while(j <= r){
		b[k++] = a[j++];
	}
	for(k = l; k <= r; ++k){
		a[k] = b[k];
	}
}
void MergeSort(int l, int r){
	if(l < r){
		int mid = (l+r) >> 1;
		MergeSort(l, mid);
		MergeSort(mid+1, r);
		Merge(l, mid, r);
	}
}
int main(){
	int n;
	cin >> n;
	for(int i = 0; i < n; ++i){
		cin >> a[i].val;
	}
	MergeSort(0, n-1);
	int ans = 0;
	for(int i = 0; i < n; ++i){//逆序数即逆序的组数累加
		ans += a[i].num;
	}
	cout << ans <<endl;
	return 0;
}

其实记录逆序的过程有助于对归并过程的加深理解;
解释一下Merge()中的这两行吧:
b[k-1].num += (r-mid);
b[k-1].num += (j-mid-1);
正因为Merge()是对两组有序的序列合并,所以这两行记录的是,a[i].val后面比它小的有多少;

  • 【注】:如果要求出逆序对,那么既要记录a[i].val后面比它小的组数,还要记录前面比它大的组数;
    其实只加一行代码即可,就是注释掉的那句
    //b[k-1].num += (mid-i+1);
    同样对于 2 4 3 1 各组逆序对结果:
    1 2 1 0
    0 0 1 3
    归并后的结果为:
    a[].val 1 2 3 4
    a[].num 3 1 2 3

法二:树状数组求解逆序数

  • 同样从问题的源头出发,还是对于一组逆序,这里只对较大的数进行标记
  • 记录a[i]后面有多少个小于它的数,显然这需要从后往前遍历
    对于 2 4 3 1 从后往前记录逆序 1 2 1 0
    粗暴的方法当然是两层循环了,可以结合树状数组的好处(单点更新、区间求和)进行改进
    有关树状数组的介绍可以参考我的另一篇博客:

算法思想

  1. 构建C[1:n]的树状数组(考虑到a[]可能数据过大,可以离散化一下,后面会有介绍),初始化为0
  2. 从后往前遍历时,单值更新a[i]+1,区间加1即可,每次单值查询即可

还原一下逆序遍历2 4 3 1过程中录入C[]的原数据 A[]的变化吧

C[]的原数据 A[]:
A[1] A[2] A[3] A[4]
0 1 0 0
0 1 0 1
0 1 0 1
0 1 1 1
单点更新即可,树状数组实现对其区间求和依次为0 1 2 1,最后累加得到逆序数

————————————完整代码————————————

#include <iostream>
#include <algorithm>

using namespace std;

const int maxn = 100005;
int n;
int tree[maxn], Hash[maxn];
int data[maxn];
struct note{
	int value;
	int index;
}a[maxn];
bool comp(struct note x, struct note y){
	return x.value < y.value;
}
int lowbit(int x){
	return x&(-x);
}
void updata(int i, int temp){
	while(i <= n){
		tree[i] += temp;
		i += lowbit(i);
	}
}
int getsum(int i){
	int res = 0;
	while(i > 0){
		res += tree[i];
		i -= lowbit(i);
	}
	return res;
}
int main(){
	cin >> n;
	for(int i = 1; i <= n; ++i){
		cin >> a[i].value;
		a[i].index = i;
	}//离散化
	sort(a+1, a+n+1, comp);
	for(int i = 1; i <= n; ++i){
		if(i != 1 && a[i].value == a[i-1].value)
			Hash[a[i].index] = Hash[a[i-1].index];
		else
			Hash[a[i].index] = i;
	}
	int ans = 0;
	for(int i = n; i >= 1; --i){
		//cout << getsum(Hash[i]) <<" ";
		ans += getsum(Hash[i]);
		if(Hash[i] != n){
			updata(Hash[i]+1, 1);
		}
	}
	cout << ans << endl;
	
	return 0;
}

***注:不就是单点更新,区间求和吗,线段树也可以搞定,但还是树状数组实现起来简单些

·【acwing】·逆序对的数量

题目链接:·【蓝桥】·逆序对的数量

  • 裸裸的求逆序数,记得开long long就没有问题了

————————————完整代码————————————

#include <cstdio>

using namespace std;

typedef long long ll;
const int maxn = 1e5 + 5;
int n;
struct note{
	int value;
	ll ans;
};
struct note a[maxn], b[maxn];

void merge(int l, int middle, int r){
	int low = l, high = middle + 1;
	int k = l;
	while(low <= middle && high <= r){
		if(a[low].value <= a[high].value){
			b[k++] = a[low++];
			b[k-1].ans += (high - middle - 1);
		}else{
			b[k++] = a[high++];
		//	b[k-1].ans += (middle - low + 1);
		}
	}
	while(low <= middle){
		b[k++] = a[low++];
		b[k-1].ans += (r - middle);
	}
	while(high <= r){
		b[k++] = a[high++];
	}
	for(int i = l; i <= r; ++i){
		a[i] = b[i];
	}
}
void mergeSort(int l, int r){
	if(l < r){
		int middle = (l + r) >> 1;
		mergeSort(l, middle);
		mergeSort(middle + 1, r);
		merge(l, middle, r);
	}
}
int main(){
	scanf("%d", &n);
	for(int i = 0; i < n; ++i){
		scanf("%d", &a[i].value);
	}
	mergeSort(0, n-1);
	ll ans = 0;
	for(int i = 0; i < n; ++i){
		ans += a[i].ans;
		//printf("%d ", a[i].ans);
	}
	printf("%lld\n", ans);
	return 0;
}

·【蓝桥】·小朋友排队

题目链接:·【蓝桥】·小朋友排队

  • 本题其实求的就是逆序对,不过在最后处理的时候,注意需要前n项求和(即某个小朋友不高兴程度总和)

如输入样例:
3
3 2 1
——————
逆序后的结果为
a[].val 1 2 3
a[].num 2 2 2
对于1,需要交换两次,而不高兴程度是累加的,因此最后是1+2

————————————完整代码————————————

#include <cstdio>

using namespace std;

typedef long long ll;
const int maxn = 1e5 + 5;
int n;
struct note{
	int value;
	ll ans;
};
struct note a[maxn], b[maxn];

void merge(int l, int middle, int r){
	int low = l, high = middle + 1;
	int k = l;
	while(low <= middle && high <= r){
		if(a[low].value <= a[high].value){
			b[k++] = a[low++];
			b[k-1].ans += (high - middle - 1);
		}else{
			b[k++] = a[high++];
			b[k-1].ans += (middle - low + 1);
		}
	}
	while(low <= middle){
		b[k++] = a[low++];
		b[k-1].ans += (r - middle);
	}
	while(high <= r){
		b[k++] = a[high++];
	}
	for(int i = l; i <= r; ++i){
		a[i] = b[i];
	}
}
void mergeSort(int l, int r){
	if(l < r){
		int middle = (l + r) >> 1;
		mergeSort(l, middle);
		mergeSort(middle + 1, r);
		merge(l, middle, r);
	}
}
int main(){
	scanf("%d", &n);
	for(int i = 0; i < n; ++i){
		scanf("%d", &a[i].value);
	}
	mergeSort(0, n-1);
	ll ans = 0;
	for(int i = 0; i < n; ++i){
		ans += (a[i].ans+1)*a[i].ans/2;	//特别要改动的地方
		//printf("%d ", a[i].ans);
	}
	printf("%lld\n", ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值