树状数组与线段树

树状数组

含义与原理

与部分位置和的一种方法,拥有前缀和快速取得结果的特点(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;
}

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值