zoukankan      html  css  js  c++  java
  • 树状数组

    树状数组是一个优美小巧的数据结构,在很多时候可以代替线段树。一句话概括就是,凡是树状数组可以解决的问题,线段树都可以解决,反过来线段树可以解决的问题,树状数组不一定能解决。

    树状数组英文名称为Binary Index Tree,直译过来就是二进制索引树,我觉得二进制索引树更能说明其本质。树状数组的本质就是一种通过二进制位来维护一个序列前i和的数据结构。

    对于维护的序列A,定义C[i]=A[j+1]+...+A[i],其中ji的二进制表示中把最右边的1换成0的值。j的值可以通过lowbit求出,即i-lowbit(i)

    lowbit(a)2^(a的二进制表示末尾0的个数)。可以用下面式子求出

    lowbit(a)=a&(~a+1)
    

    或者根据补码的性质简化为

    lowbit(a)=a&(-a)
    

    修改方式如下

        void modify(int p,int delta)
        {
            while (p<=N)
            {
                C[p]+=delta;
                p+=lowbit(p);
            }
        }
    

    求前缀和如下

        int sum(int p)
        {
            int rs=0;
            while (p)
            {
                rs+=C[p];
                p-=lowbit(p);
            }
            return rs;
        }

    树状数组经常用来求一段区间的和,适用于该区间上的值是在不断变化的情景(不然就前缀和处理下就行了),常规数组的修改是O(1),区间查询是O(n),而树状数组的修改和查询都是O(lgn). 尽管线段树也能处理这种情况,并且功能要强大的多,但是树状数组编码上简单太多~
    感谢评论区的补充:树状数组往高维扩展时非常方便(加个循环的事情),而线段树则写起来非常麻烦

    树状数组需要一个辅助数组C,假设输入数组是A,则C[i]表示A[i-2^k+1]+…+A[i]总共2^k个数的和,其中k为i在二进制表示下从右到左连续0的个数。可以看一下这张图,基本就能明白了
    QQ截图20151207170241

    我们选择C[6]来解释下这个图,6的二进制表示为110,那么从右向左连续0的个数是1,所以C[6]表示2^1=2个连续数的和,也就是A[5]+A[6],再看C[8],8的二进制表示为1000,从右往左连续0的个数的3,所以C[8]表示2^3=8个连续数的和,也就是A[1]+…A[8],可以用x&(-x)开快速求得2^k

    现在来看下对数组中的某个数做修改,数组C会怎么变,假设我们对A[3]进行了修改,那么C[3]肯定是要改变的,因为C[3] = A[3],C[4]也是要改变的,C[4]=A[3]+A[4],然后改变的就是C[8],C[16]等等。。。可以看到,其实是和二进制相关的,故修改的复杂度是O(lgn)

    然后来看下对数组中某个区间做查询该怎么处理。假设查询q = (l,r)表示求区间l到r之间的数的和,用sum(x)表示下标从1到x之间的数据之和,则q = sum(r)-sum(l-1),那么如何快速的来求sum(x)呢?我们可以看一个例子,假设我们要求sum(7),sum(7) = C[7]+C[6]+C[4],首先C[7]只表示1个数的和A[7],处理了A[7]之后,我们要求前6个数的和,这时候C[6]表示两个数的和A[5]+A[6],接下来只需要处理前4个数据的和就行了,刚好C[4]表示前4个数据的和,可以看到并不是一个个的去加,而是利用二进制的思想,一段一段的加,复杂度为O(lgn)

    对具体如何实现如果有疑问可以看看下面的代码,实现起来非常简单明了

    hdu1166:http://acm.hdu.edu.cn/showproblem.php?pid=1166

    #include <iostream>
    #include <cstdio>
    #include <string>
    #include <string.h>
    using namespace std;
     
    const int N = 50010;
    int c[N];
    int n,t;
     
    int lowbit(int i)
    {
    	return i&(-i);
    }
     
    void add(int index, int addValue)
    {
    	while (index <= n)
    	{
    		c[index] += addValue;
    		index += lowbit(index);
    	}
    }
     
    int sum(int index)
    {
    	int ret = 0;
    	while (index >= 1)
    	{
    		ret += c[index];
    		index -= lowbit(index);
    	}
    	return ret;
    }
    int main()
    {
    	scanf("%d", &t);
    	for (int cas = 1; cas <= t;cas++)
    	{
    		printf("Case %d:
    ", cas);
    		memset(c, 0, sizeof c);
    		cin >> n;
    		for (int i = 1; i <= n; i++)
    		{
    			int x;
    			scanf("%d", &x);
    			add(i, x);
    		}
    		string query;
    		while (cin>>query)
    		{
    			if (query == "End")
    				break;
    			else if (query == "Add"||query=="Sub")
    			{
    				int index, value;
    				scanf("%d%d", &index, &value);
    				add(index, value*(query=="Add"?1:-1));
    			}else
    			{
    				int from, to;
    				scanf("%d%d", &from, &to);
    				printf("%d
    ", sum(to) - sum(from - 1));
    			}
    		}
    	}
    	return 0;
    }
    hdu1541: http://acm.hdu.edu.cn/showproblem.php?pid=1541
    #include <iostream>
    #include <cstdio>
    #include <string>
    #include <string.h>
    #include <algorithm>
    #include <map>
    using namespace std;
     
    const int N = 50010;
    int c[N];
    pair<int, int> input[N];
    int n,t;
     
    int lowbit(int i)
    {
    	return i&(-i);
    }
     
    int sum(int index)
    {
    	int ret = 0;
    	while (index > 0)
    	{
    		ret += c[index];
    		index -= lowbit(index);
    	}
    	return ret;
    }
     
    void  add(int index, int addValue)
    {
    	while (index < N)
    	{
    		c[index] += addValue;
    		index += lowbit(index);
    	}
    }
    int main()
    {
    	while (cin >> n)
    	{
    		memset(c, 0, sizeof c);
    		for (int i = 0; i < n; i++)
    		{
    			cin >> input[i].first >> input[i].second;
    			input[i].first++;
    			input[i].second++;
    		}
    		sort(input, input + n);
    		map<int, int> ret;
    		for (int i = 0; i < n; i++)
    		{
    			int y = input[i].second;
    			int cnt = sum(y);
    			ret[cnt]++;
    			add(y, 1);
    		}
    		for (int i = 0; i < n ; i++)
    			cout << ret[i] << endl;
    	}
    	return 0;
    }

    二维树状数组:add 的功能是改变元素(x, y),sum的功能则是求从元素(1, 1)开始到(x, y)的总和,同样,可以求出任意一个子矩阵内的所有元素之和,即sum(x2, y2) – sum(x1-1, y2) – sum(x2, y1-1) + sum(x1-1, y1-1) 代码也很简单,可以两个for循环,也可以两个while来写,最显而易见的应用就是快速求子矩阵的和

    poj1195:http://poj.org/problem?id=1195

    #include <iostream>
    #include <cstdio>
    #include <string>
    #include <string.h>
    #include <algorithm>
    #include <map>
    using namespace std;
     
    const int N = 1100;
    int c[N][N];
     
    int n,t,k;
     
    int lowbit(int i)
    {
    	return i&(-i);
    }
     
    void add(int x, int y,int v)
    {
    	while (x <= n)
    	{
    		int tempy = y;
    		while (tempy <=n )
    		{
    			c[x][tempy] += v;
    			tempy += lowbit(tempy);
    		}
    		x += lowbit(x);
    	}
    }
    int sum(int x, int y)
    {
    	int ret = 0;
    	while (x>0)
    	{
    		int tempy = y;
    		while (tempy > 0)
    		{
    			ret += c[x][tempy];
    			tempy -= lowbit(tempy);
    		}
    		x -= lowbit(x);
    	}
    	return ret;
    }
    int main()
    {
    	int q;
    	memset(c, 0, sizeof c);
    	while (cin >> q)
    	{
    		if (q == 0)
    		{
    			cin >> n;
    			n++;
    		}
    		else if (q == 1)
    		{
    			int x, y, v;
    			cin >> x >> y >> v;
    			x++, y++;
    			add(x, y, v);
    		}
    		else if (q == 2)
    		{
    			int x1, y1, x2, y2;
    			cin >> x1 >> y1 >> x2 >> y2;
    			x1++, x2++, y1++, y2++;
    			cout << sum(x2, y2) - sum(x2, y1 - 1) - sum(x1 - 1, y2) + sum(x1 - 1, y1 - 1) << endl;
    		}
    		else
    			break;
    	}
    	return 0;
    }

    poj2155:http://poj.org/problem?id=2155
    这题其实算是二维数组的反向思维题,假定要翻转区间(x1,y1)-(x2,y2)的值,可以在(x1,y1)(x1,y2+1)(x2+1,y1)(x2+1,y2+1)四个点分别加1,然后可以发现,求某个点的值,就是求sum(i,j)%2

    #include <iostream>
    #include <cstdio>
    #include <string>
    #include <string.h>
    #include <algorithm>
    #include <map>
    using namespace std;
     
    const int N = 1010;
    int c[N][N];
     
    int n,t,k;
     
    int lowbit(int i)
    {
    	return i&(-i);
    }
     
    void add(int x, int y)
    {
    	while (x < N)
    	{
    		int tempy = y;
    		while (tempy < N)
    		{
    			c[x][tempy] += 1;
    			tempy += lowbit(tempy);
    		}
    		x += lowbit(x);
    	}
    }
    int sum(int x, int y)
    {
    	int ret = 0;
    	while (x>0)
    	{
    		int tempy = y;
    		while (tempy > 0)
    		{
    			ret += c[x][tempy];
    			tempy -= lowbit(tempy);
    		}
    		x -= lowbit(x);
    	}
    	return ret;
    }
    int main()
    {
    	cin >> t;
    	while (t--)
    	{
    		memset(c, 0, sizeof c);
    		cin >> n >> k;
    		n++;
    		while (k--)
    		{
    			char query;
    			int x1,y1,x2,y2;
    			cin >> query >> x1 >> y1;
    			if (query == 'C')
    			{
    				cin >> x2 >> y2;
    				add(x2+1, y2+1);
    				add(x2+1, y1 );
    				add(x1, y2+1);
    				add(x1, y1);
    			}
    			else
    			{
    				cout << sum(x1, y1)%2 << endl;
    			}
    		}
    		cout << endl;
    	}
    	return 0;
    }

    树状数组另一个强大的地方在于往高维扩展非常方便,比如求子矩阵和会非常方便。而二维线段树则要麻烦得多。二维树状数组就是加个for的事情,二维线段树麻烦的让人不想写。


  • 相关阅读:
    ADF中遍历VO中的行数据(Iterator)
    程序中实现两个DataTable的Left Join效果(修改了,网上第二个DataTable为空,所处的异常)
    ArcGIS api for javascript——鼠标悬停时显示信息窗口
    ArcGIS api for javascript——查询,然后单击显示信息窗口
    ArcGIS api for javascript——查询,立刻打开信息窗口
    ArcGIS api for javascript——显示多个查询结果
    ArcGIS api for javascript——用图表显示查询结果
    ArcGIS api for javascript——查询没有地图的数据
    ArcGIS api for javascript——用第二个服务的范围设置地图范围
    ArcGIS api for javascript——显示地图属性
  • 原文地址:https://www.cnblogs.com/tham/p/6827147.html
Copyright © 2011-2022 走看看