zoukankan      html  css  js  c++  java
  • 树状数组自学笔记

    树状数组自学笔记

    树状数组和线段树都是查询(O(logn))的数据结构。

    但是为什么很多人宁愿用树状数组而不是用线段树呢?

    因为树状数组写起来比线段树在一定程度上简单多了。(Author:理解看了这篇文章也OK)

    但是!树状数组维护的数据局限性要比线段树要大——这验证了一句话:

    越复杂的数据结构时间复杂度越小,但是越暴力的算法全能性越大。

    真理......

    Author:好我们先不扯淡了

    先来上一个例子:

    如果给你一个数列(n个数),然后让你求出区间([l,r])的和。

    分类讨论一下n:

    (nleq1000),很明显,暴力。

    (nleq 10000)继续吸氧暴力......

    (n leq 10^6)......线段树?

    但是,作为蒟蒻,我表示线段树RE好几次了......

    咋整啊......

    这时候,一种新的算法腾空出世:前缀和!

    一、树状数组基础1——前缀和

    前缀和,采取动态规划思想,通项公式:

    (n[i]=n[i-1]+a[i])

    然后有点数学常识就可以看得出来(O(1))访问区间和(??详见下注)。

    Tip:第n项+之前的和就是(a[n]),询问第n项即(a[n]-a[n-1])


    以上只不过是思想基础1,下面才是真正扯的树状数组。

    二、树状数组原理

    1.我们先来观察这样一个图:

    蓝的是左儿子,红的是右儿子(废话)

    然后看看线段树,我们发现线段树的节点是有重叠部分的——父节点各种重叠,导致空间存储各种爆炸。

    如果我们去掉这个烦人的重叠,发现好像空间复杂度降低了,时间复杂度不变。

    空间到底是减少了多少呢?

    手算一下:

    最上面是减少了(1over 2)

    然后是(1 over 4)

    (1over 8)......

    空间竟然一步步变成了O(n)......

    2.但是怎么访问呢???

    神奇的地方来了:

    再看一个图——

    我们发现凡是(2^n)都是一群单个1的,然后是,然后是......

    恍然大悟——

    原来我们只需要前缀和一样的存储方式

    前缀和一样的访问方式

    (戴望舒:OI一样的凄婉迷茫)

    然后我们要加上或者减去一个数的二进制中所在的位2^二进制中1所在的位

    ——这时候再引入一个东西

    3.lowbit

    求最低位权。

    num的位权就是num最低位的'1'和左边的'0'(如果有的话)组成的数字
    (Tip:位权不一定是二进制)

    根据计算机补码(位运算玄学操作),lowbit(n)=n & -n

    int lowbit(n){return n & -n;}
    

    于是我们得到如下操作:

    很明显:(C_n = A_{(n – 2^k + 1)} + ... + A_n)
    相对于维护,这样的计算省下多少力气...

    我们使用一个while循环来实现根节点到子节点的循环。

    使用树状数组只需要反复进行某些步骤就好了:

    • 1.初始化sum=0;
    • 2.如果(nleq 0),停止;否则就(sum+=c[n]);
    • 3.(n-=lowbit(n);)然后是第二步。
    int ask_(int k)//前k个数的和 
    {
        int ans=0;//初始化ans=0
        while(k>0)//不超过左界
        {
            ans+=t[k];//加上左边所有节点的数字
            k-=lowbit(k);
        }
        return ans;
    }
    int ask_seg(int l,int r)
    {
        return ask_(r)-ask_(l);
    }
    

    以上代码是求区间[1,k]的方式。

    Tip:(sum_{i=x}^y[x,y])等同于求$(sum_{i=1}yarray[i],iin[1,y])-(sum_{j=1}x array[j],jin[1,x]) $。

    如果是单点修改呢?

    void change_p(int pos,int num)
    {
    	while(i<=n)//0.在数组内部
    	{
    	    c[i]=c[i]+x;//1.修改
    	    i+=lowbit(i);//2.跳转到下一个与其有关的值
    	}
    }
    

    现在又面临一个严峻的问题:

    区间修改咋整啊?

    我们还是引进新的概念——

    三、树状数组基础2——数列差分

    差分简直是一个神奇的东西。。。

    首先我们介绍这个还得引入一个问题:

    如何快速地改变一个数列的值?朴素算法

    常数大了呢?

    差分!

    差分数组定义:

    我们求出一个类似dp求差分数组的公式:

    [dp[i]=c[i]-c[i-1] ]

    然后动动脑子(Author:没有nz),如果我们从dp[1]~dp[n]反向还原加起来就得到了原数组。

    更有意义的是如果我们仅仅对dp数组进行修改,仅仅需要修改两个数——

    假设修改区间([x,y]),就改dp[x]dp[y+1]

    为啥呢?如果我们dp[x]+=num,相当于对后面所有的元素都产生了+num的影响,然后再通过dp[y+1]改回来就完了。

    这跟树状数组有什么关系?

    类比一下:因为我们树状数组修改父亲节点会对子节点产生一个影响...

    对!修改前面的节点和后面的节点+1的地方!

    但是注意我们修改不是向左(根)修改,是向右(叶子)修改。

    如果想要单点修改和区间修改同时出现咋整?

    单点p不就是假的区间[p,p]嘛!

    我们开两种树状数组,一个是原序列的前缀和,另外一个就是前面说的差分数组。

    然后操作一下差分的树状数组pos1=p,pos2=p+1,以及前缀和树状数组就OK了。

    区间修改操作:

    void modify(int pos,int num)
    {//实现向右改数组的基本操作
        while (p<=n)//向右
        {
            sum[pos]+=num;
            pos+=lowbit(pos);
        }
    }
    void change_seg(int l,int r,int num)
    {
        modify(l,num);//差分左侧
        modify(r,-num);//差分右侧
    }
    

    然后好像还有最后一个问题——

    单点查询

    由于我们差分了树状数组,单点查询就变成了区间查询的操作。

    为啥?不是刚说了吗,前面所有的加起来不就是这个点的值嘛!

    操作:

    void ask_p(int pos)
    {
        int res=0;
        while (pos>0)//限定左边
        {
            res+=a[pos];//还原现场
            pos-=lowbit(pos);//向左循环
        }
        return res;
    }
    

    所以我们得到了如下基本操作:

    单点修改、单点查询、区间修改、区间查询。

    总结一下,整个学习过程分为两个大的板块:

    1. 前缀和思想:单点修改+区间查询
    2. 差分数列思想:区间修改+单点查询。

    然后我们甚至可以拿这个毒瘤数据结构解逆序对问题。

    Tip:
    离散化原先数组,然后食用本数据结构

    你学会了吗?

    End.

  • 相关阅读:
    调度器2—cat /proc/<pid>/sched内容分析
    调度器1—相关接口和命令行工具
    Java中的String类
    Java中的数组
    代码访问使用Let's Encrypt证书的网站提示certificate has expired的解决方法
    Linux环境Clion使用Protobuf
    PyTorch Dataloader读取时如何在进程之间传输数据
    6 安装Grafana 展示promethues数据
    5 Prometheus relabel配置
    4 PromQL
  • 原文地址:https://www.cnblogs.com/jelly123/p/10397835.html
Copyright © 2011-2022 走看看