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.

  • 相关阅读:
    jquery toggle(listenerOdd, listenerEven)
    struts quick start
    hdu 1518 Square (dfs)
    hdu 2544 最短路 (最短路径)
    hdu 1754 I Hate It (线段树)
    hdu 1856 More is better (并查集)
    hdu 1358 Period (KMP)
    hdu 2616 Kill the monster (DFS)
    hdu 2579 Dating with girls(2) (bfs)
    zoj 2110 Tempter of the Bone (dfs)
  • 原文地址:https://www.cnblogs.com/jelly123/p/10397835.html
Copyright © 2011-2022 走看看