【递归 & 分治】压缩变换(使用区间树和二分法,递归统计指定区间内数字种类)
问题描述
试题链接 压缩变换
参考
解决方案
Note:主要利用区间树这个数据结构 和 递归操作,考查分治思想,这里使用二分法实现分治。
实现步骤:
- 初始化树:创建一个最大化的数组,满足最大
n
的要求,即数组长度>=2*n
(由于区间树是一个平衡二叉树,即n
个值构建的区间树节点个数为2*n - 1
;如果n
为5
,则区间树节点个数为:非叶子节点个数4
+ 叶子节点个数5
=9
) - 使用
map
数据结构,进行数字种类标记,key
为数字种类(即数值),value
为当前子序列中该数字的最近索引下标。 - 区间树更新操作:
map
在插入键值对时,需要对区间树进行更新,把索引号为value
值的叶子节点都置为1
。 - 区间树查询操作:在统计区间
[a,b]
中的数字种类时,可以采用二分法的分治思想,找到[a,b]
的多个子区间(比如[0,2]
可以拆分成[0,1]
和[2]
),再将子区间中的数值进行汇总,即为整个[a,b]
的数字种类个数。
这里的二分法可以通过平衡二叉树的特殊性质来实现:区间树当前节点为i
,则其左孩子为2 * i + 1
,右孩子为2 * i + 2
,父节点为(i - 1) / 2
。
Note:
- 区间树上的每个节点代表索引区间,叶子节点为单一区间(区间内只有一个值),非叶子节点为离散区间(区间内至少包含两个值)。
- 如果在本题的
map
中保留以arr[0]
为key
,0
为value
的键值对,则区间树的最左叶子节点值为1
;如果map
中保留以arr[2]
为key
,2
为value
的键值对,且arr[0] = arr[2]
,此时最左叶子节点值为0
。 - 如何将原数组中的索引号映射成为区间树上的索引号:这里把区间树看成是完全二叉树,则整棵区间树的最大节点数为
2
f
l
o
o
r
(
l
o
g
2
(
n
)
)
+
1
∗
2
−
1
2^{floor(log_2(n)) + 1} * 2 - 1
2floor(log2(n))+1∗2−1,最后一层的节点数为
2
f
l
o
o
r
(
l
o
g
2
(
n
)
)
+
1
2^{floor(log_2(n)) + 1}
2floor(log2(n))+1。
当原数组有5
个元素时,整棵区间树最后一层的节点数为8
个,前面几层的节点数为7
;- 因此原数组中第
0
个元素可以映射成区间树中的第8
个节点,索引号为7
,即(7 + 1 - 1)
; - 第
2
个元素映射成区间树中的第10
个节点,索引号为9
,即(7 + 3 - 1)
,由于第9
个节点在实际的区间树中并不存在(但可以存储),可以通过(9 - 1) / 2 = 4
获取第9
个节点的父节点(即索引号为4
的节点),这样原数组的第2个元素对应的区间树的索引为4
。
- 因此原数组中第
举个例子:
如果这个数字没有出现过,则将数字变换成它的相反数,并更新区间树,假设原始数组为arr = {1,2,1,2}
- 一开始输入
{1,2}
时,区间树tree
存储值如下:tree = {2,2,0,1,1,0,0}
,其中tree[3]
,tree[4]
,tree[5]
,tree[6]
为叶子节点,tree[0]
表示区间为[0,3]
的数字种类(2个
),tree[1]
表示区间为[0,1]
的数字种类(2个
),tree[2]
表示区间为[2,3]
的数字种类(0个
)。 - 接着输入第二个
1
时,由于1
之前已出现过,需要重新更新map
字典,把第一个key = 1
的索引号,从原来的value = 0
更新为value = 2
。- 需要先将上一次出现
1
的区间树的节点tree[3]
及其父节点进行-1
,更新后的区间树tree
存储值如下:tree = {1,1,0,0,1,0,0}
; - 接着再把第二个
1
添加进tree
中,tree
存储值如下:tree = {2,1,1,0,1,1,0}
- 需要先将上一次出现
- 接着输入第二个
2
,…
具体实现过程参考如下代码及其注释。
参考代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 5;
int a[maxn], tree[maxn * 4];
int n, maxpoint;
void init()
{
maxpoint = 1;
while (maxpoint < n) //假设当前的区间树为一棵完全二叉树,最后一层节点个数为maxpoint
maxpoint *= 2;
memset(tree, 0, sizeof(tree));
memset(a, 0, sizeof(a));
}
//
void update(int k, int addnum)
{
k += maxpoint - 1; //获取原数组中的索引映射到区间树上的索引(比如5个元素,maxpoint=8,原数组中第0个元素映射成区间树中的第7个节点(0 + 8 - 1),
//第2个元素映射成区间树中的第9个节点(2 + 8 - 1),由于第9个节点在实际中并不存在(但可以存储),可以通过(9 - 1) / 2 = 4获取它的父节点)
tree[k] += addnum;
while (k)
{
k = (k - 1) / 2; //获取区间树中k节点的父结点,直到根结点
tree[k] += addnum;
}
}
//查询[a,b]之间数字种类的个数
int query(int a, int b, int k, int l, int r)
{
if (a == b || (r <= a || l >= b)) //[a,b]不构成区间
return 0;
if (a <= l && r <= b) //[a,b]区间比[l,r]区间大,直接返回该区间节点(k) 上的值,表示[l,r]区间下的数字种类
return tree[k];
else
{
int mid = (l + r) / 2;
return query(a, b, (k * 2) + 1, l, mid) + query(a, b, (k + 1) * 2, mid, r); //通过2 * k + 1获取k的左孩子, 通过2 * k + 2获取k的右孩子
}
}
int main()
{
int temp;
map<int, int> mp;
cin >> n;
init();
for (int i = 0; i < n; i++)
{
cin >> temp;
if (mp.count(temp)) //map中已存在该数字
{
int pre = mp[temp]; //获得当前数字最近的下标索引pre
a[i] = query(pre + 1, i, 0, 0, maxpoint); //从区间树根结点k=0开始,获取[pre+1, i]之间数字种类的个数
update(pre, -1); //从区间树中删除掉pre索引节点的值
}
else //map中未出现该数字,则记为相反数
{
a[i] = -temp;
}
mp[temp] = i;
update(i, 1); //更新区间树中i
}
for (int i = 0; i < n; i++)
cout << a[i] << " ";
return 0;
}