用不同的姿势求逆序对(复习篇)


文章目录

  • 用不同的姿势求逆序对(复习篇)
    • 前言
    • 讲解
      • 归并排序
      • 树状数组
      • 线段树
    • 题目
    • 思路
    • 代码
      • 归并排序求逆序对
      • 树状数组求逆序对
      • 线段树求逆序对
      • 历届试题 小朋友排队解题代码

前言

最近忙于小项目,感觉很久没刷题了!
今天在蓝桥上做了一个逆序对的题目(小朋友排队 ),之前只用过归并排序求解这类问题。
现在以现在的知识水平,新加了两种解题姿势。
目前我比较喜欢以多种姿势来解一道题,因为可以顺带复习一些以前学过的知识。小声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掉冒泡~(几种排序算法性能的比较)。
确定可以用归并排序做,再确定如何统计每个小朋友的交换次数。
我们可以知道,一个小朋友 需要和前面比他大的后面比它小的人交换位置。
所以一个小朋友的交换次数 = 前面比他大的人数 + 后面比它小的人数
》很快可以延申到:

  • 这个小朋友 前面部分 和这个小朋友 构成的逆序对;
  • 这个小朋友后面 和这个小朋友构成的 正序对(把序列倒过来,也可以换成求逆序对)

为了统一处理,求解小朋友后面部分的人数时,把整个序列倒过了求解逆序对。
总之,我们要分两次求逆序对:

  1. 第一次顺着求解每个小朋友 和 他前面部分 构成的逆序对
  2. 第二次把序列倒过来,求解每个朋友 和他“前面”部分的逆序对

确定了是求解逆序对。我们就可以根据自己的知识储备,
采用多种姿势来解题了~
最下方贴了一份我采用树状数组的求解的本题代码,其他方法,可以根据本文中的参考代码,适当修改一下即可。

代码

归并排序求逆序对

之前写过,可以去我的 排序专栏 考考古,当时几乎只贴了一份稚嫩的代码。现在回想起来真不应该,后面再去认真整理…

我今天重新写了一遍,代码如下:

/*
归并排序求逆序对
*/
#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;
}

用不同的姿势求逆序对(复习篇)相关推荐

  1. szu 寒训第二天 树状数组 二维树状数组详解,以及树状数组扩展应用【求逆序对,以及动态第k小数】

    树状数组(Binary Index Tree) 树状数组可以解决可以转化为前缀和问题的问题 这是一类用以解决动态前缀和的问题 (有点像线段树简版) 1.对于 a1 + a2 + a3 + - + an ...

  2. Tido 习题-二叉树-树状数组求逆序对

    这里给大家提供一个全新的求逆序对的方法 是通过树状数组来实现的 题目描述   样例输入 Copy 5 2 3 1 5 4 样例输出 Copy 3 提示       #include<iostre ...

  3. codevs1688 求逆序对(权值线段树)

    1688 求逆序对  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold 题解  查看运行结果 题目描述 Description 给定一个序列a1,a2,-,an,如 ...

  4. 归并排序模板(附求逆序对)

    逆序对满足两个条件, i < j 和 ai > aj 归并可以求逆序对, 因为是按顺序加入, 所以右区间加入的时候, 左区间的数满足 i < j, 然后左边还没有加入的数肯定比当前的 ...

  5. P3531 [POI2012]LIT-Letters(求逆序对)

    题目传送门:https://www.luogu.com.cn/problem/P3531 题意 给出只包含大写字母的字符串 A 和字符串 B,每次可以交换字符串 A 两个相邻的字符,求 A 变成 B ...

  6. hust1347(归并排序求逆序对)

    题意: 给出一个数列,你要对这个数列的数字进行k次交换操作,使得交换之后的数列逆序对虽少. 思路: 求原数列的逆序对,再和k比就行了.求逆序对要用归并排序,因为树状数组开不下. 代码: #includ ...

  7. 树状数组求逆序对_区间和的个数(树状数组)

    327. 区间和的个数 给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper. 区间和 S(i, j) 表示在 nums 中,位置从 i ...

  8. CodeForces - 1417E XOR Inverse(字典树求逆序对+分治)

    题目链接:点击查看 题目大意:给出一个长度为 n 的数列 a,现在要求选出一个 x,将 a 中的每个元素都异或之后得到一个新的数列 b,要求数列 b 的逆序对最小,问最小的逆序对是多少,x 该如何选择 ...

  9. 信息竞赛进阶指南--归并排序求逆序对

    // 归并排序求逆序对 void merge(int l, int mid, int r) {// 合并a[l~mid]与a[mid+1~r]// a是待排序数组, b是临时数组, cnt是逆序对个数 ...

最新文章

  1. ipa解包打包工具_7步!教你轻松搞定ios重签ipa包
  2. C++关键字volatile
  3. 揭秘 Uber API 网关的架构,建议收藏!
  4. 【异常】 Ensure that config phoenix.schema.isNamespaceMappingEnabled is consistent on client and server.
  5. 扩展系统功能——装饰模式
  6. Blazor带我重玩前端(一)
  7. java sound 多线程同一音频文件_Java在编程语言中占据何等优势?
  8. kafka key的作用_kafka系列(kafka端到端原理分析)
  9. Titanium系列--对Window和View的一点理解
  10. 老年代的更新机制_如何理解Java GC机制
  11. 无法运行的愿意_旧电脑的福音:Win10精简版,运行比Win7更快,安装包不到3GB
  12. Rhino 7 for Mac(犀牛3D造型软件)
  13. 计算机网络冲突窗口,计算机网络基础试题及答案
  14. PHP云尚发卡,搭建个人发卡平台教程:云尚发卡平台搭建
  15. 国内科技巨头的区块链布局:BAT多领域布局,迅雷领跑主链
  16. c语言tab什么意思_C语言所有的知识点干货
  17. 鸿蒙系统可以微信吗,鸿蒙系统可以用微信吗?微信鸿蒙版本下载-游戏大玩家...
  18. 倍投技巧 - 凯利公式教你如何用正确的方法投资
  19. Laravel+layui实现的通用后台管理系统
  20. 用EXCEL做九九乘法表

热门文章

  1. java启动时执行_java怎么实现项目启动时执行指定方法
  2. gns3中两个路由器分别连接主机然后分析ip数据转发报文arp协议_关于TCP/IP,必知必会的十个问题!...
  3. 计算机网络-基本概念(11)【应用层】
  4. java高并发(九)线程封闭
  5. 推荐一位大佬,在腾讯工作十年
  6. 粗虚线和细虚线_建筑图纸的细线,粗线,虚线表示什么
  7. aaynctask控制多个下载进度_史上最强的进度图绘制十大注意事项!
  8. bat代码小游戏_程序员入职被27岁领导告诫:我被BAT录用过,是算法方面泰斗大哥...
  9. 计算机四级软件工程知识点,计算机四级考点
  10. idea使用dababase tools时导出db2建表语句,索引显示错误