逆序对问题的两种求解思路(归并排序和树状数组)

news2025/1/20 5:55:18

前言:我们在求解逆序对问题时题目往往会给我们加大数据量,防止我们以暴力的方式通过该题,所以在遇到有关求解逆序对问题的时候,我们有必要知道一些具体的优化方法,对于逆序对我们,我们一般的会有两种标准求解方法:归并排序和树状数组,接下来我就分别来介绍这两种方法的原理和实现,争取能让你们一文精通逆序对问题。

话不多说,正文开始:

目录

1.背景引入:

2.归并排序思路

3.树状数组法(注意数组要从下标1开始用)

3.1树状数组和前缀和

3.1.2 lowbit函数

树状数组求解前缀和

3.2树状数组求解逆序对个数

4.金句省身


1.背景引入:

 

 这是一道简单的逆序对计数问题,我们需要找到所有的逆序对的数目并返回。

2.归并排序思路

     我们都知道,我们在学习归并排序时是将划分成的每个小子区间内的逆序的两个元素进行交换,然后将这个小子区间的排好序的元素按原位置,这点很重要,这里我以图的形式来表述,希望可以更好的帮助理解:

不知道好不好理解,但是按我的理解只能画成这样了,下面我们再根据上面的思想写代码:

#include<bits/stdc++.h>
using namespace std;

const int maxn = 5e5 + 5;
typedef long long ll;
ll mem[maxn];
ll temp[maxn];
ll n;
ll ans = 0;
void memerg(ll l, ll r)
{
	if (l >= r)
		return; //递归返回条件
	ll mid = (l + r) >> 1;
	memerg(l, mid);
	memerg(mid + 1, r);
	ll i = l, j = mid + 1, t = 0;
	while (i <= mid && j <= r)
	{
		if (mem[i] > mem[j])
		{
			temp[t++] = mem[j++];
			ans += mid - i + 1;//左半部分的i-mid都可以和j组成逆序对
		}
		else
		{
			temp[t++] = mem[i++];
		}
	}
	while (i <= mid) temp[t++] = mem[i++];
	while (j <= r)temp[t++] = mem[j++];
	for (ll k = 0; k < t; k++)
		mem[l + k] = temp[k];//注意这个地方是个易错点,一定要按照原数组对应的下标返回排好序的值,这样才会保证逆序对不会出现重复解

}
int main()
{
	while (scanf("%d", &n) && n != 0)
	{
		memset(mem, 0, sizeof(mem));
		for (ll i = 0; i < n; i++)
			scanf("%lld", &mem[i]);
		ans = 0;
		memerg(0, n - 1);
		printf("%lld\n", ans);
	}
	return 0;
}

3.树状数组法(注意数组要从下标1开始用

3.1树状数组和前缀和

树状数组是利用数的二进制特征进行检索的一种树状的结构,所以首先,我们要先来了解一下,树状数组的原理和其求前缀和所带来的高效性:

假设我们对一个长度高达几百万的数组求前缀和,我们需要怎么优化呢?这里我们可以应用线段树的思想:

 接着,我们会发现,每一行的第偶数个元素在求前缀和时都会有更好的方法,所以这个数据我们是用不到的,于是我们将其从图中删除,就变成这样:(请忽略这些蓝线

 到这里学过数据结构的就有点坐不住了,这......我咋看咋那么像个二叉树呢?于是,我们就将它按二叉树的形式展开(顺便对其进行中序遍历):

 接着,我们在引入树状数组的核心:lowbit函数

3.1.2 lowbit函数

int lowbit(int x)
{
    return x & (-x);//涉及负数的补码和正数补码的计算,这里不在展开
}

功能:找到一个数二进制数中的最后一个1,并返回这个1所代表的权值,其原理是利用了负数的补码表示

我们将我们前面求出的结构再次给出:

我们不难发现如下规律:

对于每一行元素,他们的lowbit值都是一样的:

 那么,这对我们求解前缀和的算法优化有什么帮助呢?我们现在来分析

树状数组求解前缀和

我们可以发现,中序遍历数组中的下标和lowbit可以产生联系,这里来举个例子:

       假设中序遍历数组为tree,我们以tree[6]=11为例,我们知道,6的二进制位110,6=4+2,我们从图中看出tree[6]的这个11并不是前6项的和,这个11其实是第六项和第五项的和,怎么办,我们还差前四项的和,而我们的前四项的和储存在tree[4]中,也就是说,我们的sum[6]=tree[6]+tree[4],我们再和前面的lowbit函数结合一下,我们就可以得到求解前缀和的一般形式:

int lowbit(int x)
{
	return x & (-x);//涉及负数的补码和正数补码的计算,这里不在展开
}
int sum(int x)
{
	int ans = 0;
	while (x)
	{
		ans += tree[x];
		x -= lowbit(x);  //比如sum[6]=tree[2]+tree[4](6的二进制位110)
	}
	return ans;
}

这里为了证明求解是对的,我给出这个数组的求解和标椎前缀和对比,

int tree[20] = { 0,1,1,9,12,3,11,4,34,5,11,10,40,11,29,12,132,13,29 };
int lowbit(int x)
{
	return x & (-x);//涉及负数的补码和正数补码的计算,这里不在展开
}
int s(int x)
{
	int ans = 0;
	while (x)
	{
		ans += tree[x];
		x -= lowbit(x);
	}
	return ans;
}
int main()
{
	int arr[19] = { 0,1,0,9,2,3,8,4,7,5,6,10,19,11,18,12,17,13,16 };
	int sum[20]={0};
	for (int i = 1; i <= 18; i++)
	{
		sum[i] = sum[i - 1] + arr[i];
		printf("%d  %d\n", sum[i],s(i));
	}
	return 0;
}

这样一来,我们求解数据量大的数组的前缀和的时候,就可以只进行十几次加法就可以实现,大大提高了时间效率。

我们前面是给出了一个新的tree数组来代替原数组进行前缀和的求解,下面我们需要给出tree数组的实现方式:

我们可以列出几组值:

我们和树状数组来做比对:

 其中红色部分的数字表示树状数组的下标,这样我们的的树状数组就相对清晰了

 我们发现,tree数组的元素个数和  lowbit(x)竟然相等,换句话说,tree[x]的值是把a[x]和它前面的共lowbit(x)个数相加的结果,那这样我们通过原数组构造出tree数组就相很容易了,这里实现就不再给出,但是我们这里给出一种更加简便的实现方法:

tree[x]能够以二分的复杂度存储一个数列的数据,具体的,tree[x]中存储的是原数组区间

[x-lowbit(x)+1,x]中的每个数的和,

 单点值修改对tree的影响

如果说我们需要修改原数组中某个下标处的值,对于我们的tree数组来说,只需要改变由其算出的tree的部分的值即可,

 这里注意我们修改的值可能在原始下标的左边或者右边,我们需要判断:

int updata(int x, int d)
{
	int temp = x;
	while (temp > 0)
	{
		tree[temp] += d;
		temp -= lowbit(temp);
	}
	while (x <= N)
	{
		tree[x] += d;
		x += lowbit(x);
	}

}

3.2树状数组求解逆序对个数

前面我们说了那么多,为逆序对的求解做了理论基础,现在,我们正式的来看逆序对问题,逆序对问题的关键就是将原数组中的数字看做树状数组的下标,,比如我们的序列是{5,4,2,6,3,1},那么对应的我们的树状数组就是tree[5],tree[4],tree[2],tree[6],tree[3],tree[1]。

首先,什么是离散化?我们为什么要用到离散化?

第一,离散化是在数据差异较大的情况下并且只有数据间的相对大小比较重要时,我们就需要把这些差异较大的数变成相邻的,但是仍能表示相对大小关系的数,离散化(Discretization),把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。

接着,对于我们的逆序对的求法,我们有倒序和逆序的两种方法,这两种方法都有一个共同的原理,就是当前待加入的元素与前面已经加入的元素所构成的逆序对关系

倒序:

数字1,把tree[1]加1,因为我们将1加入到了树状数组的相对应的位置,所以我们就可以数一数在加入1之后其前面的的元素有几个,也就是sum(0)的个数,sum(0)中每一个元素都能与1做成逆序对,同理,我们求解其他元素也是如此;

正序:

对于数字5,我们把5加入树状数组,即tree[5]++,那么我们可以用当前已经加入元素的个数减去sum(5),注意,我们的数字5加入也算在内,因为我们算的是逆序对,所以只要是大于5的元素,在5加入之前加入,那么其就会和5组成一个逆序对。

下面是我们的代码:

#include<bits/stdc++.h>
using namespace std;

const int maxn = 1e5 + 1;
typedef long long ll;
ll tree[maxn],temp[maxn];
ll n;
ll ans = 0;
struct node {
	ll val, order;
	bool operator<(const node& b)
	{
		if (this->val == b.val)
			return this->order < b.order;//如果有元素相等,那么返回先读到的那一个
		return this->val < b.val;
	}
}a[maxn];//设为结构体是为了方便我们排序离散化
int lowbit(int x)
{
	return x & (-x);
}
int sum(int x)//求前缀和
{
	int ans = 0;
	while (x > 0)
	{
		ans += tree[x];
		x -= lowbit(x);
	}
	return ans;
}

void updata(int x, int v)//更新树状数组的值
{
	while (x <= n)
	{
		tree[x] += v;
		x += lowbit(x);
	}
}

int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d", &a[i].val);
		a[i].order = i;
	}
	sort(a + 1, a + n+1);//重载运算符实现,也可以用外部比较函数实现
	for (int i = 1; i <= n; i++)
		temp[a[i].order] = i;//离散化

	//正序
	for (int i = 1; i <= n; i++)
	{
		updata(temp[i], 1);
		ans += (i - sum(temp[i]));
	}

	//逆序
	/*for (int i = n; i >= 1; i--)
	{
		updata(temp[i], 1);
		ans += sum(temp[i] - 1);
	}*/
	printf("%lld\n", ans);

	return 0;
}

4.金句省身

        人生越是不顺的时候,就越要沉住气!认清形势,放弃幻想。少点以为,多点作为。你若盛开,蝴蝶自来;你若精彩,天自安排。

      不管命运如何对待你,一定要好好生活,努力提升自己,努力赚钱。等你熬过所有的苦,就会尝到所有的甜。道的规律就是先苦后甜,否极泰来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/427730.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python实现GWO智能灰狼优化算法优化XGBoost分类模型(XGBClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 灰狼优化算法(GWO)&#xff0c;由澳大利亚格里菲斯大学学者 Mirjalili 等人于2014年提出来的一种群智能…

天猫数据分析软件(天猫销售数据如何查询)

俗话说得好&#xff1a;“知己知彼&#xff0c;百战不殆”。当前&#xff0c;在天猫店铺运营中&#xff0c;大家也非常注重数据分析&#xff0c;由此能够了解到竞争对手及整个行业的销售数据及销售趋势。 一、具体来讲&#xff0c;做好天猫数据分析有哪些益处呢&#xff1f; 1、…

pytorch 数据类型

文章目录一、tensor如何表示字符串数据类型类型判断Dimension 0Dimension 1Dimension 2Dimension 3Dimension 4mixed二、创建Tensorimport from numpyimport from listuninitialized 未初始化set default typerand/rand_like, randintfulllinspaceindex切片三、维度变换总结一、…

假期怕剧荒?五一假期追剧人正确打开方式

马上就是五一假期了 趁着假期必须狠狠刷剧才是我们追剧人正确的打开方式&#xff01; 追剧人&#xff0c;追剧魂 追剧人就是快乐多&#xff01; 俗话说&#xff0c;吃饭不能没有饭碗&#xff0c;那咱们追剧也不能没有好用的追剧平台啊&#xff01; 之前因为要一次性追好几部…

React styled-components (一) —— 基本使用

https://github.com/styled-components/styled-components styled-components 基本使用介绍优点缺点安装引入使用基本用法样式嵌套介绍 styled-components 是一个针对 React 的 css in js 类库。 和所有同类型的类库一样&#xff0c;styled-components 通过 js 赋能解决了原生…

教你如何搭建物业-办公管理系统,demo可分享

1、简介 1.1、案例简介 本文将介绍&#xff0c;如何搭建物业-办公管理。 1.2、应用场景 该应用包含停车、收费、投诉、通知、访客等管理功能。 2、设置方法 2.1、表单搭建 1&#xff09;新建表单【业主信息】&#xff0c;字段设置如下&#xff1a; 名称类型名称类型类型…

30天学会《Streamlit》(3)

30学会《Streamlit》是一项编码挑战&#xff0c;旨在帮助您开始构建Streamlit应用程序。特别是&#xff0c;您将能够&#xff1a; 为构建Streamlit应用程序设置编码环境 构建您的第一个Streamlit应用程序 了解用于Streamlit应用程序的所有很棒的输入/输出小部件 第3天 - st.…

京东技术专家首推:微服务架构深度解析,GitHub星标120K

前言 微服务经过了长足的发展&#xff0c;在每个阶段所产生的信息都很多。在信息爆炸的当今&#xff0c;找到一本将信息梳理得井井有条的好书&#xff0c;是提升学习效率的最佳途径。 本书层次分明&#xff0c;分为原理篇、实践篇和进阶篇&#xff0c;适用于广泛的人群。理论…

电子商务客户保留策略:如何让买家回头

潜在客户转换是电子商务企业主的一大胜利。然而&#xff0c;在您成功将潜在客户转化为客户之后&#xff0c;可以而且应该采取无数步骤。购买后的有效营销应该是您的首要任务。 您可能知道&#xff0c;获取新客户的成本高于留住现有客户的成本。因此&#xff0c;制定客户保留策略…

毅硕科技携手Sentieon独家赞助第21届亚太生物信息学大会(APBC 2023)

APBC介绍 亚太生物信息学大会&#xff08;Asia Pacific Bioinformatics Conference, APBC&#xff09;是一年一度的行业国际盛会&#xff0c;汇聚区域间生物信息学领域的学者、研究人员和产业领导者&#xff0c;共同探讨生物信息学领域的研究进展、技术发展和应用创新。自2003…

分布式计算技术(下):Impala、Apache Flink、星环Slipstream

实时计算的发展历史只有十几年&#xff0c;它与基于数据库的计算模型有本质区别&#xff0c;实时计算是固定的计算任务加上流动的数据&#xff0c;而数据库大多是固定的数据和流动的计算任务&#xff0c;因此实时计算平台对数据抽象、延时性、容错性、数据语义等的要求与数据库…

银河麒麟V10操作系统之root密码重置

一、需求说明 从kingbase工程师那拷贝了一个已经安装了kingbase数据库环境的虚拟机&#xff0c;只有一个kingbase普通账户&#xff0c;root密码位置&#xff0c;且该账户未加入到sudo组中&#xff0c;无法执行新软件等的安装和部署。为了满足需要&#xff0c;我们需要重置root密…

测试5年,从纯手工测试到测试开发,我是怎么拿到腾讯25koffer的?

什么都做了&#xff0c;和什么都没做其实是一样的&#xff0c;走出“瞎忙活”的安乐窝&#xff0c;才是避开弯路的最佳路径。希望我的经历能帮助到有需要的朋友。 在测试行业已经混了5个年头了&#xff0c;以前经常听到开发对我说&#xff0c;天天的点点点有意思没&#xff1f…

4.11---Mybatis之#{}和${}的区别(复习版本)

1、在MyBatis 的映射配置文件中&#xff0c;动态传递参数有两种方式&#xff1a; 1.#{} 占位符 2.${} 拼接符 2、#{} 和 ${} 的区别 #{} 为参数占位符 ?&#xff0c;即sql 预编译 ${} 为字符串替换&#xff0c;即 sql 拼接 #{}&#xff1a;动态解析 -> 预编译 -> 执行 $…

[STM32F103C8T6] 重做51 基于iic的oled显示实验

51单片机没有硬件iic&#xff0c;我们是通过io口软件模拟iic时序 https://blog.csdn.net/weixin_63303786/article/details/128705478?spm1001.2014.3001.5501https://blog.csdn.net/weixin_63303786/article/details/128705478?spm1001.2014.3001.5501 而stm32有iic硬件&am…

Mysql(函数) 字符串截取、拆分, 逗号分割字符串当做 in 的条件

目录 引言: 数据库函数的总结(一) 1. mysql截取拆分 2. 逗号分割的字符串 作为in条件 -> 2.1 正常的效果应该是 ---> 2.1.1 错误: 3. 字符串合并(多条数据合并 用、分割) 引言: 数据库函数的总结(一) 1. 字符串截取、拆分 2. 逗号分割字符串当做 in 的条件 …

Mysql 学习(三)InnDB 存储引擎-页结构

数据页结构的大概 首先我们先来了解一下&#xff0c;InnoDB的存储单元是数据页的概念&#xff0c;页的大小一般是16KB&#xff0c;而InnoDB里面存放了很多不同目的 的数据页&#xff0c;比如存放Insert Buffer的信息页&#xff0c;Undo的日志页等等。但是这里我们主要讲解的是…

从C语言到C++(第一章_C++入门_上篇)C++学习介绍(命名空间和C++输入输出流)

目录 前言 1.认识C 2.C的重要性 3.如何学习C 4.编写hello world&#xff01; 5.关键字 6.命名空间&#xff08;namespace&#xff09; 6.1命名空间的定义 6.2命名空间里的内容 6.3命名空间重名问题 6.4命名空间展开问题 6.5匿名命名空间 7.C的输入与输出 7.1 输入和…

生产模块-报工自动产生返工订单(触发点-Trigger Point)

目录 应用场景 配置点-完整模式&#xff08;通过自定义状态触发&#xff09; 其他说明 应用场景 一般来说适合自动化程度高&#xff0c;生产集成了MES和质检的功能&#xff0c;工序报工时发生返工业务时&#xff0c;根据返工的指令&#xff0c;系统直接下达返工订单。例如&…

3年经验,面试测试岗只会功能测试开口要求18K,令我陷入沉思。

由于朋友临时有事&#xff0c; 所以今天我代替朋友进行一次面试&#xff0c;公司需要招聘一位自动化测试工程师&#xff0c;我以很认真负责的态度完成这个过程&#xff0c; 大概近30分钟。 主要是技术面试&#xff0c; 在近30分钟内&#xff0c; 我与被面试者是以交流学习的方式…