【题解】AcWing 107.超快速排序

AcWing 107.超快速排序

题目描述

在这个问题中,您必须分析特定的排序算法----超快速排序。

该算法通过交换两个相邻的序列元素来处理 n n n 个不同整数的序列,直到序列按升序排序。

对于输入序列 9 1 0 5 4,超快速排序生成输出 0 1 4 5 9

您的任务是确定超快速排序需要执行多少交换操作才能对给定的输入序列进行排序。

输入格式

输入包括一些测试用例。

每个测试用例的第一行输入整数 n n n,代表该用例中输入序列的长度。

接下来 n n n 行每行输入一个整数 a i a_i ai,代表用例中输入序列的具体数据,第 i i i 行的数据代表序列中第 i i i 个数。

当输入用例中包含的输入序列长度为 0 0 0 时,输入终止,该序列无需处理。

输出格式

对于每个需要处理的输入序列,输出一个整数 o p op op,代表对给定输入序列进行排序所需的最小交换操作数,每个整数占一行。

数据范围

0 ≤ n < 500000 0≤n<500000 0n<500000
一个测试点中,所有 n n n 的和不超过 500000 500000 500000
0 ≤ a i ≤ 999999999 0≤a_i≤999999999 0ai999999999

输入样例

5
9
1
0
5
4
3
1
2
3
0

输出样例

6
0

题目分析

不难发现上述的“超快速排序”其实就是冒泡排序,所求的即是序列中逆序对的个数。

对于逆序对的个数,我们可以通过归并排序求解。回想归并排序的原理:对左右两半递归排序,有序后将两半进行合并。因此我们只需要在合并时考虑“左边一半里一个较大的数”与“右边一半里一个较小的数”构成逆序对的情形。

合并两个有序序列 a [ l ∼ m i d ] a[l\sim mid] a[lmid] a [ m i d + 1 ∼ r ] a[mid+1\sim r] a[mid+1r] 可以采用两个指针分别扫描的方式,不断比较两个指针所指 a [ i ] a[i] a[i] a [ j ] a[j] a[j] 的大小,将小的那个结果加入到排序的结果数组中。如果 a [ j ] a[j] a[j] 更小,那么说明 a [ i ∼ m i d ] a[i\sim mid] a[imid] 都比它大,都可以与它构成逆序对,因此答案加上 m i d − i + 1 mid-i+1 midi+1

代码 (归并排序):

#include <iostream>
using namespace std;
const int N = 500050;
long long cnt;
int a[N], t[N], n;

void merge_sort(int l, int r){
    
    
    if (l >= r) return;
    int mid = l + r >> 1;
    merge_sort(l, mid), merge_sort(mid + 1, r);
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r){
    
    
        if (a[i] <= a[j]) t[k ++] = a[i ++];
        else t[k ++] = a[j ++], cnt += mid - i + 1;
    }
    while (i <= mid) t[k ++] = a[i ++];
    while (j <= r) t[k ++] = a[j ++];
    for (int x = l; x <= r; x ++)
        a[x] = t[x];
}

int main(){
    
    
    while (cin >> n && n){
    
    
        cnt = 0;
        for (int i = 1; i <= n; i ++) cin >> a[i];
        merge_sort(1, n);
        cout << cnt << endl;
    }
    return 0;
}

当然,我们还可以通过权值树状数组求逆序对。对于给定序列 a a a,建立 t t t 数组,其中 t [ n u m ] t[num] t[num] 表示 n u m num num 在序列 a a a 中出现的次数,那么 t t t [ l , r ] [l,r] [l,r] 上的区间和就对应 a a a [ l , r ] [l,r] [l,r] 上的数有多少个。

利用上述思想,我们可以利用树状数组来维护 t t t 数组的前缀和,从而高效地求出逆序对个数,具体步骤如下:倒叙枚举序列中每一个元素 a [ i ] a[i] a[i],计算 t t t 数组前缀和 res += query(a[i] - 1),再执行单点修改,把位置 a [ i ] a[i] a[i] 上的数加 1 1 1 add(a[i], 1)

在此过程中,由于倒叙枚举,每次查询的前缀和即是" a [ i ] a[i] a[i] 后面有几个数比它小",当然是逆序对的个数。时间复杂度 O ( ( n + m ) l o g m ) O((n+m)logm) O((n+m)logm),其中 m m m 为数值范围(序列中最大值)。

特别地,当 m ≥ 1 0 6 m \ge 10^6 m106 时,需要对序列进行离散化。而离散化是基于排序算法,故不如直接用归并排序求出逆序对个数。

代码 (树状数组):

//注意这段代码过不了,因为ai最大值太大了,要离散化,不如归并排序直接做
#include <iostream>
#include <cstring>
using namespace std;
const int N = 500050;
int a[N], t[N], n, m;
long long cnt;

int lowbit(int x){
    
    
    return x & -x;
}

int query(int x){
    
    
	int res = 0;
	for (int i = x; i; i -= lowbit(i))
		res += t[i];
	return res;
}

void add(int x, int d){
    
    
	for (int i = x; i <= m; i += lowbit(i))
		t[i] += d;
}

int main(){
    
    
    while (cin >> n && n){
    
    
        cnt = 0, m = 0;
        memset(t, 0, sizeof(t));
        for (int i = 1; i <= n; i ++) cin >> a[i], m = max(m, a[i] + 1);
        for (int i = n; i; i --){
    
    
            cnt += query(a[i]);
            add(a[i] + 1, 1);
        }
        cout << cnt << endl;
    }
}

猜你喜欢

转载自blog.csdn.net/f4u4u4r/article/details/121483370