前言
看网上都说这个题目很经典,但是自己最初根本没办法将区间求和与求逆序对联系起来,思考了许久,此处进行记录并方便后来着理解。
注:本文的讲解建议配合线段树之逆序对问题代码来看。
题目
逆序对
在一个 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值**满足两个条件:
- 出现的数字所对应的下标小于 i i i;
- 且当前所有的值 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]。
然后结合这两个就可以了。
参考
后记
笔者理解了很久,可能描述不是很清晰,可以随时来扰进行交流探讨。