hihoCoder_二分·归并排序之逆序对

本文介绍了一种使用二分和归并排序算法来计算逆序对的方法。首先,详细阐述了题目的要求,即找到序列中的逆序对。接着,讲解了解题的关键技巧,包括如何巧妙地应用二分搜索来优化过程。然后,提供了完整的C++实现代码,展示如何将思路转化为程序。最后,作者分享了对此问题的深入体会和理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.题目

题目1 : 二分·归并排序之逆序对

时间限制:10000ms
单点时限:1000ms
内存限制:256MB
描述

在上一回、上上回以及上上上回里我们知道Nettle在玩《艦これ》。经过了一番苦战之后,Nettle又获得了的很多很多的船。
这一天Nettle在检查自己的舰队列表:

我们可以看到,船默认排序是以等级为参数。但实际上一个船的火力值和等级的关系并不大,所以会存在A船比B船等级高,但是A船火力却低于B船这样的情况。比如上图中77级的飞龙改二火力就小于55级的夕立改二。
现在Nettle将按照等级高低的顺序给出所有船的火力值,请你计算出一共有多少对船满足上面提到的这种情况。

提示:火力高才是猛!

输入

第1行:1个整数N。N表示舰船数量, 1≤N≤100,000
第2行:N个整数,第i个数表示等级第i低的船的火力值a[i],1≤a[i]≤2^31-1。

输出

第1行:一个整数,表示有多少对船满足“A船比B船等级高,但是A船火力低于B船”。

样例输入
10
1559614248 709366232 500801802 128741032 1669935692 1993231896 369000208 381379206 962247088 237855491
样例输出
27

二.解题技巧

    根据题目的提示,这道题可以使用二分归并排序来对数组进行排序的同时,统计逆序数的个数。具体做法是将两个子问题进行二分归并排序后得到的逆序数加上将子问题进行归并的时候,后面一半的元素移动的位置来得到最终的逆序数。


三.实现代码

#include <iostream>

#include <vector>

using namespace std;

long long Merge(vector<int>::iterator BeginIte, int Number)
{
    if (Number < 2)
    {
        return 0;
    }

    int Mid = Number / 2;

    int IndexFirst = 0;
    int IndexSecond = Mid;

    vector<int> TmpArray;
    long long Count = 0;
    int Index = 0;
    while ((IndexFirst < Mid) && (IndexSecond < Number))
    {
        int TmpFirst = *(BeginIte + IndexFirst);
        int TmpSecond = *(BeginIte + IndexSecond);

        if (TmpFirst > TmpSecond)
        {
            TmpArray.push_back(TmpSecond);
            Count += IndexSecond++ - Index++;
        }
        else
        {
            TmpArray.push_back(TmpFirst);
            IndexFirst++;
            Index++;
        }
    }

    while (IndexFirst < Mid)
    {
        TmpArray.push_back(*(BeginIte + IndexFirst++));
    }

    while (IndexSecond < Number)
    {
        TmpArray.push_back(*(BeginIte + IndexSecond++));
    }

     // copy the array back
    for (int Index = 0; Index < Number; Index++)
    {
        *(BeginIte++) = TmpArray[Index];
    }

    return Count;

}


long long MergeSort(vector<int>::iterator BeginIte, int Number)
{
    if (Number < 2)
    {
        return 0;
    }

    int Mid = Number / 2;
    long long Count = 0;
    Count += MergeSort(BeginIte, Mid);
    Count += MergeSort(BeginIte + Mid, Number - Mid);
    Count += Merge(BeginIte, Number);

    return Count;

}


int main()
{
    int Number = 0;
    cin >> Number;

    int Index = 0;
    vector<int> Array;
    while (Index++ < Number)
    {
        int Tmp = 0;
        cin >> Tmp;
        Array.push_back(Tmp);
    }

    cout << MergeSort(Array.begin(), Number) << endl;

//    for (int Index = 0; Index < Number; Index++)
//    {
//        cout << Array[Index] << endl;
//    }

    // cout << "The result is " << Count << endl;



    return 0;
}


四.体会

    这道题让我长了见识,我记得我以前的做法是先使用STL里面的sort函数对数组进行排序,然后根据排序后的数组的元素的下标与相同元素在原来数组里面的下标的关系,来计算逆序对的个数(也就是逆序数)。这种方法不仅需要进行排序的时间O(nlgn),在使用hash_table的时候,还需要O(n)的时间来判断元素在排序后的数组的下标与原来数组的下标的关系,稍微有些复杂。
    这道题使用二分归并排序在进行排序的同时,也递归计算量数组中的逆序对的个数,是一种很实用的方法。这种方法主要是基于数组中的逆序对的个数等于子问题中存在的逆序对的个数加上归并过程中存在的逆序对的个数。归并过程中存在的逆序对的个数等于后面一半的元素移动到正确位置时需要移动的位数的和。



版权所有,欢迎转载,转载请注明出处,谢谢微笑



<think>好的,我现在需要解释归并排序如何用于计算逆序对的原理。首先,我得回忆归并排序的基本步骤,因为逆序对的计算是在这个过程中进行的。归并排序是分治法的典型应用,分为分解、解决和合并三个阶段。分解阶段将数组不断二分,直到子数组长度为1;解决阶段递归排序子数组;合并阶段将两个有序子数组合并成一个有序数组。 接下来,用户想知道如何在这个过程中计算逆序对逆序对的定义是数组中前面的元素大于后面的元素的情况。比如数组[3,1,2]中的逆序对有(3,1)和(3,2),共2对。 在归并排序的合并过程中,左右两个子数组都是有序的。当合并时,比较左右两个数组的元素,如果左边的当前元素大于右边的当前元素,那么左边数组中该元素及其后面的所有元素都会与右边的这个元素形成逆序对。例如,左数组是[2,5,7],右数组是[3,4,6],当比较2和3时,2小于3,直接放入结果;然后比较5和3,此时5大于3,说明左数组中5及其后面的元素(5,7)都与3形成逆序对,这里贡献了2个逆序对。这一步是关键,需要在合并过程中统计这些情况。 根据引用[3],归并排序过程中计算逆序对的具体思路是在合并时统计跨越左右子数组的逆序对数量。每次发现右子数组的元素小于左子数组的当前元素时,累加左子数组剩余元素的个数作为逆序对的数量。这需要在合并函数中添加一个计数器,并在每次交换时更新这个计数器。 需要注意的点是,分解和递归处理子数组时,需要分别计算左半部分、右半部分以及合并时的逆序对,并将三者相加。这样才能得到整个数组的逆序对总数。例如,假设左子数组内部有a个逆序对,右子数组内部有b个,合并过程中产生c个,总逆序对数为a+b+c。 可能用户会疑惑为什么归并排序适合计算逆序对。因为归并排序在合并时已经让左右子数组有序,这样在比较时可以高效地统计跨子数组的逆序对,而不需要逐个比较,从而将时间复杂度保持在O(n log n),这比暴力法的O(n²)高效得多。 此外,引用[2]提到归并排序是稳定的,不会改变相等元素的顺序,这对保持逆序对的准确性有帮助。同时,合并过程中通过指针移动的方式,可以确保每个逆序对被正确计数一次,不会有重复或遗漏。 举个例子,假设有一个数组[4,3,2,1],使用归并排序计算逆序对。分解到最底层后,合并时每个元素比较都会产生逆序对。例如,合并[4]和[3]时,4>3,逆序对+1;合并[3,4]和[1,2]时,3>1,此时左子数组剩余2个元素(3,4),所以逆序对+2,接着3>2,再+2,4>1和4>2各+2,总共有1+2+2+2+2=9,但实际上正确总数应该是6。这说明例子可能需要更详细的计算步骤来验证,但大致思路是对的,即每次右元素较小的时候累加左剩余元素的数量。 因此,总结归并排序计算逆序对的原理为:在合并过程中,当右子数组的元素被选中放入结果数组时,统计左子数组剩余元素的个数,这些元素都与当前右元素形成逆序对,并将这些数目累加,最终得到总的逆序对数量。</think>### 归并排序计算逆序对的原理 归并排序利用**分治思想**和**有序数组合并**的特性,在合并阶段高效统计逆序对。具体步骤如下: 1. **分治分解** 将数组递归拆分为左右两个子数组,直到子数组长度为1(天然有序)。 2. **合并与统计** 合并两个有序子数组时,若发现**左子数组当前元素 > 右子数组当前元素**,则说明左子数组中该元素及之后的所有元素均与右子数组当前元素构成逆序对。此时逆序对数量需增加左子数组剩余元素的个数[^3]。 3. **累加逆序对** 逆序对总数 = **左子数组内部的逆序对** + **右子数组内部的逆序对** + **合并时跨子数组的逆序对**。 --- #### 示例说明 假设数组为 $[7, 5, 2, 6]$,分解后合并过程如下: 1. 左子数组 $[7,5]$ → 拆分为 $[7]$ 和 $[5]$,合并时发现 $7 > 5$,逆序对 $+1$,合并结果为 $[5,7]$。 2. 右子数组 $[2,6]$ → 天然有序,合并后仍为 $[2,6]$。 3. 合并 $[5,7]$ 和 $[2,6]$: - $5 > 2$ → 左子数组剩余2个元素(5,7),逆序对 $+2$。 - $5 < 6$ → 无逆序对。 - $7 > 6$ → 左子数组剩余1个元素(7),逆序对 $+1$。 - 总逆序对 $=1+2+1=4$。 --- #### 代码实现(Python) ```python def merge_sort_count(arr): if len(arr) <= 1: return arr, 0 mid = len(arr) // 2 left, cnt_left = merge_sort_count(arr[:mid]) right, cnt_right = merge_sort_count(arr[mid:]) merged, cnt_merge = merge(left, right) return merged, cnt_left + cnt_right + cnt_merge def merge(left, right): merged = [] i = j = cnt = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: merged.append(left[i]) i += 1 else: merged.append(right[j]) cnt += len(left) - i # 统计逆序对 j += 1 merged += left[i:] merged += right[j:] return merged, cnt ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值