树状数组的相关知识 及 求逆序对的运用


树状数组概念

树状数组又名二叉索引树,其查询与插入的复杂度都为 O(logN),其具有以下特征:

  1. 树状数组是一种实现了高效查询「前缀和」与「单点更新」操作的数据结构。
  2. 是求逆序对的经典做法。
  3. 不能解决数组有增加和修改的问题。

前缀和和区间和

既然树状数组是为了解决前缀和问题,那么我们首先要知道什么是前缀和?

要提前缀和就不得不提区间和,举个例子来说明两者:

ivec = {1, 2, 3, 4}
presum = {1, 3, 6, 10} // 前缀和
sumrange[1,3] = 9 // 下标1~3的区间和,2+3+4=9

由上可得,sumrange[beg, end] = presum[end] - presum[beg - 1] ,以例子来分析其合理性:
因为 sumrange[1,3] = 2+3+4presum[3] = 1+2+3+4 ,也就是说 sumrange[1,3] = presum[3] - ivec[0]ivec[0] = presum[0] = presum[1-1] ,因此, sumrange[1,3] = presum[3] + presum[0]

sumrange[beg, end] = presum[end] - presum[beg - 1] 有个隐患——访问 beg-1 的位置容易导致下标越界,如:sumrange[0,4] 。因此我们可以改变前缀和数组下标 i 保存的内容,当有访问越界风险时,前缀和数组下标 i 保存的是 [0, i] 的累加和;那么如果令 前缀和数组下标 i 保存 [0, i) 的累加和 ,令 presum[0] = 0 ,则可得到 sumrange[beg, end] = presum[end+1] - presum[beg] 。避免了下标越界的风险。
举例为证:

ivec = {1, 2, 3, 4}
presum = {0, 1, 3, 6, 10}
sumrange[1,3] = presum[3+1] - presum[1] = 10 - 1 = 9
sumrange[0,3] = presum[3+1] - presum[0] = 10 - 0 = 10

明晰了如何通过前缀和数组来算区间和,那么实际上树状数组实现的就是如何用区间和前缀和


树状数组原理

树状数组本质上是 空间换时间 的操作,保存 区间和 以求更快的算出 前缀和。以下图为例,红色数组为树状数组(称为C),蓝色数组为普通数组(称为A)。由于上面证明了从 1 开始存储可以避免访问越界的情况。另,也因为在计算前缀和时,终止条件通常为遇0。 因此 AC 都是从 1 开始存储元素。
在这里插入图片描述


区间和——单点更新

树状数组是如何保存 区间和 的呢?通过观察上图,我们可以得到如下规律:

C1 = A1 = sumrange[1]
C2 = C1 + A2 = A1 + A2 = sumrange[1, 2]
C3 = A3 = sumrange[3]
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4 = sumrange[1, 4]
C5 = A5 = sumrange[5]
C6 = C5 + A6 = A5 + A6 = sumrange[5, 6]
C7 = A7 = sumrange[7]
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 = sumrange[1, 8]

以上规律可以总结归纳为这样的特征:下标 i 存储了从 i 往前 2k (k为二进制表示的 i末尾0 的个数)个元素的区间和(出现次数),举例验证:

i = 8 = 1000, k = 3, 2^3 = 8, C8 是 A1~A8 的区间和(出现次数)
i = 6 = 110, k = 1, 2^1 = 2, C6 是 A5~A6 的区间和(出现次数)
i = 5 = 101, k = 0, 2^0 = 1, C5 是 A5 的区间和(出现次数)

怎样实现这样的存储方式呢?对于一个输入的数组A,我们每一次读取的过程,其实就是一个不断更新单点值的过程,一边读入 A[i] ,一边将 C[i] 涉及到的祖先节点值更新,完成输入后树状数组也就建立成功了。举个例子:

假设更新 A[2] = 8 ,那么管辖 A[2]C[2],C[4],C[8] 都要加上 8A2 的所有祖先节点),那么怎么找到所有的祖先节点呢?通过观察他们的二进制形式我们发现:

  • C2 = C10 ; C4 = C100 ; C8 = C1000

不明显,再观察一个一个例子,A[5] 的祖先节点有 C[5],C[6],C[8] ,观察其二进制形式:

  • C5 = C101 ; C6 = C110 ; C8 = C1000

也就是说,我们不断地对 二进制i末尾1 进行 +1 操作(寻找末尾1由Lowbit函数实现),直至到达 树状数组下标最大值 n

实现单点更新update(i, v):把下标 i 位置的数加上一个值 v

int Lowbit(int x){
	return x & -x;
}

void update(int i, int v){
	while(i<=n){ // n为树状数组.size()-1
		tree[i] += v;
		i += Lowbit(i);
	}
}

PS:在求逆序对的题目中,C[i] 保存某一区间元素出现的次数,便于快速计算前缀和。


前缀和——区间查询

如何通过 区间和 得到 前缀和 ?举例说明:

  • presum[8] = C88 = 1000
  • presum[7] = C7 + C6 + C47 = 1116 = 1104 = 100
  • presum[5] = C5 + C45 = 1014 = 100

对于 presum[i] 而言,结合着后面跟的二进制表示,不难发现,求 presum[i] 即是将 i 转换为 二进制 ,不断对 末尾的1 进行 -1 的操作(寻找末尾1由Lowbit函数实现),直到全部为0停止。

实现区间查询函数 query(i): 查询序列 [1⋯i] 区间的区间和,即 i 位置的前缀和。

PS:在求逆序对的题目中,i-1 位的前缀和 presum[i-1] 表示「有多少个数比 i 小」,也就代表了有多少个逆序对。

int query(int i){
	int res = 0;
	while(i > 0){
		res += tree[i];
		i -= Lowbit(i);
	}
	return res;
}

完整代码

class BIT {
    vector<int> tree;
    int len;
public:
    BIT(int n):len(n), tree(n){}

	BIT(vector<int>& nums>{
		len = nums.size();
		tree = vector<int>(nums.size()+1);
	} 
    
    static int Lowbit(int x){
        return x & -x;
    }
    
    int query(int x){ // 区间查询
        int res = 0;
        while(x){
            res += tree[x];
            x -= Lowbit(x);
        }
        return res;
    }
    
    void update(int x){ // 单点更新
        while(x<len){
            tree[x]++;
            x += Lowbit(x);
        }
    }
};

离散化

离散化常常用在通过树状数组求逆序对的题目中,连续化时,树状数组的长度为普通数组的最大元素。

比如题目给出一个数组 ivec = { 7, 4, 5, 100, 7, 5 } ,通过树状数组求逆序对的步骤如下:

  1. 创建长度为 100 的树状数组,下标从 1 开始。
  2. 倒序遍历 ivec ,通过区域求和得到 tree 数组中下标 ivec[i] 的前缀和,前缀和代表着比 ivec[i] 小的元素有几个。
  3. 更新单点,执行 tree[ivec[i]]++ 。举例:ivec[i]=7tree[7]++ ,代表 7 已被遍历过,出现了一次。

具体执行:

res = 0; // 存储逆序对个数
ivec = { 7, 4, 5, 100, 7, 5 }
                          ^
0 0 0 0 1 1 0 1 0 ……  0
1 2 3 4 5 6 7 8 9 …… 100
执行 res += query(4) 【已有的小于ivec[i]的元素才构成逆序对,因此从 ivec[i]-1 开始区间查询】得到 res = 0 + 0 = 0
单点更新,下标为 568…… 的 value 加 1

ivec = { 7, 4, 5, 100, 7, 5 }
                       ^
0 0 0 0 1 1 1 2 0 ……  0
1 2 3 4 5 6 7 8 9 …… 100
执行 res += query(6) 得到 res = 0 + 1 = 1
单点更新,下标为 78…… 的 value 加 1

ivec = { 7, 4, 5, 100, 7, 5 }
                   ^
0 0 0 0 1 1 1 2 0 ……  1
1 2 3 4 5 6 7 8 9 …… 100
执行 res += query(99) 得到 res = 1 + 1 = 2
单点更新,下标为 100 的 value 加 1

ivec = { 7, 4, 5, 100, 7, 5 }
               ^
0 0 0 0 2 2 1 3 0 ……  1
1 2 3 4 5 6 7 8 9 …… 100
执行 res += query(4) 得到 res = 2 + 0 = 2
单点更新,下标为 568…… 的 value 加 1

以此类推,很容易算出逆序对的数量。但是!可以发现1、2、3、6、8、9、…… 、98、99这些绝大多数位置都浪费了。因此我们需要对树状数组离散化,以节省内存空间。

实现树状数组离散化:

void Discretization(vector<int>& nums) {
	// nums 是 输入数组 的拷贝数组
    sort(nums.begin(), nums.end());
    nums.erase(unique(nums.begin(), nums.end()), nums.end()); //元素去重,下文有详细剖析
}

int getid(int x, vector<int> nums){
	return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1;
}

上述代码的作用简单来讲就是,通过 Discretization函数 将 nums 中的值保存到 a 中,并进行升序排列、元素去重的操作。以 ivec 为例,经过 Discretization函数 处理,得到

a = {4, 5, 7, 100}

而通过 getid函数 将 a 中元素映射为对应的树状数组下标,也就是 4 存在树状数组下标为 1 的地方,5 存在树状数组下标为 2 的地方……以此类推。举例:

ivec = { 7, 4, 5, 100, 7, 5 }
                          ^
0 1 0  1 // value
4 5 7 100 // 映射得到的逻辑下标
1 2 3  4// 物理下标
执行 res += query(getid(5)) 得到 res = 0
单点更新,下标为 getid(5)=2getid(100)=4 的 value 加 1

ivec = { 7, 4, 5, 100, 7, 5 }
                       ^
0 1 1  2 // value
4 5 7 100 // 映射得到的逻辑下标
1 2 3  4// 物理下标
执行 res += query(getid(7)) 得到 res = 1
单点更新,下标为 getid(7)=3getid(100)=4 的 value 加 1

下面是对 Discretization函数 的剖析。


sort函数

  • 接受两个迭代器,表示要排序的元素范围
  • 是利用元素类型的<运算符实现排序的,即默认升序

实例:
在这里插入图片描述


unique函数去重

  • 重排输入序列,将相邻的重复项“消除”;
  • “消除”实际上是把重复的元素都放在序列尾部,然后返回一个指向不重复范围末尾的迭代器。

实例:
在这里插入图片描述
从上图可知,unique返回的迭代器对应的vc下标为4,vc的大小并未改变,仍有10个元素,但顺序发生了变化,相邻的重复元素被不重复元素覆盖了, 原序列中的“1 2 2”被“2 3 4”覆盖,不重复元素出现在序列开始部分。


erase函数仅保留不重复元素

可以通过使用容器操作——erase删除从end_unique开始直至容器末尾的范围内的所有元素:
在这里插入图片描述

通过树状数组求逆序对

题源力扣:数组中的逆序对

代码实现:

class BIT {
    vector<int> tree;
    int st;
public:
    BIT(int n) :st(n), tree(n) {}

    BIT(vector<int>& nums) {
        st = nums.size();
        tree = vector<int>(nums.size());
        for (int i = 0; i < nums.size(); i++) {
            update(i, nums[i]);
        }
    }

    static int Lowbit(int x) {
        return x & -x;
    }

    int query(int x) { // 区间查询
        int res = 0;
        while (x) {
            res += tree[x];
            x -= Lowbit(x);
        }
        return res;
    }

    void update(int x, int v) { // 单点更新
        while (x < st) {
            tree[x] += v;
            x += Lowbit(x);
        }
    }

    void show() {
        for (int i : tree) {
            cout << i << " ";
        }
        cout << endl;
        cout << "  4 5 7 100" << endl;
    }
};
class Solution {
    void Discretization(vector<int>& tmp) {
        sort(tmp.begin(), tmp.end());
        tmp.erase(unique(tmp.begin(), tmp.end()), tmp.end()); //元素去重
    }

    int getid(int x, vector<int>& tmp) {
        return lower_bound(tmp.begin(), tmp.end(), x) - tmp.begin() + 1;
    }
public:
    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        vector<int> tmp = nums; // tmp作为离散化数组
        Discretization(tmp); // 排序去重
        BIT bit(tmp.size()+1);
        //bit.show();

        int res = 0; // 逆序对个数
        for (int i = n - 1; i >= 0; i--) {
            //cout << "v[i]: " << nums[i] << endl;
            int id = getid(nums[i], tmp); // 寻找映射
            res += bit.query(id - 1);
            // 因为计算的是value小于nums[i]元素的数目
            // 因此从前一位开始,下标id保存的是当前value=nums[i]的个数
            bit.update(id, 1); // nums[i]的个数+1
            //bit.show();
            //cout << "res: " << res << endl;
        }

        return res;
    }
};

int main() {
    vector<int> v = { 7, 4, 5, 100, 7, 5 };
    Solution s;
    /*int res = s.reversePairs(v);
    cout << res << endl;*/
    cout << s.reversePairs(v) << endl;
}
/*
7, 4, 5, 100, 7, 5
*/
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

·Jormungand

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

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

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

打赏作者

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

抵扣说明:

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

余额充值