求逆序对(归并排序 / 树状数组)

15 篇文章 0 订阅
7 篇文章 0 订阅

逆序对: 对于给定的一段正整数序列,逆序对就是序列中 ai > aj 且 i < j 的有序对。

(逆序数的求法至少有三种:枚举法、分治法、树状数组。分治法和树状数组时间复杂度都是O( Nlog2N )。。下面先说说分治法。。。)

归并排序解法(分治法):

归并排序(merge sort)是建立在归并操作上的一种有效的排序算法。 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
时间复杂度为O(nlogn),空间复杂度为 O(n),归并排序比较占用内存,但却效率高且是稳定的算法

//在某个时候,左区间:  5 6 7  下标为 i
//           右区间:  1 2 9  下标为 j
//          (数组若从下标为0开始,则mid=2,若从下标为1开始,则mid=3)
//这个时候我们进行合并:
//step 1:由于 5>1,所以产生了逆序对,这里,我们发现,左区间所有还没有被合并的数都
比 1 大,所以1与左区间所有元素共产生了 3 个逆序对( **即mid-i+1对** ),统计答案并合并 1 
//step 2:由于 5>2,由上产生了3对逆序对,统计答案并合并 2
//step 3:由于 5<9, 没有逆序对产生,右区间下标 j++
//step 4:由于 6<9, 没有逆序对产生,右区间下标 j++
//step 5:由于 7<9, 没有逆序对产生,右区间下标 j++
//step 6:由于右区间已经结束,正常执行合并左区间剩余,结束

代码实现如下:

/*去掉计算逆序对个数ans的语句,就是标准的归并排序(递归法)*/
#include<stdio.h>
int ans=0;///统计逆序对个数
void merge_array(int a[], int first, int mid, int last, int temp[])//将二个有序数列a[first→mid]和a[mid→last]合并。
{
	int i = first, j = mid + 1;
	int m = mid, n = last;
	int k = 0;

	while (i <= m && j <= n)
	{
		if (a[i] <= a[j]){
			temp[k++] = a[i++];
		}
		else{
			temp[k++] = a[j++];
			ans+=mid-i+1;///计算逆序对个数
		}
	}
	while (i <= m)
		temp[k++] = a[i++];
	while (j <= n)
		temp[k++] = a[j++];
	for (i = 0; i < k; i++)
		a[first + i] = temp[i];
}
void merge_sort(int a[], int first, int last, int temp[])
{
	if (first < last)
	{
		int mid = (first + last) / 2;
		merge_sort(a, first, mid, temp);    //左边有序
		merge_sort(a, mid + 1, last, temp); //右边有序
		merge_array(a, first, mid, last, temp); //再将二个有序数列合并
	}
}
int main()
{
    int a[50002]={},temp[50002]={},n;
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
    }
    merge_sort(a,0,n-1,temp);///数组下标从0开始,所以注意这里传递给函数的last是n-1
    
    for(int i=0;i<n;i++){
        printf("%d ",a[i]); ///输出已排序后的数组
    }
    puts("");
    
    printf("%d\n",ans);///输出逆序对个数
    return 0;
}

#include<cstdio>
#include<iostream>
using namespace std;
int n,a[500010],c[500010];
long long ans;

void msort(int b,int e)//归并排序
{
    if(b==e)  
        return;
    int mid=(b+e)/2,i=b,j=mid+1,k=b;
    msort(b,mid),msort(mid+1,e);
    while(i<=mid&&j<=e)
        if(a[i]<=a[j])
            c[k++]=a[i++];
        else
            c[k++]=a[j++],ans+=mid-i+1;//统计答案
    while(i<=mid)
        c[k++]=a[i++];
    while(j<=e)
        c[k++]=a[j++];
    for(int l=b;l<=e;l++)
        a[l]=c[l];
} 

int main()
{
    scanf("%d",&n); 
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    msort(1,n);
    printf("%lld",ans);
    return 0;
}

非递归法请看二路归并排序算法(递归&非递归)

OJ标程:

// 分治法求逆序数

#include<stdio.h>
#include<string.h>

#define inf 51000

int a[inf],b[inf];

long long int find(int low,int top)
{
	if(low>=top)
		return 0;
	int mid=(low+top)/2,i=low,j=mid+1,k=low;
	long long int sum;
	sum=find(low,mid)+find(mid+1,top);
	while(i<=mid&&j<=top)
	{
		if(a[i]>a[j])
		{
			sum+=mid-i+1;
			b[k++]=a[j++];
		}
		else
			b[k++]=a[i++];
	}
	while(i<=mid)
		b[k++]=a[i++];
	while(j<=top)
		b[k++]=a[j++];
	memcpy(a+low,b+low,sizeof(a[0])*(top-low+1));
	return sum;
}

int main()
{
	int i,n,T;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		for(i=1;i<=n;i++)
			scanf("%d",&a[i]);
		printf("%lld\n",find(1,n));
	}
	return 0;
}

树状数组+离散化 解法

树状数组的做法以及离散化等内容见洛谷题解:P1908 逆序对 题解

用树状数组求解的思路:

先以桶排的思想,开一个大小为这些数最大值的树状数组,并全部置0。从头到尾读入这些数,每读入一个数就在相应位置+1并更新树状数组 ,查看它前面比它小的已出现过的个数sum(查看这一步其实就是对树状数组[1, i]进行区间求和操作),然后用当前位置减去该sum,就可以得到当前数导致的逆序对数了。把所有的加起来就是总的逆序对数。

有时候数会很大,利用桶排思想建树状数组空间不够,比如共有5e5个数,但每个数的的范围是小于等于1e9,这时候就可以对数据离散化,因为求解逆序对我们只关心数的相对大小,而不关心数的具体值。

代码实现如下:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 5e5 + 5;
int n;
int qq[maxn]; //离散化后的数组
struct node {
    int x, id;
    bool operator< (const node &b)const {
        if (x == b.x) return id < b.id;
        else return x < b.x;
    }
}a[maxn];
struct BIT {
    int c[maxn];
    int lowbit(int x) {return x & (-x);}
    void add(int i, int val) {
        while (i <= n) {
            c[i] += val;
            i += lowbit(i);
        }
    }
    ll query(int i) { 
        ll ret = 0;
        while (i > 0) {
            ret += c[i];
            i -= lowbit(i);
        }
        return ret;
    }
}tr;

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i].x), a[i].id = i; 
    /*离散化*/
    sort(a+1, a+n+1);
    for (int i = 1; i <= n; i++)
        qq[a[i].id] = i;
    /*树状数组求逆序对*/   
    ll ans = 0;
    for (int i = 1; i <= n; i++) {
        tr.add(qq[i], 1);
        ans += i - tr.query(qq[i]);
    }
    printf("%lld\n", ans);
    return 0;
}

能用树状数组当然也能用线段树了。以下为线段树做法:

// 线段树
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;

#define MAX 51000
#define MID(a,b) (a+b)>>1
#define R(a) (a<<1|1)
#define L(a) a<<1

typedef struct {
    int num,left,right;
}Node;

typedef struct {
    int num,y;
}Nodes;

Nodes ans[MAX];
Node Tree[MAX<<2];
int n;

bool cmp(Nodes a,Nodes b)
{
    return a.num>b.num?0:1;
}

bool cmp2(Nodes a,Nodes b)
{
    return a.y>b.y?0:1;
}

void Build(int t,int l,int r)
{
    int mid;
    Tree[t].left=l,Tree[t].right=r;
    if(Tree[t].left==Tree[t].right)
    {
        Tree[t].num=0;
        return ;
    }
    mid=MID(Tree[t].left,Tree[t].right);
    Build(L(t),l,mid);
    Build(R(t),mid+1,r);
}

void Insert(int t,int l,int r,int x)
{
    int mid;
    if(Tree[t].left==l&&Tree[t].right==r)
    {
        Tree[t].num+=x;
        return ;
    }
    mid=MID(Tree[t].left,Tree[t].right);
    if(l>mid)
    {
        Insert(R(t),l,r,x);
    }
    else if(r<=mid)
    {
        Insert(L(t),l,r,x);
    }
    else
    {
        Insert(L(t),l,mid,x);
        Insert(R(t),mid+1,r,x);
    }
    Tree[t].num=Tree[L(t)].num+Tree[R(t)].num;
}

int Query(int t,int l,int r)
{
    int mid;
    if(Tree[t].left==l&&Tree[t].right==r)
        return Tree[t].num;
    mid=MID(Tree[t].left,Tree[t].right);
    if(l>mid)
    {
        return Query(R(t),l,r);
    }
    else if(r<=mid)
    {
        return Query(L(t),l,r);
    }
    else
    {
        return Query(L(t),l,mid)+Query(R(t),mid+1,r);
    }
}
int main()
{
    int a,n,i,t;
    scanf("%d",&t);
    long long int k;
    while(t--)
    {
        scanf("%d",&n);
        memset(Tree,0,sizeof(Tree));
        Build(1,1,n);
        for(i=1;i<=n;i++)
        {
            scanf("%d",&ans[i].num);
            ans[i].y=i;
        }
        sort(ans+1,ans+1+n,cmp);
        for(i=1;i<=n;i++)
            ans[i].num=i;
        sort(ans+1,ans+1+n,cmp2);
        for(i=1,k=0;i<=n;i++)
        {
            a=ans[i].num;
            Insert(1,a,a,1);
            k=k+(i-Query(1,1,a));
        }
        printf("%lld\n",k);
    }
    return 0;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java归并排序对是一种常见的排序算法,它通过将数组分成两个子数组,分别对子数组进行排序,然后将两个已排序的子数组合并成一个有数组。在归并的过程中,可以统计对的数量。 下面是Java归并排序对的实现步骤: 1. 首先,定义一个全局变量count,用于记录对的数量。 2. 创建一个辅助数组temp,用于存储归并过程中的临时结果。 3. 编写一个merge方法,用于将两个已排序的子数组合并成一个有数组,并在合并过程中统计对的数量。 - 在merge方法中,首先定义三个指针:left、right和index,分别指向左子数组、右子数组和辅助数组temp的起始位置。 - 然后,比较left指针和right指针所指向的元素大小: - 如果左子数组的元素小于等于右子数组的元素,则将左子数组的元素复制到temp数组,并将left指针向右移动一位。 - 如果左子数组的元素大于右子数组的元素,则将右子数组的元素复制到temp数组,并将right指针向右移动一位。同时,count需要加上left指针到左子数组末尾的长度,因为左子数组剩余的元素都大于当前的右子数组元素。 - 继续上述比较和复制的过程,直到左子数组或右子数组的元素全部复制到temp数组。 - 最后,将temp数组中的元素复制回原数组的相应位置。 4. 编写一个mergeSort方法,用于递归地对数组进行归并排序。 - 在mergeSort方法中,首先判断数组的长度是否大于1,如果不大于1,则直接返回。 - 如果数组的长度大于1,则将数组分成两个子数组,并分别调用mergeSort方法对子数组进行排序。 - 最后,调用merge方法将两个已排序的子数组合并成一个有数组,并统计对的数量。 5. 在主函数中调用mergeSort方法对数组进行排序,并输出对的数量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值