zoukankan      html  css  js  c++  java
  • sss

    <更新提示>

    <第一次更新>


    <正文>

    树状数组

    单点修改 区间和查询

    众所周知,树状数组是一个可以维护区间前缀和的数据结构,普通的树状数组应该能够支持单点修改,区间查询的操作,其修改和查询的时间复杂度均为(O(log_2n))

    (Code:)

    inline int lowbit(int x){return x&(-x);}
    inline void modify(int pos,int x)
    {
        for (;pos<=n;pos+=lowbit(pos))
            c[pos] += x;
    }
    inline int query(int pos)
    {
        int res=0;
        for (;pos;pos-=lowbit(pos))
            res += c[pos];
        return res;
    }
    

    其基础部分我们不再讲解,接下来,我们将利用树状数组的简单变形来解决更多的问题。

    区间修改 单点查询

    其实,区间修改,单点查询也是可以使用树状数组实现的,我们考虑如下数组:

    [d_i=a_i-a_{i-1} ]

    这其实是差分数组,即原序列相邻两项的差,那么由定义可以得到:

    [a_i=sum_{j=1}^id_j ]

    这是一个前缀和,可以用树状数组求得。

    对于区间修改问题,我们就可以直接利用差分数组的性质,将(c_l)位置和(c_{r+1})进行对应的正负修改,在前缀和中,得到的体现就是区间修改。

    (Code:)

    inline int lowbit(int x){return x&(-x);}
    inline void modify(int pos,int x)
    {
        for (;pos<=n;pos+=lowbit(pos))
            c[pos] += x;
    }
    inline int query(int pos)
    {
        int res=0;
        for (;pos;pos-=lowbit(pos))
            res += c[pos];
        return res;
    }
    inline void solve(void)
    {
        scanf("%d%d",&n,&m);
        int last=0,val;
        for (int i=1;i<=n;i++)
        {
            scanf("%d",&val);
            modify(i,val-last);
            last=val;
        }
        int op,x,y,k;
        for (int i=1;i<=m;i++)
        {
            scanf("%d",&op);
            if (op==1)
            {
                scanf("%d%d%d",&x,&y,&k);
                modify(x,k);modify(y+1,-k);
            }
            if (op==2)
            {
                scanf("%d",&k);
                printf("%d
    ",query(k));
            }
        }
    }
    

    区间修改 区间和查询

    对于该问题,我们同样需要引入差分数组(d_i=a_i-a_{i-1})。但是,就这样还不足以完成区间和查询的操作,我们考虑展开求和式:

    [sum_{i=1}^n a_i=sum_{i=1}^n sum_{j=1}^id_j \ =sum_{i=1}^n(d_1+d_2+...+d_i) \=(d_1)+(d_1+d_2)+...+(d_1+d_2+...+d_n) \ =sum_{i=1}^n(n-i+1)d_i \=nsum_{i=1}^nd_i-sum_{i=1}^n(i-1)d_i ]

    (d_i'=(i-1)d_i),则

    [sum_{i=1}^n a_i=nsum_{i=1}^nd_i-sum_{i=1}^nd_i' ]

    用树状数组维护两个前缀和(d)(d'),就可以解决区间查询问题。对于区间修改,利用差分的方式对两个数组同时进行修改即可。

    (Code:)

    inline int lowbit(int x){return x&(-x);}
    inline long long query(int index,int pos)
    {
    	long long res=0LL;
    	if(index) {for(;pos;pos-=lowbit(pos))res+=d[pos];}
    	else {for(;pos;pos-=lowbit(pos))res+=d_[pos];}
    	return res;
    } 
    inline void modify(int index,int pos,long long delta)
    {
    	if(index) {for(;pos<=n;pos+=lowbit(pos))d[pos]+=delta;}
    	else {for(;pos<=n;pos+=lowbit(pos))d_[pos]+=delta;}
    }
    inline void init(void)
    {
    	for(int i=1;i<=n;i++)
    	{
    		modify(1,i,a[i]-a[i-1]);
    		modify(0,i,(i-1)*(a[i]-a[i-1]));
    	}
    }
    inline void solve(void)
    {
    	for(int i=1;i<=m;i++)
    	{
    		char order;int l,r;long long delta;
    		cin>>order; 
    		if(order=='C')
    		{
    			scanf("%d%d%lld",&l,&r,&delta);
    			modify(1,l,delta); modify(1,r+1,-delta);
    			modify(0,l,(l-1)*delta); modify(0,r+1,r*(-delta));
    		}
    		if(order=='Q')
    		{
    			scanf("%d%d",&l,&r);
    			printf("%lld
    ",(r*query(1,r)-query(0,r))-((l-1)*query(1,l-1)-query(0,l-1)));
    		}
    	}
    }
    

    二维树状数组

    对于一个二维矩阵内的部分和,其实直接利用树状数组进行简单拓展就可以实现了。

    修改如下定义:(c_{ij})代表以((i,j))为右下角,长度为(lowbit(i)),高度为(lowbit(j))的矩形中所有元素的和

    然后和树状数组一样直接维护就可以了,修改,查询的时间复杂度均为(O(log_2^2n))

    当然,求部分和也要用到二维前缀和求部分和的公式。

    (Code:)

    inline int lowbit(int x){return x&(-x);}
    inline void modify(int x,int y,int delta)
    {
    	for(int i=x;i<=n;i+=lowbit(i))
    		for(int j=y;j<=n;j+=lowbit(j))
    			c[i][j]+=delta;
    }
    inline int query(int x,int y)
    {
    	int res=0;
    	for(int i=x;i;i-=lowbit(i))
    		for(int j=y;j;j-=lowbit(j))
    			res+=c[i][j];
    	return res;
    }
    

    在二维树状数组的定义中,也可以拓展出区间修改,单点查询和区间修改,区间查询等内容,但由于其运用不多,实现价值不大,代码内容繁琐,故不详细讲解。

    类树状数组

    接下来,我们将讲解类树状数组,即利用树状数组的结构与思想来实现其他不同于前缀和的功能。

    单点修改 区间最值

    以维护区间最小值为例,还是先重新定义:(c_i)代表原序列区间([i-lowbit_i+1,i])中元素的最小值

    首先,我们需要能够根据原序列建立(c)数组,实现建树操作。

    观察树状数组的基本结构图,我们发现对于求解(c_i),其树结构上的每一个子节点刚好包括了完整的([1,i])区间。

    又因为其子节点的(c)值都是已经求得的,所以,我们可以使用类似于递推的方法来更新(c_i)的值,更新一个节点的时间复杂度为(O(log_2n)),则建树的时间复杂度为(O(nlog_2n))

    (Code:)

    inline void build(void)
    {
        for (int i=1;i<=n;i++)
        {
            c[i] = a[i];
            int sec = lowbit(i);
            for (int j=1;j<sec;j*=2)
                c[i] = max( c[i] , c[i-j] );
        }
    }
    

    那么对于修改操作,其实我们套用树状数组的修改框架,利用相同的方式进行修改即可。时间复杂度为(O(log_2^2n))

    (Code:)

    inline void modify(int pos,int x)
    {
        a[pos] = x;
        for (;pos<=n;pos+=lowbit(pos))
        {
            c[pos] = a[pos];
            int sec = lowbit(pos);
            for (int j=1;j<sec;j*=2)
                c[pos] = max( c[pos] , c[pos-j] );
        }
    }
    

    对于区间([l,r])的最小值查询,由于我们已经得到了(c)数组,所以直接利用(lowbit)函数向左缩小区间,不断使用(c)数组更新答案即可,时间复杂度为(O(log_2(r-l+1)))

    (Code:)

    inline int query(int l,int r)
    {
        int res = a[r];
        while (true)
        {
            res = max( res , a[r] );
            if (l==r)break;r--;
            for (;r-l>=lowbit(r);r-=lowbit(r))
                res = max( res , c[r] );
        }
        return res;
    }
    

    简单平衡树

    树状数组可以实现基础平衡树中的如下操作:

    (1.)增加元素
    (2.)删除元素
    (3.)查询排名
    (4.)查询第(k)小值
    (5.)查询前驱
    (6.)查询后继

    我们在值域上建立一个树状数组(c)(c_i)代表值域区间([i-lowbit_i+1,i])中元素的个数

    由定义,我们就可以用树状数组的方法实现增加函数和删除函数了,其时间复杂度为(O(log_2max{a_i}))

    (Code:)

    inline void insert(int val,int cnt)
    {
        for (;val<=Uplim;val+=lowbit(val))
            c[val] += cnt;
    }
    

    对于查询值(val)的排名,我们可以用树状数组方便地统计出值域([1,val-1])上元素的个数,进而得到元素(val)的排名,实际复杂度为(O(log_2val))

    (Code:)

    inline int rank(int val)
    {
        int res=1;val--;
        for (;val;val-=lowbit(val))
            res += c[val];
        return res;
    }
    

    对于查询排名为(rank)的数,我们需要使用倍增法解决。每一次我们使用(2)的整次方倍尝试扩展树状数组的值域下标,并累加元素个数,直到倍增的值恰巧符合要求即可。

    (Code:)

    inline int find(int rank)
    {
        int res=0,cnt=0;
        for (int i=30;i>=0;i--)
        {
            res += (1<<i);
            if ( res>Uplim || cnt+c[res]>=rank )//避免有多个值相同的元素
                res -= (1<<i);
            else cnt += c[res];
        }
        return ++res;
    }
    

    对于查询前驱和后继,可以直接利用以上两个函数实现。不过,两个函数分别有一些增减细节:

    (1.)对于查询一个值的前驱,只需要查询排名比他小(1)的数即可,所以要减(1)
    (2.)对于查询一个数的后继,只需要查询比它大(1)的数它的排名即可,这样可以防止有多个相同的数造成查询到同一个数,所以要加(1)

    (Code:)

    inline int prep(int val)
    {
        return find( rank(val)-1 );
    }
    inline int next(int val)
    {
        return find( rank(val+1) );
    }
    

    总结

    树状数组在维护前缀和方面有很好的表现,也能够拓展来维护最值,排名,第(k)大等。可以替代简单的线段树,平衡树,并且代码量极小,时间表现也不错,值得我们学习,可以在适时灵活运用。


    <后记>

  • 相关阅读:
    前端git开发分支各种场景管理
    RxJS Subject学习
    微信小程序登陆流程(20200322)
    vue依赖收集的策略
    eggjs2.x版本异步获取config配置方案
    dubbo连接过程
    计算机中对流的理解
    Egg.js运行环境配置场景
    Promise和Observable的映射
    eggjs异常捕获机制
  • 原文地址:https://www.cnblogs.com/Parsnip/p/10719789.html
Copyright © 2011-2022 走看看