树状数组
含义与原理
与部分位置和的一种方法,拥有前缀和快速取得结果的特点(O(2logn)),也拥有普通枚举快速修改的优势(O(logn))。
以数组的形式模仿树,一般用于维护前缀和,算法复杂度与线段树相同,但由于是数组,且线段树保存的是区间,面对过大的数据,无法解决,故树状数组能解决的问题线段树也能,线段树能解决的问题,树状数组不一定可以。
以这张图为例:A数组为原始数组 C数组为树状数组。
C【i】=A【i-2^k+1】+A【i-2 ^k+2】+…+A【i】 其中 k为i的二进制形式中第一个1前方0的个数。
例如:4——100 k=2 3——11 k=0
C【1】=A【1】
C【2】=A【1】+ A【2】
C【3】=A【3】
C【4】=A【1】+ A【2】+ A【3】+ A【4】
C【5】=A【5】
C【6】=A【5】+ A【6】
C【7】=A【7】
C【8】=A【1】+ A【2】+ A【3】+ A【4】+ A【5】+ A【6】+ A【7】 + A【8】
取前缀和:
sum【i】=C【i】+C【i-2 ^k1】+C【i-2 ^k1-2 ^k2】+…直到【】中的数为0停止
k为前一个数的第一个1前方0的个数
例如:sum【7】=C【7】+C【6】+C【4】
2^ k怎么求:2^ k=i&(-i)
原理
-i为i的反码+1:
i为奇数,补码不会进1最后一位与第一位都为1前面的位数都是相反的,故此时k=1
i为偶数,补码进位,直到第一个1的位置进位才停止
例如:6——110 -6——010 到第一个1的位置进位停止,异或运算后只会保存第一个1的位置,这不就是2 ^k了。
线段树实现,累加与改变数值
#include <stdio.h>
#include <stdlib.h>
#define N 5050
int a[N],c[N];
int lowbit(int x)
{
return x&(-x);
}
int c_num(int x)
{
int num=a[x];
int start=x-lowbit(x)+1;
while(start<x)
{
num+=a[start];
start++;
}
return num;
}
void creatTree(int n)
{
for(int i=1;i<=n;i++)
c[i]=c_num(i);
}
int getsum(int index)
{
int sum=0;
while(index>0)
{
sum+=c[index];
index-=lowbit(index);
}
return sum;
}
void updata(int i,int k,int n) //对a[i]增加k因此所有和要改变
{
while(i<=n)
{
c[i]+=k;
i+=lowbit(i);
}
}
int main()
{
int n;
printf("请输入要输入的数组数:");
scanf("%d",&n);
printf("请输入数组:\n");
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
creatTree(n);
printf("输出每个树结点的值:");
for(int i=1;i<=n;i++)
printf("%d ",c[i]);
printf("计算每一位的前缀和:");
for(int i=1;i<=n;i++)
printf("%d ",getsum(i));
putchar('\n');
int k,x;
printf("输入要改变的数值以及数组位置");
scanf("%d%d",&k,&x);
updata(k,x,n);
printf("新的前缀和:");
for(int i=1;i<=n;i++)
printf("%d ",getsum(i));
return 0;
}
代码改进
其实每次输入一个数组的数,就可以利用更新逐步算出树而不需要调用函数建立
#include <stdio.h>
#include <stdlib.h>
#define N 5050
int a[N],c[N];
int lowbit(int x)
{
return x&(-x);
}
void updata(int k,int i,int n)
{
while(i<=n)
{
c[i]+=k;
i+=lowbit(i);
}
}
int getsum(int i)
{
int sum=0;
while(i>0)
{
sum+=c[i];
i-=lowbit(i);
}
return sum;
}
int main()
{
int n;
printf("请输入要输入的数组数:");
scanf("%d",&n);
printf("请输入数组:\n");
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
updata(a[i],i,n);
}
printf("输出每个树结点的值:");
for(int i=1;i<=n;i++)
printf("%d ",c[i]);
printf("计算每一位的前缀和:");
for(int i=1;i<=n;i++)
printf("%d ",getsum(i));
putchar('\n');
int k,x;
printf("输入要改变的数值以及数组位置");
scanf("%d%d",&k,&x);
updata(k,x,n);
printf("新的前缀和:");
for(int i=1;i<=n;i++)
printf("%d ",getsum(i));
return 0;
}
树状数组练习
http://acm.hdu.edu.cn/showproblem.php?pid=1166
#include <stdio.h>
#include <stdlib.h>
#define N 50002
int lowbit(int x)
{
return x&(-x);
}
void update(int c[],int k,int i,int n)
{
while(i<=n)
{
c[i]+=k;
i+=lowbit(i);
}
}
int getsum(int c[],int i,int n)
{
int sum=0;
while(i>0)
{
sum+=c[i];
i-=lowbit(i);
}
return sum;
}
int main()
{
int T;
int t=1;
scanf("%d",&T);
while(t<=T)
{
printf("Case %d:\n",t++);
int a[N]={0},c[N]={0};
int n;
scanf("%d",&n);
for(int x=1;x<=n;x++)
{
scanf("%d",&a[x]);
update(c,a[x],x,n);
}
int i,j;
char choice[20];
while(scanf("%s",choice) && choice[0]!='E')
{
scanf("%d%d",&i,&j);
if(choice[0]=='Q')
{
int ans=getsum(c,j,n)-getsum(c,i-1,n);
printf("%d\n",ans);
}
else if(choice[0]=='A')
update(c,j,i,n);
else if(choice[0]=='S')
update(c,-j,i,n);
}
}
return 0;
}
线段树
普通的线段树
线段树的含义与用处
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
线段树可以用来维护修改求和区间上的最值,求和,算法复杂度和树状数组相同,但线段树的应用不仅限于RMQ问题和求和。
线段树的实现,修改和查询(RMQ问题)
线段树一般是一颗二叉树,我们将其当做完全二叉树看待,结点下标与完全二叉树性质相同,每个结点所存储的数据可根据要求而改变,我们这里解决的是RMQ问题,存储的值为该区间里的最大值(区间和只是改改操作)
最后一行中间空的6点也要算作,这就是我们需要把数组至少开到4*N防止内存溢出的原因。
树的建立
1、我们可以看到左孩子的结点下标为:k2 右孩子的结点为:k2|1 (k为父节点下标)
2、每个结点存储的值为该区间的最大值,就是其左右孩子结点的值的最大值。
3、最下层叶节点就是原数组本身
根据以上三点,再利用递归我们就可以实现建树
#define N 10000
#define Max(a,b) ((a)>(b)?(a):(b))
int a[N],c[N<<2];
void Pushup(int k)
{
c[k]=Max(c[k<<1],c[k<<1|1]);
}
void bulidTree(int k,int l,int n) //k是下标,l,r左右区间
{
if(l==n)
c[k] = a[l];
else
{
int m = (l+n)/2;
bulidTree(k<<1,l,m);
bulidTree(k<<1|1,m+1,n);
Pushup(k);
}
}
树的查询
假设:我们要找区间【4,8】的最大值,我们需要【4,5】(包含【4,4】,【5,5】),【6,8】。
也就是我们需要往下递归找出它的子区间(首个子区间,不需要再往下递归找子区间的子区间不然重复了)
int query(int L,int R,int l,int n,int k) //L R为查询的区间 l n为当前树结点的区间 k为结点下标
{
if(L<=l && n<=R) //若是子区间不用向下递归
return c[k];
else
{
int res=-(2<<28);
int m=(l+n)<<1;
if(m>=L)
res=Max(res,query(L,R,l,m,k<<1));
if(m<=R) //m+1不能等于R
res=Max(res,query(L,R,m+1,n,k<<1|1));
return res;
}
}
树的更新
我们改变原始数组中的一个数,树里的数组最高层对应的数组也要,并且其父节点也要改变,改变至根
改变原始数组【5】,那么树中【5,5】 【4,5】 【1,5】 【1,10】都需要进行更新
void updata(int p,int v,int l,int n,int k) //p为更新的下标 v为更新的值 l r为当前树结点的区间 k为结点的下标
{
if(l==n)
{
a[l]+=v;
c[k]+=v;
}
else
{
int m=l+((n-l)>>1);
if(m>=p)
updata(p,v,l,m,k<<1);
else
updata(p,v,m+1,n,k<<1|1);
Pushup(k);
}
}
测试
#include <stdio.h>
#include <stdlib.h>
#define N 10000
#define Max(a,b) ((a)>(b)?(a):(b))
int a[N],c[N<<2];
void Pushup(int k)
{
c[k]=Max(c[k<<1],c[k<<1|1]);
}
void bulidTree(int k,int l,int n) //k是下标,l,r左右区间
{
if(l==n)
c[k] = a[l];
else
{
int m = (l+n)/2;
bulidTree(k<<1,l,m);
bulidTree(k<<1|1,m+1,n);
Pushup(k);
}
}
int query(int L,int R,int l,int n,int k) //L R为查询的区间 l n为当前树结点的区间 k为结点下标
{
if(L<=l && n<=R) //若是子区间不用向下递归
return c[k];
else
{
int res=-(2<<28);
int m=(l+n)/2;
if(m>=L)
res=Max(res,query(L,R,l,m,k<<1));
if(m<R) //m+1不能等于R
res=Max(res,query(L,R,m+1,n,k<<1|1));
return res;
}
}
void updata(int p,int v,int l,int n,int k) //p为更新的下标 v为更新的值 l r为当前树结点的区间 k为结点的下标
{
if(l==n)
{
a[l]+=v;
c[k]+=v;
}
else
{
int m=l+((n-l)>>1);
if(m>=p)
updata(p,v,l,m,k<<1);
else
updata(p,v,m+1,n,k<<1|1);
Pushup(k);
}
}
int main()
{
int n;
printf("请输入数组个数:");
scanf("%d",&n);
printf("输入%d个数字:",n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
bulidTree(1,1,n);
putchar('\n');
printf("请输入查询的区间(0结束):");
int start,end;
while(scanf("%d",&start) && start!=0)
{
scanf("%d",&end);
printf("该区间最大值为:%d\n",query(start,end,1,n,1));
printf("请输入查询的区间(0结束):");
}
putchar('\n');
printf("请输入要改变的位置和数值:");
int p,v;
scanf("%d%d",&p,&v);
updata(p,v,1,n,1);
putchar('\n');
printf("再次进行查询\n");
printf("请输入查询的区间(0结束):");
while(scanf("%d",&start) && start!=0)
{
scanf("%d",&end);
printf("该区间最大值为:%d\n",query(start,end,1,n,1));
printf("请输入查询的区间(0结束):");
}
return 0;
}