逆序对总结
概念
度娘是这么说的:
设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。
如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <a[i], a[j]="">这个有序对称为 A 的一个逆序对,也称作逆序数
求法
- 通常情况下有这几种求逆序对的方法
- $O(n^{2}$)暴力,几乎没有用,不再介绍
- $O(n*logn)$的归并排序和树状数组.
归并排序
思想
仔细想想归并排序的核心思想;
实际上归并排序的交换次数就是这个数组的逆序对个数;
归并排序是将数列a[l,h]分成两半a[l,mid]和a[mid+1,h]分别进行归并排序,然后再将这两半合并起来。
在合并的过程中(设l<=i<=mid,mid+1<=j<=h),当a[i]<=a[j]时,并不产生逆序数;当a[i]>a[j]时,在前半部分中比a[i]大的数都比a[j]大,将a[j] 放在a[i]前面的话,逆序数要加上mid+1-i。
因此,可以在归并排序中的合并过程中计算逆序数.
代码
#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
const int N = 1005;
int a[N],tmp[N];
int ans;
void Merge(int l,int m,int r)
{
int i = l;
int j = m + 1;
int k = l;
while(i <= m && j <= r)
{
if(a[i] > a[j])
{
tmp[k++] = a[j++];
ans += m - i + 1;
}
else
{
tmp[k++] = a[i++];
}
}
while(i <= m) tmp[k++] = a[i++];
while(j <= r) tmp[k++] = a[j++];
for(int i=l;i<=r;i++)
a[i] = tmp[i];
}
void Merge_sort(int l,int r)
{
if(l < r)
{
int m = (l + r) >> 1;
Merge_sort(l,m);
Merge_sort(m+1,r);
Merge(l,m,r);
}
}
int main()
{
int n,T,tt=1;
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
ans = 0;
Merge_sort(0,n-1);
printf("Scenario #%d:
%d
",tt++,ans);
}
return 0;
}
树状数组
先理解树状数组在该背景下的意义
我们假设一个数组
A[n]
,当A[n]=0
时表示数字n在序列中没有出现过,A[n]=1
表示数字n在序列中出现过。A对应的树状数组为c[n]
,则c[n]
对应维护的是数组A[n]
的内容,即树状数组c可用于求A中某个区间的值的和。树状数组的插入函数(假设为 void insert(int i,int x) )的含义:在求逆序数这个问题中,我们的插入函数通常使用为
insert( i , 1 )
,即将数组A[i]的值加1 (A数组开始应该初始化为0,所以也可以理解为设置A[ i ]的值为1,即将数字i 加入到序列的意思 )。,同时维护c数组的值。树状数组中区间求和函数(假设函数定义为: int getsun(int i ) )的含义:该函数的作用是用于求序列中小于等于数字 i 的元素的个数。这个是显而易见的,因为树状数组c 维护的是数组A的值,则该求和函数即是用于求下标小于等于 i 的数组A的和,而数组A中元素的值要么是0要么是1,所以最后求出来的就是小于等于i的元素的个数。
所以要求序列中比元素a大的数的个数,可以用
i - getsum(a)
即可( i 表示此时序列中元素的个数)。
如何使用树状数组求逆序数总数:
首先来看如何减小问题的规模:
要想求一个序列 a b c d,的逆序数的个数,可以理解为先求出a b c的逆序数的个数k1,再在这个序列后面增加一个数d,求d之前的那个序列中值小于d的元素的个数k2,则k1+k2即为序列a b c d的逆序数的个数。
举个例子加以说明:
假设给定的序列为 4 3 2 1,我们从左往右依次将给定的序列输入,每次输入一个数temp时,就将当前序列中大于temp的元素的个数计算出来,并累加到ans中,最后ans就是这个序列的逆序数个数。
序列的变化 | 序列中大于新增加的数字的个数 | 操作 |
---|---|---|
{} | 0 | 初始化时序列中一个数都没有 |
{4} | 0 | 往序列中增加4,统计此时序列中大于4的元素个数 |
{4 3} | 1 | 往序列中增加3,统计此时序列中大于3的元素个数 |
{4 3 2} | 2 | 往序列中增加2,统计此时序列中大于2的元素个数 |
{4 3 2 1} | 3 | 往序列中增加1,统计此时序列中大于1的元素个数 |
当所有的元素都插入到序列后,即可得到序列{4 3 2 1}的逆序数的个数为1+2+3=6.
代码
#include <iostream>
#include <string>
using namespace std;
#define N 1010
int c[N];
int n;
int lowbit(int i)
{
return i&(-i);
}
int insert(int i,int x)
{
while(i<=n){
c[i]+=x;
i+=lowbit(i);
}
return 0;
}
int getsum(int i)
{
int sum=0;
while(i>0){
sum+=c[i];
i-=lowbit(i);
}
return sum;
}
void output()
{
for(int i=1;i<=n;i++) cout<<c[i]<<" ";
cout<<endl;
}
int main()
{
while(cin>>n){
int ans=0;
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++){
int a;
cin>>a;
insert(a,1);
ans+=i-insert(a);//统计当前序列中大于a的元素的个数
}
cout<<ans<<endl;
}
return 0;
}
应用实例
寒假作业
有n项寒假作业。a给每项寒假作业都定义了一个疲劳值Ai,表示抄这个作业所要花的精力。b现在想要知道,有多少组连续的寒假作业的疲劳值的平均值不小于k?
题目大意
简单地说,给定n个正整数A1,A2,A3,…,An,求出有多少个连续的子序列的平均值不小于k。
思路
- 我们可以求前缀和sum,若要满足题意则
(sum[j]-sum[i-1])/(j-i+1)>=k
。- 等价于
(a[i]-k)+(a[i+1]-k)+…+(a[j]-k)/(j-i+1)>=0
; - 即
(a[i]-k)+(a[i+1]-k)+…+(a[j]-k)>=0
;
- 等价于
- 我们可以另设一个数组 b[],使
b[i]=a[i]-k
;- 等价于
b[i]+b[i+1]+…+b[j]>=0
- 所以可以再对b数组用一次前缀和,则有
pre[j]-pre[i-1]>=0
- 等价于
所以说,这是“顺”序对。
代码
#pragma GCC optimize("Ofast")
#include<cstdio>
#include<iostream>
#include<algorithm>
#define redir(name) freopen(name".in","r",stdin),freopen(name".out","w",stdout)
using namespace std;
inline char gc(){
static char buf[1<<17],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<17,stdin),p1==p2)?EOF:*p1++;
}
template<class T>
inline void read(T&n){
register char ch=gc();
for(n=0;ch<'0'||ch>'9';ch=gc());
for(;ch>='0'&&ch<='9';ch=gc()) n=(n<<1)+(n<<3)+ch-'0';
}
const int N=1000010;
int a[N],b[N],n,k,s;
long long sum[N];
long long t[N];
void mergesort(int l,int r) {
if(l==r)
return;
int mid=(l+r)/2;
int p=l;
int i=l;
int j=mid+1;
mergesort(l,mid);
mergesort(mid+1,r);
while(i<=mid&&j<=r) {
if(sum[j]>=sum[i]) {
s+=mid-i+1;
t[p]=sum[j];
p++;
j++;
} else {
t[p]=sum[i];
p++;
i++;
}
}
while(i<=mid) {
t[p]=sum[i];
p++;
i++;
}
while(j<=r) {
t[p]=sum[j];
p++;
j++;
}
for(i=l; i<=r; i++)
sum[i]=t[i];
}
int main(){
redir("choose");
read(n),read(k);
for(unsigned int i=1;i<=n;++i) read(a[i]);
for(unsigned int i=1;i<=n;++i) sum[i]=sum[i-1]+a[i]-k;
mergesort(0,n);
printf("%d
",s);
return 0;
}