浅谈排序网络与并行排序算法

对于普通的基于比较排序我们拥有一个复杂度下界 \(O(n\log n)\),然而如果我们允许并行计算的话,将得到一些复杂度更优秀的计算方法。

听到并行这个词许多人就会认为你有几个线程复杂度就除以几,所以线程堆得越多越好。但许多的算法问题都必须要满足你必须要算完 A 才能去计算 B,比如对于普通的前缀和算法,每一个位置的结果都依赖上一个位置,如果直接并行将不会对复杂度产生优化。所以一但允许并行我们还要重新去设计一些算法,此时我们更少地去关心计算网络的大小而是更多地去关心它的深度,一些原本不太优秀的算法在并行计算这方面表现得更加优秀。

OI 甚至 CP 中考察并行算法的题目并不多见,比如说这道考察了计算并行环三染色问题。

研究并行排序时,我们也同时会研究排序网络。排序网络问题与并行排序问题十分有关联性,因为许多排序网络更浅的算法更加易于并行。排序网络指得是不依赖排序数组具体的取值,只允许你使用比较器构造出一个操作序列,满足任意一个序列经过这些操作后都变得有序。比较器的操作定义为如果 \(a_x<a_y\) 则交换 \(a_x,a_y\) 的取值。一个排序网络的深度指的是如果相邻的若干次操作如果互不相干就可以划分到同一层那么它的最小层数,这直接影响了该排序网络的并行复杂度。冒泡排序算法可以直接建出一个大小为 \(O(n^2)\) 深度 \(O(n^2)\) 的网络,如果修改成对奇偶位置分别冒泡则可以做到深度为 \(O(n)\) 级别。

而下面的并行排序算法都是时间复杂度 \(O(n\log^2 n)\),可以建出深度为 \(O(\log^2 n)\) 的排序网络。这些算法都是分治算法,为了方便起见以下默认将序列长度扩充成 2 的整幂

双调排序算法

定义一个序列是双调的,当且仅当它能够分成一个非严格单调递增的前缀和非严格单调递减的后缀(即“单峰”),或者说序列经过循环移位后满足前面的条件。

双调排序的基本思路是将原序列排序成双调序列,然后将双调序列再排序成单调序列。

对于如何排序成双调序列,思路十分暴力:将原序列折成两半,前面一半递归用双调排序将排成递增序列,后面一半也双调排成递减序列。

而双调排序的骄傲之处在于,对于一个双调序列,我们可以只使用 \(O(\log n)\) 层比较器将其归并成一个单调序列。

有这么一个 Batcher 定理,它指出了如果将双调序列劈成两半,对应位置(即 \(i\)\(i+\frac{n}{2}\))使用一次比较器后,原序列将对半分成两个双调序列,而且两边的值域不相交。那么我们只需要直接往两边递归下去就可以得到一个单调序列了。

我们考虑用更 OI 的证明方式来说明 Batcher 定理的正确性:将原序列的值域劈成两半变成 01 序列,容易发现如果对于每一种 01 序列 Batcher 定理都成立那么自然它也能正确地对任意值域的数列排序。

一个 01 序列如果是双调的实际上在说将这个序列首尾相接拼成一个环,那么 1 的位置正好是一个区间。

接下来我们只用简单的分类讨论便可以证明 Batcher 定理:考虑对半分然后对应位置比较器这个操作,实际上相当于找一个位置把这个环砍成相等的两半,如果砍出来的一半有全 0/全 1 那么 Batcher 定理直接得证。否则这一刀一边砍在了 0 的区间,一边砍在了 1 的区间里。此时两半都是前缀 0 后缀 1 或者后缀 1 前缀 0。运行比较器之后自然最多变成一个前缀和后缀是 1 或者一个区间是 1。于是 Batcher 定理就这样成立了!

奇偶归并算法

观察上面的双调排序算法,关键的 Batcher 定理本质上在于对于两个单调序列合并,有人会想:直接归并不就是 \(O(n)\) 的吗,然而裸的归并算法并不能排序网络化。

于是我们也可以考虑改造归并算法。我们可以这样做:对于长度大于 \(2^1\) 的序列我们将这个序列按照奇偶性分成两个序列,分别运行奇偶归并算法,然后再依次添加比较器 \((2,3),(4,5),(6,7)\dots\) 就可以完成归并了。

为什么呢?依旧考虑对于 01 序列进行证明。按照奇偶性分成两个序列分别归并之后,序列肯定会变成前 0 后 1 的形态,只跟 0 的个数有关。而根据我们的选取方式,前半和后半部分奇数位置都有可能比偶数位置多选一个 0。所以奇数位置最多比偶数位置多选两个 0。这样序列的形态就只会变成 \(\dots 000001011111\dots\) 这种形式,于是我们只需要消灭所有偶数位置与偶数位置加一处的 10,只需要加 \(\frac{n}{2}-1\) 个比较器就可以了。

#include <cstdio>
#include <algorithm>
#include <functional>
using namespace std;
int read(){/*...*/}
const int INF=0x3f3f3f3f;
int a[1<<20];
void ConditionalSwap(int x,int y,bool fl=0){
	if(fl) swap(x,y);
	if(a[x]>a[y]) swap(a[x],a[y]);
}
void BitonicChange(int l,int r,bool fl){ //bitonic->monotonic
	if(l+1==r) return;
	int p=(r-l)>>1;
	for(int i=l;i<l+p;++i)
		ConditionalSwap(i,i+p,fl);
	BitonicChange(l,l+p,fl);
	BitonicChange(r-p,r,fl);
}
void BitonicSort(int l,int r,bool fl=0){ //random->bitonic->monotonic
	if(l+1==r) return;
	int p=(r-l)>>1;
	BitonicSort(l,l+p,fl^1);
	BitonicSort(r-p,r,fl);
	BitonicChange(l,r,fl);
}
void OddEvenMerge(int l,int r,int t){
	if(l+t==r) return;
	if(l+t+t==r) return ConditionalSwap(l,r-t);
	OddEvenMerge(l,r,t<<1);
	OddEvenMerge(l+t,r+t,t<<1);
	for(int i=l+t;i<r-t;i+=(t<<1)) ConditionalSwap(i,i+t);
}
void OddEvenSort(int l,int r){
	if(l+1==r) return;
	int p=(r-l)>>1;
	OddEvenSort(l,l+p);
	OddEvenSort(r-p,r);
	OddEvenMerge(l,r,1);
}
int main(){
	int len=read();
	for(int i=0;i<len;++i) a[i]=read();
	int n=1;
	while(n<len) n<<=1;
	for(int i=len;i<n;++i) a[i]=INF;
	OddEvenSort(0,n);
	// Or BitonicSort(0,n);
	for(int i=0;i<len;++i) printf("%d ",a[i]);
	putchar('\n');
	return 0;
}
posted @ 2023-10-24 16:43  yyyyxh  阅读(291)  评论(0编辑  收藏  举报