【递归 & 分治】压缩变换(使用区间树和二分法,递归统计指定区间内数字种类)

【递归 & 分治】压缩变换(使用区间树和二分法,递归统计指定区间内数字种类)

问题描述

试题链接 压缩变换
参考

解决方案

Note:主要利用区间树这个数据结构 和 递归操作,考查分治思想,这里使用二分法实现分治。

实现步骤

  • 初始化树:创建一个最大化的数组,满足最大n的要求,即数组长度>=2*n(由于区间树是一个平衡二叉树,即n个值构建的区间树节点个数为2*n - 1;如果n5,则区间树节点个数为:非叶子节点个数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]key0value的键值对,则区间树的最左叶子节点值为1;如果map中保留以arr[2]key2value的键值对,且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))+121,最后一层的节点数为 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值