归并排序之神奇的逆序对

文章包含四部分

  1. 归并排序的实现(略讲)

  2. 归并排序求逆序对

  3. 归并排序例题详讲

  4. 树状数组和归并排序求逆序对的区别


  • 关于归并排序的实现

归并排序主要分为两大步:
1.分解
2.合并

关于归并排序的具体原理请自行百度.
这里只略讲一下归并排序的代码实现方式

  • 分解

我们采用二分递归的方式处理

  • 合并

我们采用三个指针来实现

板子题:快速排序

Code:

#include<iostream>
#include<cstdio>

using namespace std;

const int N=100000+5;

int n;
int a[N];//需要排序的数组 
int r[N];//用来辅助排序的数组 

void msort(int s,int t)
{
    if(s==t) return;//如果只有一个数就返回 
    int mid=(s+t)/2;
    //递归分解 
    msort(s,mid);//分解左边 
    msort(mid+1,t);//分解右边 
    int i=s,j=mid+1,k=s;
    //合并左右序列 
    while(i<=mid && j<=t) //正常排序合并 
    { 
        if(a[i]<=a[j]) {r[k]=a[i];k++;i++;}
        else {r[k]=a[j];k++;j++;}
    }
    while(i<=mid){r[k]=a[i];k++;i++;}//复制排序后左边子序列剩余 
        
    while(j<=t){r[k]=a[j];k++;j++;}//复制排序后右边子序列剩余
        
    for(int i=s;i<=t;i++)
        a[i]=r[i];//更新a数组 
}

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

测试结果:


第一个是 S T L STL STL里的 s o r t sort sort
第二个是开了 O 2 O2 O2 s o r t sort sort
第三个是手写的归并排序

n o i p noip noip不能开 O 2 O2 O2

Q w Q QwQ QwQ


  • 关于归并排序求逆序对

模板题

关于逆序的定义:大的数排在小的数前面

当然你也可以用冒泡排序

实现方法:

每次合并的时候,记录有多少次交换

假定我们要将一串无序数字排成升序(从小到大)
根据归并排序的原理,每次我们进行合并操作的时候,左边子序列中小的数都会被合并进辅助数组。
这时如果左边子序列有数剩余,则说明这些数都比右边子序列的数要大,那么我们可以根据这一点来计算逆序对的个数

核心Code:

while(i<=mid && j<=t)
    {
        if(a[i]<=a[j]) {r[k]=a[i];k++;i++;}
        else {r[k]=a[j];k++;j++;ans+=mid-i+1;}//核心操作
		//mid-i+1即左边子序列剩余元素的个数 
    }

其余部分没有什么差别

模板题Code:

#include<iostream>
#include<cstdio>

using namespace std;

const int N=5e5+5;

long long n,ans;
int a[N];
int r[N];

void msort(int s,int t)
{
    if(s==t) return;
    int mid=(s+t)/2;
    msort(s,mid);
    msort(mid+1,t);
    int i=s,j=mid+1,k=s;
    while(i<=mid && j<=t)
    {
        if(a[i]<=a[j]) {r[k]=a[i];k++;i++;}
        else {r[k]=a[j];k++;j++;ans+=mid-i+1;}//核心操作
		//mid-i+1即左边子序列剩余元素的个数 
    }
    while(i<=mid){r[k]=a[i];k++;i++;}
        
    while(j<=t){r[k]=a[j];k++;j++;}
        
    for(int i=s;i<=t;i++)
        a[i]=r[i];
}

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


  • 归并排序例题详讲

最接近神的人

关键词:

交换序列中相邻的两个元素
用最少的交换次数使原序列变成不下降序列

显然在求逆序对的个数 Q w Q QwQ QwQ

套上板子即可


瑞士轮

这里只用到了归并排序中的合并操作

分析:

每组比赛的胜者:赛前,总分是按降序排的;获胜后都得1分,仍是降序;

每组比赛的负者:赛前,总分是按降序排的;不得分,仍是降序。

先按初始分数排序,然后按分数高低两人一组比赛;

胜者入队 A A A,负者入队 B B B。这样 A A A B B B自身仍是有序的;

只需进行合并操作即可,合并操作的复杂度是 O ( n ) O(n) O(n),而如果用快排其复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

此处引用List大佬的博客

Code:

#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;

const int N=1e5+5;

int n,r,q;
struct fengzi
{
    int id,sorce,power;
}m[2*N],x[2*N],y[2*N];
//m是选手,x是胜利者,y是失败者 
bool cmp(fengzi a,fengzi b)
{
    if(a.sorce>b.sorce) return 1;//依题意从高到低排序 
    if(a.sorce==b.sorce && a.id<b.id) return 1;//处理特殊情况 
    return 0;
}

//合并操作
void merge()
{
    int i=1,j=1,cnt=0;
    while(cnt<=2*n)
    {
        if(i>n)
        {
            while(j<=n){m[++cnt]=y[j];j++;}
            break;
        }
        if(j>n)
        {
            while(i<=n){m[++cnt]=y[i];i++;} 
            break;
        }
        if(x[i].sorce>y[j].sorce || (x[i].sorce==y[j].sorce && x[i].id<y[j].id))//依题意更新排名 
            m[++cnt]=x[i++];
        else m[++cnt]=y[j++];
    }
 } 

int main()
{
    scanf("%d%d%d",&n,&r,&q);
    for(int i=1;i<=2*n;i++)
    {
        scanf("%d",&m[i].sorce);//得分 
        m[i].id=i;//编号 
    }
    for(int i=1;i<=2*n;i++)
        scanf("%d",&m[i].power);//实力值 
    sort(m+1,m+1+2*n,cmp);//排序 
    for(int qwq=1;qwq<=r;qwq++)//枚举每场比赛 
    {
        int i=0,j=0;
        for(int ppp=1;ppp<=n;ppp++)//枚举每组选手 
        {
            int pos=2*ppp;
            if(m[pos-1].power>m[pos].power)//如果该组中的第一个选手比第二个选手强 
            {
                m[pos-1].sorce++;//第一个选手的分数+1 
                x[++i]=m[pos-1];//第一个选手分到胜利者的队伍 
                y[++j]=m[pos];//第二个选手分到失败者的队伍 
            }
            else
            {
                m[pos].sorce++;
                x[++i]=m[pos];
                y[++j]=m[pos-1];
            }
        }
        merge();//合并胜利者和失败者,更新  新的排名 
    }
    printf("%d",m[q].id);
    return 0;
}

火柴排队

分析:

由数学知识可知,要使$ \sum (a_i-b_i)^2 最 小 我 们 就 要 使 每 一 个 最小 我们就要使每一个 使a_i-b_i$最小

也就是说, a a a中第 k k k小的数要和 b b b中第 k k k小的数排在一个位置

依据这个思想,我们可以新建一个数组 x x x
这个数组的下标是 a a a数组中第 k k k小的数在原序列中的编号
这个数组中存的是 b b b数组中第 k k k小的数在原序列中的编号

例如:

a a a的原始序列为 5 , 3 , 1 , 2 , 6 5,3,1,2,6 5,3,1,2,6
b b b的原始序列为 7 , 2 , 9 , 3 , 1 7,2,9,3,1 7,2,9,3,1
把两个序列从小到大排序后,
a a a序列为 1 , 2 , 3 , 5 , 6 1,2,3,5,6 1,2,3,5,6
b b b序列为 1 , 2 , 3 , 7 , 9 1,2,3,7,9 1,2,3,7,9
我们假定此时的 k k k为3
a a a序列中第 3 ( k ) 3(k) 3(k)小的数的原始编号就是 2 2 2
b b b序列中第 3 ( k ) 3(k) 3(k)小的数的原始编号就是 4 4 4

x [ 3 ] = 4 x[3]=4 x[3]=4
显然,这个数组的特性就是,如果 x [ i ] = i x[i]=i x[i]=i那么此时, a a a序列中第k小的数就和 b b b序列中的第 k k k小的数,一一对应了,即原始序列时,就有 a i − b i a_i-b_i aibi最小。
然后,我们的目标是,将 x x x数组升序排列,求出交换的次数,这不就转换成求逆序对了…

Code:

#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;

const int mod=99999997;
const int N=100000+5;

int n,ans,cnt;

struct team
{
    int id,hight;//id是原始编号,hight是高度 
}a[N],b[N];
int x[N],y[N];
//x待排序的数组
//y是辅助数组 

bool cmp(team a,team b)
{
    return a.hight<b.hight;//先按高度排序 
}

void merge(int l,int r)
{
    if(l==r) return;
    int mid=(l+r)/2;
    merge(l,mid);
    merge(mid+1,r);
    int i=l,j=mid+1,k=l;
    while(i<=mid && j<=r)
    {
        if(x[i]>x[j])
        {
            y[k++]=x[j++];
            ans+=mid-i+1;
            ans%=mod;
        }
        else
            y[k++]=x[i++];
    }
    while(i<=mid) y[k++]=x[i++];
    while(j<=r) y[k++]=x[j++];
    for(int i=l;i<=r;i++)
        x[i]=y[i];
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i].hight);
        a[i].id=i;
    }
        
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&b[i].hight);
        b[i].id=i;
    }
    sort(a+1,a+1+n,cmp);
    sort(b+1,b+1+n,cmp);
    for(int i=1;i<=n;i++) 
        x[a[i].id]=b[i].id;//初始化x数组 
    merge(1,n);//求逆序对 
    printf("%d",ans);
    
    return 0;
}

下面是来自 P O J POJ POJ的水题

板子题.1–poj2299

就不放代码啦,给大家一个练手的机会,记得开 l o n g l o n g long long longlong

板子题.2–poj1804

P O J POJ POJ板子真多,题面还都不一样…,记得输出的时候要换两次行


M × N Puzzle–poj2893

这题就有点难度啦

首先我们把它给出的 M × N M\times N M×N个数转为一串数字,存进一个数组中,称为原始状态
把最终有序的状态也转为一串数字也存进一个数组,称为最终状态,

分析:

八数码问题的有解无解的结论:

一个状态表示成一维的形式,求出除0之外所有数字的逆序数之和,也就是每个数字前面比它大的数字的个数的和,称为这个状态的逆序。

若两个状态的逆序奇偶性相同,则可相互到达,否则不可相互到达。
  1. 我们可知, 0 0 0在左右移动的时候,是不会改变原来状态的顺序的.即,不会给原来状态增加或减少逆序对的个数

比如:

原始状态: 1 , 3 , 0 , 2 1,3,0,2 1,3,0,2
0 0 0往左移动一格,变为 1 , 0 , 3 , 2 1,0,3,2 1,0,3,2
逆序对的个数是没有发生改变的

注意我们算逆序对的个数时,不把 0 0 0考虑进去
  1. 0 0 0上下移动的时候,

  • 若列数为奇数,上下移动不会是逆序对的奇偶性发生改变,这个自己画画图就能理解了.
  • 若列数为偶数,我们就要考虑 0 0 0移动的距离了, 0 0 0上下移动一格,就会增加或减少奇数个逆序对

现在考虑 M × N M\times N M×N的棋盘

首先算出逆序对的个数,在处理出 0 0 0要移动的距离,然后让看看两状态奇偶性是否一致即可。

Code:

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

int n,m,cnt,ans,step;
int a[1000*1000];
int b[1000*1000];

//求逆序对的个数 
void merge(int l,int r)
{
	if(l==r) return;
	int mid=(l+r)/2;
	merge(l,mid);
	merge(mid+1,r);
	int i=l,j=mid+1,k=l;
	while(i<=mid && j<=r)
	{
		if(a[i]>a[j])
		{
			b[k++]=a[j++];
			ans+=mid-i+1;
		}	
		else b[k++]=a[i++];
	}
	while(i<=mid) b[k++]=a[i++];
	while(j<=r) b[k++]=a[j++];
	for(int i=l;i<=r;i++)
		a[i]=b[i];
}

int main()
{
	while(scanf("%d%d",&n,&m))
	{ 
		if(n==0) break;
		memset(a,0,sizeof(a));
//		memset(b,0,sizeof(b));这个不要初始化,会T掉 
		ans=cnt=step=0;
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				int x;
				scanf("%d",&x);
				if(x!=0) a[++cnt]=x;
				else step=n-i;//计算0的移动距离 
			}
		}
				
		merge(1,cnt);
//		cout<<ans<<endl;
		if(m&1)
		{
			if(ans%2==0) puts("YES");//大写!!! 
			else puts("NO");
		}
		else
		{
			if(ans%2==step%2) puts("YES");
			else puts("NO");
		} 
	} 
	
	return 0;
}

  • 树状数组和归并排序求逆序对的区别

emmmm

树状数组:在求逆序对的时候还可以执行一些其它的操作

归并排序:强制离线

如果只是单单求逆序对的话,建议使用归并排序.

By Yfengzi

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值