您的位置:首页 > 其它

树状数组小结

2014-01-09 20:51 176 查看
                                    树状数组小结

  寒假计划是两天的树状数组,一天RMQ+树状数组,四天的线段树,三天的图论。

  所以,趁着寒假多学点。*__*

  树状数组也可以像线段树一样好用。总共可以使用的类型有三个。

 (一)改点问线

 (二)改线问点

 (三)改线问线

  不过运用树状数组解体的一般就改点问线。因为,树状数组本身就是一个很难理解的算法。所以,不可能在套用其他复杂的题目。一般我们遇到了改线问点和改线问线的问题时候,就运用线段树,而不再是运用树状数组。

  下面就用高中生大牛MATO IS NO.1的博客内容,介绍以上三点吧。然后,在重点讲解常用的改点问线问题。

树状数组在区间求和问题上有大用,其三种复杂度都比线段树要低很多……有关区间求和的问题主要有以下三个模型(以下设A[1..N]为一个长为N的序列,初始值为全0):

(1)“改点求段”型,即对于序列A有以下操作:

【1】修改操作:将A[x]的值加上c;

【2】求和操作:求此时A[l..r]的和。

这是最容易的模型,不需要任何辅助数组。树状数组中从x开始不断减lowbit(x)(即x&(-x))可以得到整个[1..x]的和,而从x开始不断加lowbit(x)则可以得到x的所有前趋。代码:

void ADD(int x, int c)
{
for (int i=x; i<=n; i+=i&(-i)) a[i] += c;
}
int SUM(int x)
{
int s = 0;
for (int i=x; i>0; i-=i&(-i)) s += a[i];
return s;
}


操作【1】:ADD(x, c);

操作【2】:SUM(r)-SUM(l-1)。

(2)“改段求点”型,即对于序列A有以下操作:

【1】修改操作:将A[l..r]之间的全部元素值加上c;

【2】求和操作:求此时A[x]的值。

这个模型中需要设置一个辅助数组B:B[i]表示A[1..i]到目前为止共被整体加了多少(或者可以说成,到目前为止的所有ADD(i, c)操作中c的总和)。

则可以发现,对于之前的所有ADD(x, c)操作,当且仅当x>=i时,该操作会对A[i]的值造成影响(将A[i]加上c),又由于初始A[i]=0,所以有A[i] = B[i..N]之和!而ADD(i, c)(将A[1..i]整体加上c),将B[i]加上c即可——只要对B数组进行操作就行了。

这样就把该模型转化成了“改点求段”型,只是有一点不同的是,SUM(x)不是求B[1..x]的和而是求B[x..N]的和,此时只需把ADD和SUM中的增减次序对调即可(模型1中是ADD加SUM减,这里是ADD减SUM加)。

代码 :

void ADD(int x, int c)
{
for (int i=x; i>0; i-=i&(-i)) b[i] += c;
}
int SUM(int x)
{
int s = 0;
for (int i=x; i<=n; i+=i&(-i)) s += b[i];
return s;
}


操作【1】:ADD(l-1, -c); ADD(r, c);

操作【2】:SUM(x)。

(3)“改段求段”型,即对于序列A有以下操作:

【1】修改操作:将A[l..r]之间的全部元素值加上c;

【2】求和操作:求此时A[l..r]的和。这是最复杂的模型,需要两个辅助数组:B[i]表示A[1..i]到目前为止共被整体加了多少(和模型2中的一样),C[i]表示A[1..i]到目前为止共被整体加了多少的总和(或者说,C[i]=B[i]*i)。对于ADD(x, c),只要将B[x]加上c,同时C[x]加上c*x即可(根据C[x]和B[x]间的关系可得);而ADD(x, c)操作是这样影响A[1..i]的和的:若x<i,则会将A[1..i]的和加上x*c,否则(x>=i)会将A[1..i]的和加上i*c。也就是,A[1..i]之和
= B[i..N]之和 * i + C[1..i-1]之和。这样对于B和C两个数组而言就变成了“改点求段”(不过B是求后缀和而C是求前缀和)另外,该模型中需要特别注意越界问题,即x=0时不能执行SUM_B操作和ADD_C操作!

代码:

void ADD_B(int x, int c)
{
for (int i=x; i>0; i-=i&(-i)) B[i] += c;
}
void ADD_C(int x, int c)
{
for (int i=x; i<=n; i+=i&(-i)) C[i] += x * c;
}
int SUM_B(int x)
{
int s = 0;
for (int i=x; i<=n; i+=i&(-i)) s += B[i];
return s;
}
int SUM_C(int x)
{
int s = 0;
for (int i=x; i>0; i-=i&(-i)) s += C[i];
return s;
}
inline int SUM(int x)
{
if (x) return SUM_B(x) * x + SUM_C(x - 1); else return 0;
}


操作【1】:

ADD_B(r, c); ADD_C(r, c);

if (l > 1) {ADD_B(l - 1, -c); ADD_C(l - 1, -c);}

操作【2】:SUM(r) - SUM(l - 1)。

 树状数组一般都要用到离散化 。而什么事离散化呢?下面就给出离散化的概念.

什么是离散化?

--->排序后处理/对坐标的近似处理

--->基本思想:只考虑需要用的值。

--->作用:有效的降低时间复杂度和提高空间使用率。

先来一道简单的树状数组入门题吧:

HDU 2492 Ping Pong

题目链接:Click Here~

题目分析:

   题目描述为,在一个街道上有N个人,每个人都有一个技能值。且每个人的位置是一个固定的值。现在有一个游戏,游戏规则是挑出三个人,一个人当裁判,另外两个人比赛乒乓球。而这裁判是有条件限制的。必须在满足技能值和位置值都在另外的两个人之间。问:有多少种这种组合方式。

思路解析:

   我们可以在不考虑超时的前提下,试想一下。如果,我们要确定一个a[i]以他为裁判,会有多少种组合方式呢?我们要如何去确定?显然,只要我们找到他前面人的技能比他小的有多少和后面比他大的是多少是不是就很容易解决该问题了?结论很显然。那么我们假设前面技能值比他小的人有x1个,则另一面就说明前面的人技能值比他大的有

y1 = i-x1-1。同理,假设后面技能值比他小的人有x2个,则后面技能值比他大的有y2 = n-x2;由乘法原理和加法原理我们可以得到以a[i]为裁判的人的组合方式是 sum = x1*y2 + y1*x2(前小后大+前大后小)。

  但是这么做显然是超时的,那么要怎么去找出x1,x2,y1,y2呢?我们就会想到了树状数组。因为树状数组的另一种理解就是找出最小值最大值。

  之后运用树状数组模板就可以很容易的解决本道题了。就是要两次调用,因为,要分别来求前后的最小值。

具体实现看如下代码:

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
using namespace std;

typedef __int64 LL;
const int N = 1e5 + 5;
int n,a
,c
,d
,x
;

static inline int lowbit(int t)
{
return(t&-t);
}
void Update(int i,int num)
{
for(;i <= N;i += lowbit(i)) x[i] += num;
}
int GetSum(int i)
{
int s = 0;
for(;i > 0;i -= lowbit(i)) s += x[i];
return s;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
memset(x,0,sizeof(x));
for(int i = 1;i <= n;++i)
{
scanf("%d",&a[i]);
c[i] = GetSum(a[i]);
Update(a[i],1);
}
memset(x,0,sizeof(x));
for(int i = n;i >= 1;--i)
{
d[i] = GetSum(a[i]);
Update(a[i],1);
}
LL res = 0;
for(int i = 1;i <= n;++i)
{
res += c[i]*(n-d[i]-i)+d[i]*(i-c[i]-1);
}
printf("%I64d\n",res);
}
return 0;
}

树状数组的另外一种解释:

  树状数组的高效就在于: 与一般数组不同,一般数组都是下标不断加一来遍历的,而树状数组是不断加2^p来变化的,故效率为(logn)级别的。树状数组的最基本功能就是求比某点x小的点的个数(这里的比较是抽象的概念,可以使数的大小,坐标的大小,质量的大小等)。

  比如给定个数组a[5] = {2, 5, 3, 4, 1},求b[i] = 位置i左边小于等于a[i]的数的个数.如b[5] = {0, 1, 1, 2, 0},这是最正统的树状数组的应用,直接遍历遍数组,每个位置先求出Getsum(a[i]),然后再修改树状数组Update(a[i], 1)即可。当数的范围比较大时需要进行离散化,即先排个序,再重新编号。

  如a[] = {10000000, 10, 2000, 20, 300},那么离散化后a[] = {5, 1, 4, 2, 3}。

  但我们想个问题,如果要求b[i] = 位置i左边大于等于a[i]的数的个数呢?当然我们可以离散化时倒过来编号,但有没有更直接的方法呢?答案是有。

几乎所有教程上树状数组的三个函数都是那 样写的,但我们可以想想问啥修改就是x不断增加,求和就是x不断减少,我们是否可以反过来呢,答案是肯定的。

  先来一道北大的经典题目: POJ 2299 Ultra-QuickSort

  经典的归并求逆序数,但是也同样是一道经典的树状数组。

  题目就不解释了,很显然的意思。唯一要注意的就是如我开始所说的要注意离散化的过程。其实,一开始的时候我也一直没理解离散化的本质。后来才慢慢的理解了。就拿该题的例子解释吧。

   输入是:9 1 0 5 4

   而经过离散后本质上就会变成了5 2 1 4 3(从小到大排序),下面我就开始给出奇迹的结果,前述表示输入数,后为其逆序数的个数:

          离散化前:

          9-->0

          1-->1

          0-->2

          5-->1

          4-->2

          离散化后:

          5-->0

          2-->1

          1-->2

          4-->1

          3-->2

显然结果是正确的,因此,我们也证明了为什么离散化后的结果的可行性。而我们如何找回离散后5,2,1,4,3在离散化之前的位置呢?显然,根据原来记录的index就完全可以找回来原来的位置。

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
using namespace std;

typedef long long LL;
const int N = 5e5 + 5;
int n,num
,tree
;
struct Node
{
int index,val;
bool operator < (const Node x)const
{
return val < x.val;
}
}data
;

static inline int lowbit(int t)
{
return(t&-t);
}
void Updata(int x,int c)
{
for(;x > 0;x -= lowbit(x)) tree[x] += c;
}
LL GetSum(int x)
{
LL s = 0;
for(;x <= n;x += lowbit(x)) s += tree[x];
return s;
}
int main()
{
while(scanf("%d",&n),n)
{
memset(tree,0,sizeof(tree));
for(int i = 1;i <= n;i++)
{
scanf("%d",&data[i].val);
// data[i].val += 1;
data[i].index = i;
}
sort(data+1,data+1+n);
for(int i = 1;i <= n;i++)
{
num[data[i].index] = i;
}
LL res = 0;
for(int i = 1;i <= n;++i)
{
res += GetSum(num[i]);
Updata(num[i],1);
}
printf("%lld\n",res);
}
return 0;
}

夜深人静明天在写HDU3450(经典好题,加难题啊!!!!!!)


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: