线段树求逆序对的详情理解

前言

看网上都说这个题目很经典,但是自己最初根本没办法将区间求和与求逆序对联系起来,思考了许久,此处进行记录并方便后来着理解。
注:本文的讲解建议配合线段树之逆序对问题代码来看。

题目

剑指 Offer 51. 数组中的逆序对

逆序对

在一个 n u m s nums nums序列中,如果 i < j i < j i<j n u m s [ i ] > n u m s [ j ] nums[i] > nums[j] nums[i]>nums[j],且称 n u m s [ i ] nums[i] nums[i] n u m s [ j ] nums[j] nums[j]是一个逆序对,然后求该nums序列中逆序对的个数。

分析如下

int k = 0;
for(int i = 0; i < N; ++i) {
    change(root, pos[i]);
    if(pos[i] == len) {   // 如果最大的数在最后,就没有比较的意义了。查询反而会使线段树结构出错
        continue;
    }
    k += query(root, pos[i] + 1, len + 1); // 插入后,立刻进行累加,在线的
}

先不考虑离散化,离散化仅仅是缩小线段树的大小以及减少随之产生的查询次数,我们直接将离散后的pos当做原先说明的nums序列。
我们按照顺序迭代序列 n u m num num,此时在线段树中插入当前数字 p o s [ i ] pos[i] pos[i](见代码第3行),随后便查询(pos[i] + 1, len + 1)的序列sum(见代码第7行)。
为什么查询(pos[i] + 1, len + 1)的序列sum呢?因为此时** (pos[i] + 1, len + 1)的序列sum值 是在之前已经进行插入的,且(pos[i] + 1, len + 1)保证了这里面的值肯定是大于当前 p o s [ i ] pos[i] pos[i]的。
(pos[i] + 1, len + 1)的序列sum值**满足两个条件:

  1. 出现的数字所对应的下标小于 i i i
  2. 且当前所有的值 p o s [ d ] > p o s [ i ] pos[d]>pos[i] pos[d]>pos[i]

这样一思考意味着什么?即我们将当前插入 i i i是为了后面判断是否有大于判断时所对应的数值,而query是为了统计大于 p o s [ i ] pos[i] pos[i]的数值的个数。
换句话说就是,把 p o s [ i ] pos[i] pos[i]理解为 n u m s [ j ] nums[j] nums[j]就好了,此时的 i i i理解为定义的 j j j

注:这样就将一个统计大于对应的关系转换为一个区间查询的问题。

扩展思考

此处我们是以 n u m s [ j ] nums[j] nums[j]去进行思考的,我们能不能从 n u m s [ i ] nums[i] nums[i]的角度去思考,也就是统计其序列右侧小于它元素的个数?
答案肯定是可以的,考虑时效性。
也就是我们需要逆序遍历(为了让 n u m s [ i ] nums[i] nums[i]右边的数先都出现,并在线段树中进行标记嘛)
还有查询的是 [ 1 , n u m s [ i ] − 1 ] [1,nums[i]-1] [1,nums[i]1](因为我们要找比当前 n u m s [ i ] nums[i] nums[i]更小的数嘛)

逆序对扩展

在数列中只要有 a i < a j > a k a_i<a_j>a_k ai<aj>ak,且 i < j < k i<j<k i<j<k,那么就称这是一个“好的”组合,给出任意个这个组合,求解“好的”组合的个数。
思路与逆序对一样,建树统计的代码也和逆序对的一样,区别在于统计方法上。

分析如下

// 处理逆序的,注意是反向循环的!!!!!!!
for(int i = N - 1; i >= 0; --i) {
    change(root, pos[i]);   // 将pos[i]的位置加一,顺便更新其父节点
    if(pos[i] + 1 == len + 1) {
        continue;
    }
    r[i] += query(root, pos[i] + 1, len + 1);
}
Node* root1{nullptr};
build(root1, 1, len + 1);
// 处理正序的
for(int i = 0; i < N; ++i) {
    change(root1, pos[i]);
    if(pos[i] <= 1) {
        continue;
    }
    l[i] += query(root1, 1, pos[i]);
}

通过对逆序对扩展思考的理解,我们可以明白。逆序对扩展我们可以理解为统计序列中 n u m s [ i ] nums[i] nums[i]左侧小于其自身的数的个数 l [ i ] l[i] l[i],以及右侧大于其自身数的个数 r [ i ] r[i] r[i]
结合代码我们可以看出:
逆序时统计的是 ( p o s [ i ] + 1 , l e n + 1 ) (pos[i] + 1, len + 1) (pos[i]+1,len+1),则意味着求的是 a j > a k a_j>a_k aj>ak,且 j < k j<k j<k这一段,找到所有数右侧大于它的部分,即 r [ i ] r[i] r[i]
正序时统计的是 ( 1 , p o s [ i ] ) (1, pos[i]) (1,pos[i]),则意味着求的是 a i < a j a_i<a_j ai<aj i < j i<j i<j这一段,找到所有数左侧小于它的部分,即 l [ i ] l[i] l[i]
然后结合这两个就可以了。

参考

线段树之逆序对问题

后记

笔者理解了很久,可能描述不是很清晰,可以随时来扰进行交流探讨。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值