前言
虽然在考场中,不到万不得已,各位应该都会使用快排吧,而非花大量时间特意写个大函数作其他的排序,且一般情况下快排的时间复杂度都是。在今天的对cdq分治的学习中,其核心便是归并排序(还有树状数组优化),其功能强大且时间复杂度也是稳定且高效的去到
!今天析一下这归并排序。
算法
何为归并排序?其实是分治的思想,结构类似树:将数列不断划分为若干“子节点”比较,方法有三步:
1.划分数列:把序列分成元素尽量相等的序列
2.子序列排序:对相邻两个子序列分别排序
3.子序列合并:合并相邻两个子序列并回溯
如此下去,就会得到排好序的序列。
前两步则是普通的递归和比较,那么第三步怎么办?此时就需要引入辅助数组,并引入两个指针
和
分别遍历两个序列(
和
),将
和
进行比较——如果
则将
压入
,否则将
压入
,这样子就能得到两个子序列合并后的有序状态!最后把
中数据挪入对应下标的
中。代码如下:
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.我们可以用归并排序求逆序对。例如给定一个序列,让你求这个序列拥有逆序对的对数。如果用暴力算法时间复杂度要去到
,在
大的情况下不优。
这个时候就可以使用“归并排序”的思想了,这其实是前文提到的cdq分治算法的一类。
在归并排序判断的过程中,总是,所以当
时便产生了逆序对,并且不难发现,
都可以和
组成逆序对,所以直接可以统计逆序对数量
,并且只需要稍微改动判断语句即可!代码如下:
......
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刚学习物理的匀速直线运动,所以他来到机房做一道关于匀速直线运动的信息学题目,在一条无限长的直线上有个点,每个点有如下属性:
(1)第i个点在数轴上的位置是
(2)每个点有一个初始的速度
(3)每个点有一个运动的方向,当为正数时,代表点i从初始位置向右进行匀速直线运动,反之则向左进行匀速直线运动。如
秒后,
点所在的位置是
令表示i点和j点在任意时刻可能存在的距离的最小值。
现在请帮小z求出所有的的总和(其中
)
第一行一个整数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
这个问题其实和刚刚的逆序对问题很像:对于两点和
,若要使得其产生最小距离
,只有
时才能产生,否则就用一个前缀和维护距离。这里和归并排序代码相似,只需要更改判断条件。代码如下:
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
}
最后将相邻两个子序列的右半边的答案也统计一遍,然后用辅助数组更新原数组
就好啦。全代码贴贴:
#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)