AcWing788.逆序对的数量

该题来自www.acwing.com,yxc讲得非常好,以下题解是我看完yxc的讲解后的一点点启发。

题目

给定一个长度为n的整数数列,请你计算数列中的逆序对的数量。

逆序对的定义如下:对于数列的第i个和第j个元素,如果满足i<j且a[i]>a[j],则其为一个逆序对;否则不是。

输入格式

第一行包含整数n,表示数列的长度。

第二行包含n个整数,表示整个数列。

输出格式

输出一个整数,表示逆序对的个数。

数据范围

1≤n≤100000,
数列中的元素的取值范围[1 , eq?10%5E9]

输入样例:

6
2 3 4 5 6 1

输出样例:

5

题解

第一步思路

这道题是明显的分治问题,像归并排序一样思考。

以下奇数组分为:(奇数-1)/2是一组,(奇数+1)/2是一组,偶数组直接除以二就行了。

逆序对必须是当前组的前半组挑一个数和当前组的后半组挑一个数来组成的(而不能是前半组挑两个数组成逆序对)这样想是因为这道题是明显的分治问题

将数列分为两个数一组的,计算每一组的逆序对个数并相加,将所有两个数一组的计为第一层, 

将数列每四个数分为一组,计算每一组的逆序对个数并相加,将所有四个数一组的计为第二层

将数列每八个数分为一组,计算每一组的逆序对个数并相加,将所有八个数一组的计为第三层

以此类推,一直到将整个数列作为一组,计算这最后一组的逆序对个数,这一组是最后一层

最后将每一层的逆序对个数相加,得到一个数,就是这道题的答案。

这里的每一层的逆序对都是不重复的,所有层包括了逆序对的所有情况,这一点应当是非常容易想到的,结合以下两张图也许能更直观的思考

333f6a4e7f214a4f947635cbee3c46b2.png

b1d7a0b4b3ec4b6aa2309ca58d18f13e.png

 因此,可以写出代码如下

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N];
int n;
long long merge_nixvdui(int q[], int l, int r)
{
    if (l >= r)return 0;
    int mid = l + r >> 1;
    long long num = merge_nixvdui(q, l, mid) + merge_nixvdui(q, mid + 1, r);
    int i = l, j = mid + 1;
    for (int i = l; i <= mid; ++i)
        for (int j = mid + 1; j <= r; ++j)
            if (q[i] > q[j])
                ++num;
    return num;
}
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; ++i)
        scanf("%d", q + i);
    printf("%lld", merge_nixvdui(q, 0, n - 1));
    return 0;
}

d6dbadee58494281854858cefe7cf95b.png

经典的错误,标准的零分,这一看时间复杂度就太高了,指定超时。

第二步思路

b1d7a0b4b3ec4b6aa2309ca58d18f13e.png

如图是最后一层(即最后一组);先将前半组的数字在前半组内任意颠倒顺序,再将右半组的数字在右半组内任意颠倒顺序;这影响左边取一个数,右边取一个数计算出的最后一层的逆序对个数吗?

显然不影响,那么将左边的数列单独排个序,再将右边的数列单独排个序也不影响逆序对个数,不过此时已经排好序的逆序对个数很好算,可以看看下面的代码

int i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j])++i;
        else
            ++j, sum += mid - i + 1;

沿着这个思路,我们需要先算得当前组的逆序对个数,再将当前组排序,这是明显的归并排序。

于是,最终代码如下

代码

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], tmp[N];
int n;
long long merge_sort(int q[], int l, int r)
{
    if (l >= r)return 0;
    int mid = l + r >> 1;
    long long sum = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r);
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j])tmp[k++] = q[i++];
        else
            tmp[k++] = q[j++], sum += mid - i + 1;
    while (i <= mid)tmp[k++] = q[i++];
    while (j <= r)tmp[k++] = q[j++];
    for (int i = l, k = 0; i <= r; ++i, ++k)q[i] = tmp[k];
    return sum;
}
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; ++i)
        scanf("%d", q + i);
    printf("%lld", merge_sort(q, 0, n - 1));
    return 0;
}

第三步思路(可以忽略,不影响这道题)

虽然上面的代码已经ac了,但是可以从中看出这是一个二叉树,也许有办法将其进一步优化?

千里之行,始于足下。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值