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

7 篇文章 0 订阅
5 篇文章 0 订阅

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


前言

最近忙于小项目,感觉很久没刷题了!
今天在蓝桥上做了一个逆序对的题目(小朋友排队 ),之前只用过归并排序求解这类问题。
现在以现在的知识水平,新加了两种解题姿势。
目前我比较喜欢以多种姿势来解一道题,因为可以顺带复习一些以前学过的知识。小声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 逆序对 --》 这个是裸题,直接写即可

  • 蓝桥: 试题 历届试题 小朋友排队 --》解题思路:

很明显只需要统计出每个小朋友的交换次数,然后根据等比数列求和求解最终的结果。
关键就是如何统计每个小朋友的交换次数,从题目的只允许相邻两个小朋友作交换。第一时间想到排序中的:冒泡排序,归并排序。根据题目数据范围和时限,很快可以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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leo Bliss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值