一本通题解——1311:【例2.5】求逆序对

本题没有什么难度,一个模板题。之所以要写这个题解,是因为我自己第一次提交的时候,竟然没有 AC。老子立马懵逼了。后面耐心的分析了一下数据,才发现自己的错误在哪里。透剧一下,掉到数据越界的深坑了。所以记录一下。

题目

题目链接

一本通OJ:http://ybt.ssoier.cn:8088/problem_show.php?pid=1311

我的OJ:http://47.110.135.197/problem.php?id=4134

题目描述

给定一个序列 a1 , a2 , … , an,如果存在 i < j 并且 ai > aj,那么我们称之为逆序对,求序列中逆序对的数目。

输入

第一行为 n,表示序列长度。
接下来一行中有 n 个元素,第 i 个数表示序列中的第 i 个数。

输出

一行。所有逆序对总数。

样例输入

4
3 2 3 2

样例输出

3

数据范围

1 ≤ N ≤ 10^5,
-10^5 ≤ ai ≤ 10^5。

分析

一个标准求逆序对模板题。

逆序对

设 A 为一个有 n 个数字的有序集(n > 1),其中所有数字各不相同。如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。

例如,数组(3,1,4,5,2)的逆序对有(3, 1),(3, 2),(4, 2),(5, 2),共 4 个逆序对。

算法思路

求数组的逆序对就是对数组进行排序,在排序过程中记录逆序的数量即可。这样我们需要使用稳定排序算法,比如冒泡排序、插入排序和归并排序。

数据范围分析

1、从题目中,我们可以知道 n 的范围是 [1, 10^5],这样我们就知道所有 O(n^2) 复杂度的排序算法肯定是 TLE,也就是说冒泡排序和插入排序不能使用,只有是要归并排序。

2、假设最坏的情况,也就是这 10^5 个数据全部是倒序,那么逆序对应该是 10^{5}+(10^{5}-1)+(10^{5}-2)+\cdots +2+1=\frac{10^{5}*(10^{5}+1)}{2}=5*10^{9}+5*10^{4}。这个数据什么意思?我们来回忆一下 C++ 数据类型 unsigned int 的最大范围是 4,294,967,295,也就是 4.2*10^{9},说明 unsigned int 已经容纳不下这个数据。太太太坑爹了。

归并排序求逆序对

这个部分是分析的核心。首先我们用一个样例数据来说明,使用归并排序。假设我们有一个数列 [5 4 2 6 3 1],使用归并排序来看一下有几个逆序对。

第一步:二分,如下图所示。逆序对数量 = 0。

第二步:第四层 5 和 4 合并:i=l=1; r=2; mid=(l+r)/2=1; j=mid+1=2; 因为 5>4,逆序对数量+mid-i+1=1。b数组:[4 5 0 0 0 0],j+1>r;退出;a 数组 [4 5 2 6 3 1]。如下图所示。

第三步:第四层 6 和 3 合并:i=l=4; r=5; mid=(l+r)/2=4; j=mid+1=5; 因为 6>3,逆序对数量+mid-i+1=2。b数组:[4 5 0 3 6 0],j+1>r;退出;a 数组 [4 5 2 6 3 1]。如下图所示。

第四步:第三层 4,5 和 2 合并:i=l=1; r=3; mid=(l+r)/2=2; j=mid+1=3; 因为 4>2,逆序对数量+mid-i+1=4。b数组:[2 4 5 3 6 0],j+1>r;退出;a 数组 [2 4 5 3 6 1]。如下图所示。

第五步:第三层 3,6 和 1 合并:i=l=4; r=6; mid=(l+r)/2=5; j=mid+1=6; 因为 3>1,逆序对数量+mid-i+1=6。b数组:[2 4 5 1 3 6],j+1>r;退出;a 数组 [2 4 5 1 3 6]。如下图所示。

第六步:第二层 2,4,5 和 1,3,6 合并:i=l=1; r=6; mid=(l+r)/2=3; j=mid+1=4; 因为 2>1,逆序对数量+mid-i+1=9。b数组:[1 2 4 5 3 6],j+1;对应 2<3,b数组:[1 2 4 5 3 6],i+1;对应 4>3,逆序对数量+mid-i+1=11,b数组:[1 2 3 4 5 6]。j+1;对应 4<6,b数组:[1 2 3 4 5 6];i+1,5<6,b数组:[1 2 3 4 5 6];i+1>mid,退出,a 数组 [1 2 3 4 5 6],有序。如下图所示。

从上面的分析可以看出,使用归并排序求逆序对,最核心的是下面这句话

ans+=mid-i+1;

AC 参考代码

//http://ybt.ssoier.cn:8088/problem_show.php?pid=1311
//求逆序对
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e6+4;
int nums[MAXN] = {};
int tmps[MAXN];
long long ans = 0;

//使用归并排序
void merge(int l, int mid, int r) {
    int i=l;
    int j=mid+1;
    int k=l;
    while (i<=mid && j<=r) {
        if (nums[i]>nums[j]) {
            tmps[k++]=nums[j++];
            ans+=mid-i+1;
        } else {
            tmps[k++]=nums[i++];
        }
    }

    while (i<=mid) {
        tmps[k++]=nums[i++];
    }
    while (j<=r) {
        tmps[k++]=nums[j++];
    }
    for (i=l; i<=r; i++) {
        nums[i]=tmps[i];
    }
}

void mergeSort(int l, int r) {
    int mid;
    if (l < r) {
        mid = l+((r-l)>>1);
        mergeSort(l, mid);
        mergeSort(mid+1, r);
        merge(l, mid, r);
    }
}

int main() {
    int n;
    cin >> n;
    int i;
    for (i=0; i<n; i++) {
        cin >> nums[i];
    }

    mergeSort(0, n-1);

    cout << ans << endl;

    return 0;
}
  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
### 回答1: 题目描述: 给定一个序列a1,a2,…,an,如果存在i<j并且ai>aj,那么我们称之为逆序对逆序对的数目。 解题思路: 1. 归并排序 归并排序是一种分治思想的排序算法,它的基本思想是将待排序的序列分成若干个子序列,每个子序列都是有序的,然后再将有序的子序列合并成一个有序的序列。 在归并排序的过程中,我们可以统计逆序对的数量。具体来说,我们可以在合并两个有序子序列的时候,统计其中一个子序列中的元素a[i]与另一个子序列中的元素a[j](i<j)之间的逆序对数量。因为这两个子序列都是有序的,所以当a[i]>a[j]时,a[i]后面的所有元素都大于a[j],因此逆序对的数量就是i之后的元素个数。 时间复杂度:O(nlogn) 空间复杂度:O(n) 2. 树状数组 树状数组是一种用于维护序列前缀和的数据结构,它可以在O(logn)的时间内完成单点修改和前缀和查询操作。 在逆序对的过程中,我们可以使用树状数组维护序列中每个元素之前比它小的元素的个数。具体来说,我们可以将序列从大到小排序,然后依次将每个元素插入到树状数组中,并查询它之前比它小的元素的个数,这个个数就是它的逆序对数量。 时间复杂度:O(nlogn) 空间复杂度:O(n) 代码实现: 1. 归并排序 ```python def merge_sort(nums): def merge(left, right): nonlocal cnt res = [] i, j = 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: res.append(left[i]) i += 1 else: res.append(right[j]) j += 1 cnt += len(left) - i res += left[i:] res += right[j:] return res if len(nums) <= 1: return nums mid = len(nums) // 2 left = merge_sort(nums[:mid]) right = merge_sort(nums[mid:]) return merge(left, right) def count_inversions(nums): global cnt cnt = 0 merge_sort(nums) return cnt ``` 2. 树状数组 ```python class FenwickTree: def __init__(self, n): self.tree = [0] * (n + 1) def update(self, i, delta): while i < len(self.tree): self.tree[i] += delta i += i & -i def query(self, i): res = 0 while i > 0: res += self.tree[i] i -= i & -i return res def count_inversions(nums): sorted_nums = sorted(nums, reverse=True) rank = {num: i + 1 for i, num in enumerate(sorted_nums)} tree = FenwickTree(len(nums)) cnt = 0 for num in nums: cnt += tree.query(rank[num] - 1) tree.update(rank[num], 1) return cnt ``` ### 回答2: 逆序对指的是,对于任意一个序列中的一对数ai和aj,如果i<j且ai>aj,则这一对数就构成了一个逆序对逆序对的数量是衡量一个序列中乱序程度的重要指标,对于数据挖掘、机器学习和推荐系统等领域都有着广泛的应用。 逆序对的数目可以用归并排序来解决。在归并排序的过程中,将原始序列递归地划分成若干个子序列,每次合并两个有序子序列时,可以过统计左右两个子序列之间的逆序对数量来计算当前子序列的逆序对数量。具体实现过程如下: 1. 定义一个计数器count,表示逆序对数量 2. 定义一个辅助函数merge,用于合并两个有序子序列。假设left和right分别表示左右两个子序列,它们的长度分别为m和n,那么该函数的实现如下: ``` def merge(left, right): i, j, count = 0, 0, 0 res = [] while i < len(left) and j < len(right): if left[i] <= right[j]: res.append(left[i]) i += 1 else: res.append(right[j]) j += 1 count += len(left) - i res += left[i:] res += right[j:] return res, count ``` 3. 使用归并排序将原始序列排序,并计算逆序对数量。假设原始序列为a,该函数的实现如下: ``` def merge_sort(a): if len(a) <= 1: return a, 0 mid = len(a) // 2 left, x = merge_sort(a[:mid]) right, y = merge_sort(a[mid:]) res, z = merge(left, right) return res, x + y + z ``` 其中,merge_sort函数首先判断序列是否为空或只有一个元素,如果是则直接返回;否则将序列划分为左右两个子序列,并递归调用merge_sort函数,最后过调用merge函数将左右两个子序列合并为一个有序序列,并计算其中的逆序对数量。 4. 最后返回merge_sort函数的结果即可。逆序对的数量即为该函数的第二个返回值。 该算法的时间复杂度为O(nlogn),空间复杂度为O(n)。 ### 回答3: 逆序对是指在一个序列中,若前面的数比后面的数大,则这两个数构成一个逆序对逆序对的数目是经典的数学问题,也是算法设计中的一个重要问题之一。 一个朴素的做法是,遍历每对数对,判断是否为逆序对,时间复杂度为O(n^2),显然不是一个好的算法。更好的做法是基于归并排序的思想,即归并排序的“分治”和“合并”两个步骤。具体来说,对于一个给定序列,先将其分为两部分,对每一部分进行递归地分解,直到子部分不能再分,也就是只有一个元素。然后将这些子部分“合并”起来,得到新的有序序列。在合并过程中,如果一个元素在左子部分序列中,其下标为i,在右子部分序列中,其下标为j,且左子部分需要调用前k-1个元素,右子部分需要调用前l-1个元素才能得到有序的序列,则i,j之间包含l-1-k个逆序对。这是因为左右两部分元素按序合并时,当前左子部分遍历到i,右子部分遍历到j,如果此时左子部分当前元素a[i] > 右子部分当前元素a[j],则左子部分从a[1]到a[i-1]都大于a[j],因此有l-j个数与a[j]构成逆序对;同时,左右子部分的元素都是有序的,因此左子部分从a[1] 到 a[i-1],右子部分从a[1]到a[j-1]都是小于a[j]的,因此共有i-1-k个数与a[j]构成逆序对归并排序的时间复杂度为O(nlogn),因此逆序对的时间复杂度也是O(nlogn)。''' 举说明:序列23 12 3 56 78 (1) 23 12 3 56 78 (2) 23 12 3 | 56 78 (3) 23 12 | 3 | 56 78 (4) 23 | 12 | 3 | 56 | 78 对于(4),左部分的(23),右部分的(12),(3),分别组成(23,12),(23,3),(12,3),形成了3个逆序 对于(3),左部分的(23),右部分的(3),分别组成(23,3),形成了1个逆序 对于(2),左部分的(23,12,3),右部分的(56,78),形成了3个逆序 对于(1),左部分的(23,12,3,56,78),右部分的(),没有逆序 综上所述,该序列中共有7个逆序,因此逆序对数目为7。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力的老周

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值