更强大的排序——归并排序

前言

虽然在考场中,不到万不得已,各位应该都会使用快排吧,而非花大量时间特意写个大函数作其他的排序,且一般情况下快排的时间复杂度都是\Theta (nlog_2{n})。在今天的对cdq分治的学习中,其核心便是归并排序(还有树状数组优化),其功能强大且时间复杂度也是稳定且高效的去到\Theta (nlog_2{n})!今天析一下这归并排序。

算法

何为归并排序?其实是分治的思想,结构类似树:将数列不断划分为若干“子节点”比较,方法有三步:

1.划分数列:把序列分成元素尽量相等的序列

2.子序列排序:对相邻两个子序列分别排序

 3.子序列合并:合并相邻两个子序列并回溯

如此下去,就会得到排好序的序列。

前两步则是普通的递归和比较,那么第三步怎么办?此时就需要引入辅助数组b,并引入两个指针pq分别遍历两个序列(a_{l\rightarrow mid}a_{mid+1\rightarrow r}),将a_pa_q进行比较——如果a_p\leq a_q则将a_p压入b,否则将a_q压入b,这样子就能得到两个子序列合并后的有序状态!最后把b中数据挪入对应下标的a中。代码如下:

void Ms(ll l,ll r)
{
    if(l<r)
    {
        ll mid=(l+r)>>1;
        Ms(l,mid);
        Ms(mid+1,r);
        ll i=l,p=l,q=mid+1;//i:辅助数组b的下标
        while(p<=mid||q<=r)
        {
            if(q>r||(p<=mid&&a[p]<=a[q]))b[i++]=a[p++];
            else b[i++]=a[q++];
        }
        for(i=l;i<=r;i++)
        a[i]=b[i];
    }
    return;
}

用法 

1.我们可以用归并排序求逆序对。例如给定一个序列a,让你求这个序列拥有逆序对的对数。如果用暴力算法时间复杂度要去到\Theta (n^2),在|a|大的情况下不优。

这个时候就可以使用“归并排序”的思想了,这其实是前文提到的cdq分治算法的一类

在归并排序判断的过程中,总是p<q,所以当a_p>a_q时便产生了逆序对,并且不难发现,a_{p+1\rightarrow mid}都可以和a_q组成逆序对,所以直接可以统计逆序对数量C_q=mid-p+1,并且只需要稍微改动判断语句即可!代码如下:

......
if(q>r||(p<=mid&&a[p]<=a[q]))b[i++]=a[p++];
else
{
    b[i++]=a[q++];
    cnt+=mid-p+1;
}
......

 全代码贴贴:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll z=1e6+9;
ll n,a[z],b[z],cnt;
void fastread()
{
	ios::sync_with_stdio(0);
}
void Ms(ll l,ll r)
{
    if(l<r)
    {
        ll mid=(l+r)>>1;
        Ms(l,mid);
        Ms(mid+1,r);
        ll i=l;
        ll p=l,q=mid+1;
        while(p<=mid||q<=r)
        {
            if(q>r||(p<=mid&&a[p]<=a[q]))b[i++]=a[p++];
            else
            {
                b[i++]=a[q++];
                cnt+=mid-p+1;
            }
        }
        for(i=l;i<=r;i++)
        a[i]=b[i];
    }
    return;
}
int main()
{
	fastread();
    cin>>n;
	for(int i=1;i<=n;i++)
    cin>>a[i];
    Ms(1,n);
//	for(int i=1;i<=n;i++)
//	cout<<a[i]<<" ";
//	cout<<endl;
	cout<<cnt;
    return 0;
}

详见题目:P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

2.将归并排序延伸,即可得到cdq分治算法(据说这是以曾经的IOI选手陈丹琦命名的)。

cdq分治是分治算法的一种,主要用于求偏序问题。通过对一维排序进而对下一维度进行分治,就形成了一套处理二维偏序问题三维偏序问题的处理方法。不同于普通分治,后者是将原问题划分为若干子问题,每个子问题相互独立且与原问题形式相同,递归求解子问题,最后合并子问题的解得到原问题的解;而前者,对于每一次划分出来的两个子问题,前一个子问题可以用来解决后一个子问题,而并非自身。不过,cdq分治算法是离线的:题目里修改操作对询问的应贡献独立,修改操作需互不影响,并且题目允许使用离线算法。

cdq分治算法,在归并排序的框架上,可用树状数组加以维护。来看一道题:

高一小z刚学习物理的匀速直线运动,所以他来到机房做一道关于匀速直线运动的信息学题目,在一条无限长的直线上有n个点,每个点有如下属性:

(1)第i个点在数轴上的位置是x_i

(2)每个点有一个初始的速度v_i

(3)每个点有一个运动的方向,当v_i为正数时,代表点i从初始位置向右进行匀速直线运动,反之则向左进行匀速直线运动。如t秒后,i点所在的位置是x_i+v_it

f_{i,j}表示i点和j点在任意时刻可能存在的距离的最小值。

现在请帮小z求出所有的f_{i,j}的总和(其中1<=i<j<=n

第一行一个整数n,表示数轴上有n个点(2<=n<=2e5)

第二行n个整数,x(1),x(2),x(3),...x(n-1),x(n)

第三行n个整数,v(1),v(2),v(3),...v(n-1),v(n)

1<=x(i)<=1e8   -1e8<=v(i)<=1e8

这个问题其实和刚刚的逆序对问题很像:对于两点pq,若要使得其产生最小距离f_{p,q},只有v_p>v_q时才能产生,否则就用一个前缀和维护距离。这里和归并排序代码相似,只需要更改判断条件。代码如下:

if(a[p].v<=a[q].v)//和前面的模版很像
{
    b[i++]=a[p];//更新辅助数组
    xb+=a[p++].x;//前缀和维护,并让指针p右移寻找a[p].v>a[q].v
}
else 
{
    b[i++]=a[q];
    cnt+=(p-l)*a[q++].x-xb;//计算f(p,q)并加入答案cnt,右移指针q
}

最后将相邻两个子序列的右半边的答案也统计一遍,然后用辅助数组b更新原数组a就好啦。全代码贴贴:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll z=1e6+9;
ll n,cnt;
void fastread()
{
	ios::sync_with_stdio(0);
}
struct node
{
	ll x,v;
}a[z],b[z];
bool cmp(node x,node y)
{
	if(x.x==y.x)return x.v<y.v;
	return x.x<y.x;
}
void cdq(ll l,ll r)
{
    if(l<r)
    {
        ll mid=(l+r)/2;
        cdq(l,mid);
        cdq(mid+1,r);
        ll i=l;
        ll p=l,q=mid+1;
        ll xb=0;
        while(p<=mid&&q<=r)
        {
        	if(a[p].v<=a[q].v)
        	{
				b[i++]=a[p];
        		xb+=a[p++].x;
			}
        	else 
        	{
        		b[i++]=a[q];
        		cnt+=(p-l)*a[q++].x-xb;
			}
        }
        while(p<=mid)b[i++]=a[p++];
        while(q<=r)b[i++]=a[q],cnt+=(p-l)*a[q++].x-xb;
        for(int o=l;o<=r;o++)
        a[o]=b[o];
    }
    return;
}
int main()
{
	fastread();
    cin>>n;
	for(int i=1;i<=n;i++)
    cin>>a[i].x;
    for(int i=1;i<=n;i++)
    cin>>a[i].v;
    sort(a+1,a+n+1,cmp);
    cdq(1,n);
	cout<<cnt;
    return 0;
}

而三维偏序就是再来一维处理,可以使用树状数组维护。可以看这道经典例题:P3810 【模板】三维偏序(陌上花开) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
MPI(Message Passing Interface)是一种用于并行计算的编程模型和库。归并排序是一种经典的排序算法,适合并行计算。 在MPI中,可以通过发送和接收消息来实现进程间的通信。下面是一个基于MPI的归并排序的伪代码: ```python def parallel_merge_sort(data): # 获取进程总数和当前进程编号 size = MPI.COMM_WORLD.Get_size() rank = MPI.COMM_WORLD.Get_rank() # 计算每个进程要处理的数据量 chunk_size = len(data) // size remainder = len(data) % size # 将数据分发到各个进程 if rank == 0: for i in range(size): if i < remainder: chunk = data[i * (chunk_size + 1):(i + 1) * (chunk_size + 1)] else: chunk = data[remainder + i * chunk_size:remainder + (i + 1) * chunk_size] MPI.COMM_WORLD.send(chunk, dest=i, tag=0) # 接收数据 chunk = MPI.COMM_WORLD.recv(source=0, tag=0) # 对本地数据进行排序 chunk.sort() # 归并排序 for step in range(size): # 计算要交换数据的进程编号 partner = (rank + step) % size # 发送和接收数据 sendbuf = chunk recvbuf = MPI.COMM_WORLD.recv(source=partner, tag=step) if rank < partner: sendtag = step recvtag = step + size else: sendtag = step + size recvtag = step MPI.COMM_WORLD.send(sendbuf, dest=partner, tag=sendtag) chunk = merge(chunk, recvbuf) # 将排序好的数据返回 if rank == 0: result = [] for i in range(size): chunk = MPI.COMM_WORLD.recv(source=i, tag=size) result.extend(chunk) return result else: MPI.COMM_WORLD.send(chunk, dest=0, tag=size) ``` 在这个算法中,首先将原始数据分发到各个进程,然后每个进程对本地数据进行排序,接着对每个步骤进行归并排序,并且使用MPI的send和recv函数进行交换数据。最后将排序好的数据返回到主进程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值