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的事情,二维线段树麻烦的让人不想写。


  • 相关阅读:
    关于AJAX与form表单提交数据的格式
    MongoDB
    Redis
    在django中使用django_debug_toolbar进行日志记录
    python第三方库,你要的这里都有
    Django之用户认证auth模块
    Django中常用命令
    form表单钩子,局部钩子和全局钩子
    当我开始爱自己
    FOR YOU
  • 原文地址:https://www.cnblogs.com/tham/p/6827147.html
Copyright © 2011-2022 走看看