分治-归并:高效解决逆序对问题

【算法(三·三):分治思想——逆序对计数问题】 本文介绍了逆序对计数问题的解决方案,用分治思想配合归并排序框架的效果是最好的。 阅读详情

摘要

本篇章利用分治思想,把大问题划分多个相似或相同的小问题,通过解决小问题从而达到解决大问题的目的,利用归并解决,上一篇是利用快速排序来解决,对于归并和快速排序不清楚的可以去看我的数据结构精讲中的排序章节,这里简要说明一下归并和快排的区别

他们都是利用了分治的思想,但是快排是在分治的过程当中边分边解决问题,一般来说对于算法题目分治后只需要递归处理 “包含目标元素” 的一侧子数组,另一侧直接舍弃。对于排序分的过程就是解决排序问题的过程,最后自然得到有序数组。

对于归并,是先分后解决,分治后需要递归处理两侧子数组,再通过 “合并” 操作将两侧有序子数组合成完整有序数组。分是铺垫,真正的排序工作在 “合并” 阶段集中完成。

快速选择像 “找快递”:按区域划分后,只去目标快递所在的区域查找,其他区域直接忽略,效率更高但只关注单个目标。

归并排序像 “整理书架”:先分区域整理小堆书籍,再把所有整理好的小堆合并成完整的有序书架,需要处理全部内容。

例题讲解

leetcode912.排序数组

本题主要是复习归并排序算法

代码

class Solution {
public:
    vector<int> tmp;
    vector<int> sortArray(vector<int>& nums) {
        int n=nums.size();
        tmp.resize(n);
        mergerSort(nums,0,n-1);
        return nums;
    }
    void mergerSort(vector<int>&nums,int left,int right){
        if(left>=right) return;
        
        //1.选择中间数
        int mid=(right+left)>>1;  //这里右移表示/2

        //2.递归左右区间
        mergerSort(nums,left,mid);
        mergerSort(nums,mid+1,right);

        //3.合并排序
        int cur1=left,cur2=mid+1;
        int i=0;
        while(cur1<=mid&&cur2<=right){
            tmp[i++]=nums[cur1]<=nums[cur2]?nums[cur1++]:nums[cur2++];//前置++
        }
        while(cur1<=mid) tmp[i++]=nums[cur1++];
        while(cur2<=right) tmp[i++]=nums[cur2++];

        //放回原数组
        for(i=left;i<=right;i++){
            nums[i]=tmp[i-left];
        }
    }
};

注意:

我们选择left+right直接/2是因为left+right题目给的数据不会超出范围,如果超过范围我们还需要防止,使用left+(right-left)/2这种在二分那里为了避免越界的做法

此处我们是选择一个全局的变量来存储我们归并过程中的数组,然后再拷贝,我们也可以使用每次递归进去创建的数组,也就是栈数组,随着递归才创建,此时时间复杂度会变高,创建也是耗时的,所以为了减少时间复杂度我们创建了一个全局的数组,这样就不需要再每次递归的时候才创建

leetcode170.交易逆序对的总数

题目解析

前一个数大于后一个数就需要统计,如果是=就不需要,依次统计出有多少对,然后返回即可

算法原理讲解

暴力算法:枚举每一对,然后检查是否是前一个大于后一个即可,时间复杂度O(n^2)

分治-归并:归并时会自动划分左右区间,这样就天然的给我们准备了一个前和后的关系,只需要比较前面的是否大于后面的即可,所以合并过程当中我们需要排降序(这里排升序也行,看你怎么计算怎么比较怎么统计)

如果前一个大于后一个,那说明都大于右边区间的所有数,此时统计一下即可

如果前一个小于等于后一个,说明没有前面的都不会大于后面的这一个,此时不需要统计

为什么这样可以,因为使用归并的时候你就已经划分了数组,分为前后/左右了,所以你递归下去排序某一边根本不影响另一边,也就是你的相对顺序是对的,你只是改变了左区间里面的顺序,不影响和右区间的相对顺序,还是一前一后

代码

class Solution {
public:
    int count = 0;
    vector<int> tmp;
    int reversePairs(vector<int>& record) {
        // 使用归并排降序,然后比较
        int n = record.size();
        tmp.resize(n); // 防止后续访问的时候野指针
        mergerSort(record, 0, n - 1);
        return count;
    }
    void mergerSort(vector<int>& nums, int left, int right) {
        // 递归出口
        if (left >= right)
            return;

        // 选择中间数
        int mid = (left + right) >> 1;
        // 递归左右区间
        mergerSort(nums, left, mid);
        mergerSort(nums, mid + 1, right);
        // 合并过程排降序,注意count的结果
        int cur1 = left, cur2 = mid + 1, i = 0;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur1] <= nums[cur2]) {
                // 左边最大的小于右边最大的,不处理count
                tmp[i++] = nums[cur2++];
            } else {
                // 左边最大的大于右边最大的
                count += right - cur2 + 1;
                tmp[i++] = nums[cur1++];
                
            }
        }
        // 如果某一边全部放完了count不需要处理,只需要放到tmp
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];

        // 拷贝
        for (i = left; i <= right; i++) {
            nums[i] = tmp[i - left];
        }
    }
};

leetcode315.计算右侧小于当前元素的个数

此题和170一样的,统计的结果放到数组当中返回即可,代码上稍加修改一下即可

注意:

难点就在于:我们在排序的过程当中会改变原始数组,所以一开始我们要存元素:原始下标的对应关系,否则你在递归合并的过程当中会改变原数组导致下标变换,进而导致统计的对数会乱加到不同的位置上,至于你最后返回的数组会出错

如果这里是弄一个hash的话就会出错,因为有相同元素,相同元素遍历到后面会覆盖前面的相同元素的下标,就导致了所有的相同元素共用一个下标,导致全部加在一起

我们要额外开辟一个数组,然后如果nums数组的元素进行了排序,index数组也要跟着移动,也就是绑定移动,要移动就同时移动

我们还需要额外开辟一个数组也就是tmp2,tmp2的作用就是辅助index数组的变换

tmp1的作用是辅助nums的变换

最后两个都需要进行拷贝变换

简单来说就是你的nums变换的时候,index也要跟着变换,下标就是正确的了,ret数组一直存结果cur1和cur2指向哪,nums的元素对,index的下标也对

代码

class Solution {
public:
    vector<int> tmp1;  // 辅助ret
    vector<int> tmp2;  // 辅助index变换更新
    vector<int> ret;   // 存结果
    vector<int> index; // 存下标
    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        index.resize(n);
        tmp1.resize(n);
        ret.resize(n, 0);
        tmp2.resize(n);
        for (int i = 0; i < n; i++) {
            index[i] = i;
        }
        mergerSort(nums, 0, n - 1);
        return ret;
    }
    void mergerSort(vector<int>& nums, int left, int right) {
        // 递归出口
        if (left >= right)
            return;

        // 选择中间数
        int mid = (left + right) >> 1;
        // 递归左右区间
        mergerSort(nums, left, mid);
        mergerSort(nums, mid + 1, right);
        // 合并过程排降序,注意count的结果
        int cur1 = left, cur2 = mid + 1, i = 0;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur1] <= nums[cur2]) {
                // 左边最大的小于右边最大的,不处理count
                tmp2[i] = index[cur2];
                tmp1[i++] = nums[cur2++];

            } else {
                // 左边最大的大于右边最大的
                tmp2[i] = index[cur1];
                int count = right - cur2 + 1; // 统计当前次数
                ret[index[cur1]] += count;
                tmp1[i++] = nums[cur1++];
            }
        }
        // 如果某一边全部放完了count不需要处理,只需要放到tmp
        while (cur1 <= mid) {
            tmp2[i]=index[cur1];
            tmp1[i++] = nums[cur1++];
        }
        while (cur2 <= right) {
            tmp2[i]=index[cur2];
            tmp1[i++] = nums[cur2++];
        }

        // 拷贝
        for (i = left; i <= right; i++) {
            index[i]=tmp2[i-left];
            nums[i] = tmp1[i - left];
        }
    }
};

leetcode493.翻转对

算法原理讲解

有人见到这道题可能就会想要用之前的解法一模一样的套进去,但是发现出错,这道题的解法原理和之前一样,但是有些细节要处理

因为之前是单单不叫前面一个数大于后面一个数就叫逆序对,然后同时进行排序

但是这里不是,这里是大于两倍,你不能同时排序

假设统计翻转对的同时进行排序

比如132这个数组,递归到下面时就是【1,3】【2】

左边【3,1】右边【2】

因为3<=2*2,所以不满足

tmp【2】,然后循环结束因为cur2到尾巴了

所以把cur1接到tmp后面就会变成

tmp【2,3,1】,此时数组不是一个有序的情况,就会导致后面如果还有变的元素就会出错

因为你所有的基础都来源于左边区间有序,右边区间有序才能统计

所以解决方案就是,我们先统计完翻转对之后,再排序

就是你先全部统计完,然后再使用cur1>cur2这样来排序,因为两倍会把排序搞乱

注意

一定要清楚因为比较条件改变了,原来合并数组的时候比较条件就是前一个数的一倍和后一个数的一倍进行比较,排序是这样比,逆序对也是这样比,可以边排序边找逆序对

但是这里翻转对不行,排序是一倍比一倍,但是翻转对是一倍比两倍

一定要注意这是一个坑,也是这道题的本质

class Solution {
public:
    int count = 0;
    vector<int> tmp;
    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        tmp.resize(n);
        mergerSort(nums, 0, n - 1);
        return count;
    }
    void mergerSort(vector<int>& nums, int left, int right) {
        // 递归出口
        if (left >= right)
            return;
        // 选择中间数
        int mid = (left + right) >> 1;
        // 递归左右区间
        mergerSort(nums, left, mid);
        mergerSort(nums, mid + 1, right);
        // 合并过程统计,注意count的结果
        int cur1 = left, cur2 = mid + 1;
        while (cur1 <= mid && cur2 <= right) {
            if ((long long)nums[cur1] > 2 * (long long)nums[cur2]) {
                // 统计翻转对
                count += right - cur2 + 1;
                cur1++;
            }
            else cur2++;
        }

        //统计完之后再进行排序
        cur1 = left, cur2 = mid + 1;
        int i=0;
        while(cur1<=mid&&cur2<=right){
            if(nums[cur1]>=nums[cur2]){
                tmp[i++]=nums[cur1++];
            }
            else{
                tmp[i++]=nums[cur2++];
            }
        }
        while(cur1<=mid) tmp[i++]=nums[cur1++];
        while(cur2<=right) tmp[i++]=nums[cur2++];

        // 拷贝
        for (i = left; i <= right; i++) {
            nums[i] = tmp[i - left];
        }
    }
};

注意:处理当中可能会*2可能会溢出,所以使用long long来比较

升序和降序本质都是一样的,只是比较统计的时候角度不一样而已,可以自行写一下

总结

如果题目符合以下任一逻辑,可优先考虑快排 / 归并的分治思路:

  • 逻辑 1:问题可拆分为 “子问题独立解决,再合并结果”(归并的核心)。
  • 逻辑 2:问题可通过 “分区操作,只处理部分子问题” 来缩小规模(快排递归某一区间)。

举个例子:

  • 若题目是 “统计数组中所有 i<j 且 nums [i]>nums [j] 的对数”→ 归并的分治(合并时统计逆序对)。
  • 若题目是 “找到数组中第 3 大的元素”→ 快速选择的分治(分区后只处理包含目标的区间)。

基本算法之分治 分治 阅读详情

相关推荐

分治算法,逆序对,三维偏序与CDQ分治

当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。 对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。 如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治策略的基本思想。

tanjianlang的博客 1511

算法分析与设计——分治策略求逆序对(归并排序)

算法分析与设计——分治策略求逆序对(归并排序),包含代码,python实现。

计算机硕士的博客 2675

java实现求逆序对

1 问题描述 给定一个随机数数组,求取这个数组中的逆序对总个数。要求时间效率尽可能高。 那么,何为逆序对? 引用自百度百科: 设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。 如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。 ...

南 墙 1万+

求逆序对数(分治法)

核心思路:利用分治将问题折半,认为分成的两个子问题均有解并已返回,因此原问题就转化成如何处理跨越两个子问题部分的逆序对数。 子问题有解:是分治的基本假设,(同数学归纳法假设n=k时成立)即假设该问题是有解的,换种方式理解就是在不断递归的过程中,我们最先处理到的是当序列长度为1的情况,毫无疑问返回0。然后再处理将两个子序列合并的过程,合并后整个序列又可以作为更长序列的子序列。因此只要我们能够处理序列合并,该问题可解。 序列合并:合并时由于我们追求O(nlogn)的时间复杂度,由主定理可知合并操...

既脱春火的博客 2194

CDQ分治详解

CDQ分治

m0_57344422的博客 1757

分治法求数组中的逆序数

有一实数序列a1,a2,....an,若iaj,则(ai,aj)形成了一个逆序对,请使用分治算法求整个序列中逆序对个数,并分析算法时间复杂度。

分治法求逆序对个数

分治法求逆序对数,归并排序

weixin_50894562的博客 3210

分治法求逆序数

求逆序数的方法很多。最容易想到的办法是分别对序列中每一个元素求其逆序数,再求所有元素的逆序数总和,易分析得出这样的方法其时间复杂度为O(n2)。而这里采用的分治法求逆序数,其时间复杂度为O(nlogn)。

分治归并算法第一弹

本文以三道题目为例,讲解了分治归并算法,希望对你有所帮助!

2301_79171011的博客 1082

算法设计之分治思想(求数组的逆序对)

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 示例 1: 输入: [7,5,6,4] 输出: 5 限制: 0 <= 数组长度 <= 50000 首先最容易想到的是暴力解法。 方法一:暴力解法(超时) 使用两层 for 循环枚举所有的数对,逐一判断是否构成逆序关系。 参考代码 1: java private static int reversePairs(int[] nums) { // TODO Auto-generated method stub int res = 0; int len = num

归并求逆序对 分治 递归

利用二路归并排序求逆序对,很巧妙的一种算法

leetcode **剑指 Offer 51. 数组中的逆序对(分治 - 归并排序)(2020.4.24)

【题目】**面试题51. 数组中的逆序对 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 示例 1: 输入: [7,5,6,4] 输出: 5 限制: 0 <= 数组长度 <= 50000 【解题思路1】分治 - 归并排序 两个子区间分别有序,先计算两个子区间内部的逆序对个数,然后计算在合并的过程中跨越两个区...

Sophia_fez的博客 603

DRAM内存模拟持久化设备的方法

由于持久化内存介质NVM技术的发展,越来越多的科研工作者投身到NVM相关的研究当中。但由于目前工业界并没有推出广泛使用的NVM介质设备,所以科研工作都需要自己以现有的条件,模拟NVM设备使用场景。一个简单的方法是:通过修改DRAM模拟器,达到模拟NVM的目的(可以参考:DRAMSim2模拟器分析与应用)。更高端的方式是:在真实系统中,将内存划分出一块儿,用来模拟NVM设备(类似于传统的ramdis

酷炫人笨笨熊 1899

LeetCode算法日记 - Day 26: 归并排序、交易逆序对的总数

本文介绍了两个基于归并排序的算法问题。第一个是力扣912题"排序数组",要求在不使用内置函数的情况下实现O(nlogn)时间复杂度的数组排序,采用归并排序的分治策略,通过递归二分和合并有序子数组实现,空间复杂度为O(n)。第二个是力扣LCR170题"交易逆序对的总数",本质是统计数组中满足i<j且record[i]>record[j]的有序对数量,同样利用归并排序的特性,在合并过程中批量统计跨段逆序对,将暴力解法O(n²)优化至O(nlogn)。两个问题都

Miraitowa_cheems的博客 780

如何构建高质量的 QA 问答知识库

NLP 技术落地最普遍的场景就是智能客服,如阿里的”小蜜“,京东的“JIMI”等等。NLP 技术也广泛应用于搜索系统。无论是“智能客服”,还是“搜索系统”,构建的前提是拥有“高质量的QA知识库”。今天聊聊“如何构建高质量QA知识库。”。 历史对话数据分布分析 基于公司现有系统积累的大规模对话文本数据,需要对文本数据进行归类处理,分析每个部分数据的占比,挖掘出有价值的文本通过 NLP 相关技术进行商业价值转化。下文以医疗领域来举例说明,通过输入对象,可以将文本分为客服输入和用户输入两部分。挖掘相关问题,主要从

Jiassheng的博客 2151

台达PLC DIAdesigner-AX 编程笔记

1、登陆台达思图软件网站,选择Download,登陆注册好的账号,下载DIAInstaller,下载后安装(安装目录不要修改);

weixin_45823621的博客 6675

【优选算法】D&C-Mergesort-Harmonies:分治-归并的算法之谐

本文介绍了分治归并排序算法及其应用。分治归并通过“拆分-排序-合并”三步将无序数组转化为有序数组,核心是后序遍历思路。文章给出了三个LeetCode题目及其解法:1) 排序数组,使用归并排序实现;2) 统计逆序对,利用归并排序的有序合并特性高效计算;3) 计算右侧小于当前元素的个数,通过绑定值和下标解决。关键细节包括中间值计算防溢出、部分数组排序处理等。代码实现展示了归并排序在这些问题中的高效应用。

Zero_VPN的博客 3311

RS485/CAN总线防护电路设计:TVS选型与ESD防护实战解析

本文深入解析了RS485与CAN总线防护电路设计中的TVS选型与ESD防护实战技巧。针对工业现场常见的静电放电、浪涌等干扰,文章详细阐述了TVS管关键参数的选择、多级防护体系的构建,并特别强调了在隔离电路设计中地回路处理的重要性,帮助工程师设计出更可靠的通信接口。

metaverse5vr的博客 386

算法笔记08 - 归并排序 | 分治 | 小和问题 | 逆序对问题 | BiggerThanTwice问题 | 分治做题总结

归并排序是一种稳定高效的排序算法,采用分治策略将数组不断二分直到单元素有序,再逐步合并有序子数组。其核心在于合并过程,通过双指针依次比较左右部分元素,将较小值存入辅助数组。该算法时间复杂度稳定为O(nlogn),不受数据初始状态影响。文章详细介绍了递归实现(划分左右区间分别排序后合并)和迭代实现(通过步长控制逐步合并子数组)两种方法,并提供了完整的Java代码示例,展示了从单元素开始逐步合并直至整个数组有序的过程。

zyw4259的博客 1000

WPF+Prism+LiveCharts+materialdesignin 搭建WPF开发框架

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。提示:以下是本篇文章正文内容。

T_Tboy的博客 3051

网络工程师教程第6版(2024年最新版)

本书 根据《网络工程师考试大纲》(2024年审定通过)编写,对网络工程师岗位所要求的主要知 识及应用技术进行了阐述。本书主要内容包括数据通信、局域网、无线通信网、网络互连、网络安全、网络操作系统与应用服务器、组网技术、网络管理、网络规划和设计。本书是参加网络工程师考试的必备教材,也可作为网络工程从业人员学习网络技术的教材或日常工作的参考用书。王涛和严体华编写了第2章、第3章和第4章;网络工程师教程(第6版)由清华大学出版社出版,由工业和信息化部教育与考试中心组编,张永刚、王涛、高振江任主编,具体介绍如下。

鹿鸣天涯 6902

算法日记分治:用归并排序解决逆序对问题

文章摘要: 本文介绍了利用归并排序解决数组中逆序对问题的两种策略。策略一通过升序数组统计当前数之前比它大的数的个数,策略二通过降序数组统计当前数之后比它小的数的个数。两种方法均通过分治思想实现,时间复杂度为O(nlogn)。代码示例分别展示了升序和降序归并排序的实现,核心步骤包括递归划分、合并有序子数组及逆序对统计。适用于算法学习及面试准备场景。 关键词:逆序对、归并排序、分治算法、升序/降序统计、时间复杂度优化

asciiletters的博客 506

DaVinci配置DID之三:代码实现

本系列主要基于DaVinCi工具链来展开具体DID读写的配置以及最终实现。1,CDD准备2,工具链配置3,代码实现4,验证结果本文讲述流程3话不多说,走你~博主是AutoSar小白,如果哪里表达的有问题,还请大佬们指点指点哈。接下来,让我们在下章关于测试验证的博文再相会哈~

weixin_48498880的博客 5357

从归并排序(Merge Sort)到求逆序对个数

从归并排序(Merge Sort)到求逆序对个数 在前面的博客中,我们已经讨论了归并排序算法的核心思想,以及代码实现(代码我复制在下面了)。事实上归并排序经常和求逆序对个数这个问题联系在一起,并且求逆序对个数这个问题还是比较常见的,现在我们讨论一下求逆序对个数这个问题。 思路介绍 我们还记得之前讨论归并排序时,归并排序的算法核心在于是采用分治法将整个序列不断地进行分割合并。和归并排序一样,逆序对问题的算法核心也是在于采用分治法将将整个序列不断地进行分割合并。分治三步法如下: 划分问题:把序列分成元素个数尽

weixin_55835175的博客 623

Cadence Orcad Capture原理图断线的检查与避免方法

在设计电路图中,尤其是直接借鉴别人的设计进行二次开发的过程中难免会遇到如下尴尬的情况。有没有眼神好的朋友发现图纸的哪里有不对劲的地方呢?

fydar的博客 3268

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

上一篇: 分治思想之快排优化:三分区防退化与剪枝降复杂度
下一篇: 高并发内存池:从tcmalloc到实战
Fcw0551
博客等级 码龄2年 330粉丝 115原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值