用不同的姿势求逆序对(复习篇)
用不同的姿势求逆序对(复习篇)
文章目录
- 用不同的姿势求逆序对(复习篇)
- 前言
- 讲解
- 归并排序
- 树状数组
- 线段树
- 题目
- 思路
- 代码
- 归并排序求逆序对
- 树状数组求逆序对
- 线段树求逆序对
- 历届试题 小朋友排队解题代码
前言
最近忙于小项目,感觉很久没刷题了!
今天在蓝桥上做了一个逆序对的题目(小朋友排队 ),之前只用过归并排序求解这类问题。
现在以现在的知识水平,新加了两种解题姿势。
目前我比较喜欢以多种姿势来解一道题,因为可以顺带复习一些以前学过的知识。小声bb一句:做题不在于多,而在于精!
逆序对的概念很简单。当ai > aj,i < j,称(ai,aj)为一对逆序对。
对应的还有一个正序对,解法几乎是一致的。
直接问法(裸题):就是给定一个序列,直接让你求逆序对。
隐晦问法:需要根据特性来推出是求逆序对个数。如本文提到的小朋友排队问题。
常见的解法有:
- 归并排序
- 树状数组
- 线段树
(其他高级解法目前触及到了我的知识盲点…)
因为我是当作复习,所以本篇博客就粗略讲解上面这三种解法。
(树状数组和线段树解法是类似的,只是换成了不同的数据结构。)
讲解
归并排序
归并排序基本做法是,将一个序列不断二分,直到子序列不能再分了(只有一个元素)就进行两两合并。在合并过程中 优先原则(先取大的还是小的) 可以决定最终序列为升序还是降序。具体实现—》排序专栏
归并排序是如何来求解逆序对?
关键就在于两个子序列合并过程,
比如现在归并过程中(原则:优先取大的)有两个子序列待合并:
归并临时存储数组 tmp[] = {},逆序对数 ans = 0;
子序列1 : 5 3
子序列2: 4 2 1
5 > 3 -----》子序列1中的5 会大于 子序列2 中的4以及它后面的所有元素,
此时可以统计到子序列2和子序列1中的5构成的逆序对数:
(5,3)(5,2),(5,1),这个对数就是此时子序列2中的元素个数。
ans += 3;tmp = {5}.
继续合并,子序列1中的3 < 子序列2中的4:
tmp = {5,4}
子序列1中的3 > 子序列2中的2,
ans += 2
tmp = {5,4,3}
最后tmp中加上剩下子序列中没有比较的元素:
tmp = {5,4,3,2,1}
…
树状数组
树状数组主要用于解决区间修改(一般是单点修改,区间修改要引入差分),区间查询(一般是求区间和)的问题。
关于树状数组的入门题目
树状数组的结构(图片来源B站目前树状数组Top1讲解视频):
t[]数组用于存储 对应位置上的a[]数组元素以及在它的部分元素 的和。
基础性质:
- lowbit(x) = x&-x;
- t[x]维护的区间长度len = lowbit(x)
- t[x_root] = t[x+lowbit(x)]
- 单点更新:如果此时更新某个a[x],只需顺着向上更新覆盖了a[x]的区间即可。比如:a[2] += 1,则:t[2] += 1,t[4] += 1,t[8] += 1;
(ps : 2+lowbit(2) = 4,4+lowbit(4) = 8) - 区间求和:区间求和基于求解前缀和,这里的前缀和是单点更新的逆过程,比如求解前缀和sum[6],sum[6] = t[6] + t[4] ,(ps: 6 - lowbit(6) = 4)。
[l,r]区间和只需要用前缀和sum[r] - sum[l-1]即可得到。
总结了一大堆基础知识,那么如何用树状数组来求解逆序对?
朴素做法:
有一个数字序列,序列中的元素可能重复。现在要求这个序列的逆序对。
比如这样一个序列(7个元素):
__value: 5 4 6 8 9 4 5
index(id) :1 2 3 4 5 6 7
要求逆序对数,即求每个元素的前面有多少个比它大的元素。
现在我们来想象一下:把序列元素想象成小球,id是每个小球的唯一编号。现在 在一条路上(一条线段并标有数值)有9个坑位(小球value_max = 9,坑位可以更多,但是没有必要),
我们需要根据小球的value值将小球推到对应数值的坑里,而且每次只能推一个小球入坑(一个坑可以放很多个对应value值的小球)。现在,每次推入一个小球,就可以看一下,这条路上在即将要推入的坑后面有几个坑是已经有小球的(之前推进去了更后面的坑,即值比当前大,这样就构成逆序对了)。后面有小球的坑数 就是 可以和这个小球的value构成逆序对的数目。依次做下去就可以统计到这个序列的逆序对总数目。
这个例子推小球换成树状数组的单点更新,看后面的坑位几个有小球 换成树状数组区间求和即可。
这个是很朴素的做法,所以有比较大的局限性。
当序列value值特别大的时候(比如1e9),内存可能不够。
这时需要 将序列的值离散化,换成相对大小即可。
但是因为有value相同的元素,离散化处理起来还是有点小麻烦。
下面介绍一种更简单的方法:
抓住逆序对是 统计 在这个数前面并且比这个树大 的数 的数目 的特性
所以我们可以先对原序列从大到小排序,这里的排序规则需要注意一点,因为有相同的元素,所以需要增加一个规则:当元素value值相同时,需要让元素下标(id)大的优先排序。这是为什么?
我们先看如何来处理这个排序好的序列。
现在想象有一条线段,线段上标有一系列id数值。
我们根据排好的的序列的顺序,依次在线段上相应位置(线段上的id和元素id一一对应)插入元素所对应的id值(id就是原来序列的位置),
在插入前,我们可以在线段上看一下,在这个id前有多少个已经插入的元素。
这些前面的插入的元素一定时比当前元素value大的,因为我们从大到小预先排好了顺序。所以这些元素的个数 就是可以和 当前即将插入的id所对应的元素 构成逆序对的数目。
现在来看一下,为什么元素value相同时,id大的优先?
因为两个相同的元素不能构成逆序对,如果让id小的优先插入,
那么当后面value值相同(id更大)的元素插入,统计时(以当前id向前看)
就会多统计到逆序对的数目,因为把value值相同的也算进去了。
细品一下很容易领悟到。
我觉得这种做法很精妙,不需要离散化处理即可处理很大的数据。因为这种做法只跟元素个数有关了。但是需要先排序,相对朴素做法会慢一些。
这种做法放到树状数组上就是单点更新,求前缀和。
初始化t[] = {0},每插入一个元素,t[id] += 1,
统计就是统计id前 1的个数。
…
线段树
线段树也是主要解决区间修改,区间查询的问题。但是相比于树状数组,它在区间修改方面更方便,线段树也更强大些,有各种变形。
关于线段树的入门题目
线段树结构(图片来源百度图片):
线段树这里不多介绍了。
线段树做法和树状数组做法差不多。
在上面提到的 树状数组来求逆序对 的简单做法种 ,数据结构换成线段树,
统计时,把求前缀和换成求[1,id-1]的区间和即可。
具体实现请参考本文的代码~
题目
用于练习的题目:
- 洛谷: P1908 逆序对
- 蓝桥: 试题 历届试题 小朋友排队
思路
洛谷: P1908 逆序对 --》 这个是裸题,直接写即可
蓝桥: 试题 历届试题 小朋友排队 --》解题思路:
很明显只需要统计出每个小朋友的交换次数,然后根据等比数列求和求解最终的结果。
关键就是如何统计每个小朋友的交换次数,从题目的只允许相邻两个小朋友作交换。第一时间想到排序中的:冒泡排序,归并排序。根据题目数据范围和时限,很快可以pass掉冒泡~(几种排序算法性能的比较)。
确定可以用归并排序做,再确定如何统计每个小朋友的交换次数。
我们可以知道,一个小朋友 需要和前面比他大的和后面比它小的人交换位置。
所以一个小朋友的交换次数 = 前面比他大的人数 + 后面比它小的人数
》很快可以延申到:
- 这个小朋友 前面部分 和这个小朋友 构成的逆序对;
- 这个小朋友后面 和这个小朋友构成的 正序对(把序列倒过来,也可以换成求逆序对)
为了统一处理,求解小朋友后面部分的人数时,把整个序列倒过了求解逆序对。
总之,我们要分两次求逆序对:
- 第一次顺着求解每个小朋友 和 他前面部分 构成的逆序对
- 第二次把序列倒过来,求解每个朋友 和他“前面”部分的逆序对
确定了是求解逆序对。我们就可以根据自己的知识储备,
采用多种姿势来解题了~
最下方贴了一份我采用树状数组的求解的本题代码,其他方法,可以根据本文中的参考代码,适当修改一下即可。
代码
归并排序求逆序对
之前写过,可以去我的 排序专栏 考考古,当时几乎只贴了一份稚嫩的代码。现在回想起来真不应该,后面再去认真整理…
我今天重新写了一遍,代码如下:
/*
归并排序求逆序对
*/
#include <iostream>
using namespace std;
const int N = 5e5+5;
int a[N];
int b[N]; //temp
long long ans;
void merge_a(int l,int mid,int r)
{int i = l,j = mid + 1;int k = 0; //注意b数组最好从0开始存,后面不易出错while(i <= mid && j <= r){//从大到小排序if(a[i] > a[j]){ans += r - j + 1;b[k++] = a[i++];}else{b[k++] = a[j++];}}while(i <= mid) b[k++] = a[i++];while(j <= r) b[k++] = a[j++];for(int i = 0; i < k; i++){a[l+i] = b[i];}
}
void merge_sort(int l,int r)
{if(l < r){int mid = (l + r) >> 1;//递归,不断划分merge_sort(l,mid);merge_sort(mid+1,r);//合并merge_a(l,mid,r);}
}
int main()
{int n;cin>>n;for(int i = 1; i <= n; ++i){cin>>a[i];}merge_sort(1,n);cout<<ans<<endl;return 0;
}
树状数组求逆序对
/*
树状数组求逆序对
*/
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 5e5+5;
int n;
int t[N];
struct Node
{int v;int id;bool operator<(const Node& x)const{if(v == x.v){return id > x.id;}return v > x.v;}
}node[N];
inline int lowbit(int x)
{return x&-x;
}void add(int x,int v)
{for(int i = x; i <= n; i+=lowbit(i)){t[i] += v;}
}LL get_sum(int x)
{LL sum = 0;for(int i = x; i > 0; i-=lowbit(i)){sum += t[i];}return sum;
}
int main()
{cin>>n;for(int i = 1; i<= n; ++i){cin>>node[i].v;node[i].id = i;}sort(node+1,node+n+1);LL ans = 0;for(int i = 1; i<= n; ++i){ans += get_sum(node[i].id-1);add(node[i].id,1);}cout<<ans<<endl;return 0;
}
线段树求逆序对
/*
线段树求逆序对
*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
using namespace std;
const int N = 5e5;
int sum[N<<2];struct Node
{int v;int id;bool operator<(const Node &x)const{if(v == x.v) return id > x.id;return v > x.v;}
} node[N];//向上更新
inline void push_up(int rt)
{sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}// 建树和向下传递这里不需要...//单点更新
void update_point(int pos,int v,int rt,int l,int r)
{if(l == r){sum[rt] += v;return;}int mid = (l + r) >> 1;if(pos <= mid) update_point(pos,v,lson);else update_point(pos,v,rson);push_up(rt);
}
//区间查询
int query(int L,int R,int rt,int l,int r)
{if(L > R) return 0;if(L <= l && r <= R){return sum[rt];}int mid = (l + r) >> 1;int res = 0;if(L <= mid)res += query(L,R,lson);if(R > mid)res += query(L,R,rson);return res;
}
int main()
{int n;//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);scanf("%d",&n);for(int i = 1; i <= n; ++i){scanf("%d",&node[i].v);node[i].id = i;}sort(node+1,node+n+1);long long ans = 0;for(int i = 1; i <= n; ++i){ans += query(1,node[i].id-1,1,1,n);update_point(node[i].id,1,1,1,n);}printf("%lld\n",ans);return 0;
}
历届试题 小朋友排队解题代码
#include <iostream>
#include <cstring>
#include <algorithm>
const int N = 1e5+5;
typedef long long LL;
using namespace std;
struct Node
{int val;int id;int cnt;bool operator<(const Node &x)const{if(val == x.val){return id > x.id;}return val > x.val;}
} a[N];
LL b[N],t[N];
int n;
inline int lowbit(int x)
{return x&(-x);
}
void add(int x,int v)
{for(int i = x; i <= n; i+= lowbit(i)){t[i] += v;}return;
}
LL get_sum(int x)
{LL sum = 0;for(int i = x; i > 0; i-=lowbit(i)){sum += t[i];}return sum;
}
int main()
{cin>>n;for(int i = 1; i <= n; ++i){cin>>a[i].val;a[i].id = i;}sort(a+1,a+n+1);for(int i = 1; i <= n; ++i){b[a[i].id] = get_sum(a[i].id-1); //前面比它大的add(a[i].id,1);}memset(t,0,sizeof(t));for(int i = n,j = 0; i; --i,++j){b[a[i].id] += j - get_sum(a[i].id-1); //后面比它小的add(a[i].id,1);}LL ans = 0;for(int i = 1; i <= n; ++i){ans += (1 + b[i]) * b[i] / 2;}cout<<ans<<endl;return 0;
}
用不同的姿势求逆序对(复习篇)相关推荐
- szu 寒训第二天 树状数组 二维树状数组详解,以及树状数组扩展应用【求逆序对,以及动态第k小数】
树状数组(Binary Index Tree) 树状数组可以解决可以转化为前缀和问题的问题 这是一类用以解决动态前缀和的问题 (有点像线段树简版) 1.对于 a1 + a2 + a3 + - + an ...
- Tido 习题-二叉树-树状数组求逆序对
这里给大家提供一个全新的求逆序对的方法 是通过树状数组来实现的 题目描述 样例输入 Copy 5 2 3 1 5 4 样例输出 Copy 3 提示 #include<iostre ...
- codevs1688 求逆序对(权值线段树)
1688 求逆序对 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold 题解 查看运行结果 题目描述 Description 给定一个序列a1,a2,-,an,如 ...
- 归并排序模板(附求逆序对)
逆序对满足两个条件, i < j 和 ai > aj 归并可以求逆序对, 因为是按顺序加入, 所以右区间加入的时候, 左区间的数满足 i < j, 然后左边还没有加入的数肯定比当前的 ...
- P3531 [POI2012]LIT-Letters(求逆序对)
题目传送门:https://www.luogu.com.cn/problem/P3531 题意 给出只包含大写字母的字符串 A 和字符串 B,每次可以交换字符串 A 两个相邻的字符,求 A 变成 B ...
- hust1347(归并排序求逆序对)
题意: 给出一个数列,你要对这个数列的数字进行k次交换操作,使得交换之后的数列逆序对虽少. 思路: 求原数列的逆序对,再和k比就行了.求逆序对要用归并排序,因为树状数组开不下. 代码: #includ ...
- 树状数组求逆序对_区间和的个数(树状数组)
327. 区间和的个数 给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper. 区间和 S(i, j) 表示在 nums 中,位置从 i ...
- CodeForces - 1417E XOR Inverse(字典树求逆序对+分治)
题目链接:点击查看 题目大意:给出一个长度为 n 的数列 a,现在要求选出一个 x,将 a 中的每个元素都异或之后得到一个新的数列 b,要求数列 b 的逆序对最小,问最小的逆序对是多少,x 该如何选择 ...
- 信息竞赛进阶指南--归并排序求逆序对
// 归并排序求逆序对 void merge(int l, int mid, int r) {// 合并a[l~mid]与a[mid+1~r]// a是待排序数组, b是临时数组, cnt是逆序对个数 ...
最新文章
- ipa解包打包工具_7步!教你轻松搞定ios重签ipa包
- C++关键字volatile
- 揭秘 Uber API 网关的架构,建议收藏!
- 【异常】 Ensure that config phoenix.schema.isNamespaceMappingEnabled is consistent on client and server.
- 扩展系统功能——装饰模式
- Blazor带我重玩前端(一)
- java sound 多线程同一音频文件_Java在编程语言中占据何等优势?
- kafka key的作用_kafka系列(kafka端到端原理分析)
- Titanium系列--对Window和View的一点理解
- 老年代的更新机制_如何理解Java GC机制
- 无法运行的愿意_旧电脑的福音:Win10精简版,运行比Win7更快,安装包不到3GB
- Rhino 7 for Mac(犀牛3D造型软件)
- 计算机网络冲突窗口,计算机网络基础试题及答案
- PHP云尚发卡,搭建个人发卡平台教程:云尚发卡平台搭建
- 国内科技巨头的区块链布局:BAT多领域布局,迅雷领跑主链
- c语言tab什么意思_C语言所有的知识点干货
- 鸿蒙系统可以微信吗,鸿蒙系统可以用微信吗?微信鸿蒙版本下载-游戏大玩家...
- 倍投技巧 - 凯利公式教你如何用正确的方法投资
- Laravel+layui实现的通用后台管理系统
- 用EXCEL做九九乘法表
热门文章
- java启动时执行_java怎么实现项目启动时执行指定方法
- gns3中两个路由器分别连接主机然后分析ip数据转发报文arp协议_关于TCP/IP,必知必会的十个问题!...
- 计算机网络-基本概念(11)【应用层】
- java高并发(九)线程封闭
- 推荐一位大佬,在腾讯工作十年
- 粗虚线和细虚线_建筑图纸的细线,粗线,虚线表示什么
- aaynctask控制多个下载进度_史上最强的进度图绘制十大注意事项!
- bat代码小游戏_程序员入职被27岁领导告诫:我被BAT录用过,是算法方面泰斗大哥...
- 计算机四级软件工程知识点,计算机四级考点
- idea使用dababase tools时导出db2建表语句,索引显示错误