树状数组

树状数组基础模板

树状数组,顾名思义就是用树的结构来存储数据,树状数组支持的操作有区间修改和区间查询(单点也是可以),具体原理不再解释。

一维树状数组模板

点击查看代码
const int N=;
int c[N];//树状数组
int n;
int a[N];//存储数组
int lowbit(int x)
{
	return x&-x;
}
void add(int x,int val)//a[x]+val
{
	while(x<=n)
	{
		c[x]+=val;
		x+=lowbit(x);
	}
}
int getsum(int x)
{
	int ans=0; 
	while(x)
	{
		ans+=c[x];
		x-=lowbit(x);
	}
	return ans;
}

首先是单点修改和区间查询的操作

点击查看代码
#include<bits/stdc++.h>
using namespace std;

int a[1000000],c[1000000];
int n,m;
int lowbit(int x)
{
	return x&-x; 
}
int getsum(int x)
{
	int s=0;
	while(x>0)
	{
		s+=c[x];
		x-=lowbit(x);
	}
	return s;
}
void add(int x,int key)//a[x]+key
{
	while(x<=n)
	{
		c[x]+=key;
		x+=lowbit(x);
	}
}
signed main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		add(i,a[i]);//修改(也可理解为初始化)
	}
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		int x,y;
		char ch[3];
		scanf("%s",ch);
		scanf("%d%d",&x,&y);
		if(ch[0]=='S')
		{
			printf("%d\n",getsum(y)-getsum(x-1));//区间求和
		}
		if(ch[0]=='A')
		{
			add(x,y);//单点修改操作
		}
	}
} 

区间修改和单点查询
这里需要用到一个小技巧,即用树状数组保存差值,这样区间修改的时候是修改整个区间,整个区间两数之间的差值不变,只需修改区间两边的差值,那单点查询的时候只需正常求和。
缘由:
c[1]=a[1]-a[0];//默认a[0]为0
c[2]=a[2]-a[1];
......
c[n]=a[n]-a[n-1];
因此要求a[2]只需c[1]+c[2]=a[2]-a[1]+a[1]-a[0]=a[2];
同理要求a[n]只需让c[1]+...+c[n],即区间求和。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
int sum[1000000],a[1000000],c1[1000000],c2[1000000];
int n,m;
int lowbit(int x)
{
	return x&-x; 
}
//简化版 
//int getsum(int x)
//{
//	int s=0;
//	int i=x;
//	while(i>0)
//	{
//		s+=(x+1)*c1[i]-c2[i];
//		i-=lowbit(i);
//	}
//	return s;
//}
int getsum(int x)
{
	int ans=0,ans1=0,ans2=0;
	int i=x;
	while(i>0)
	{
		ans1+=c1[i];
		i-=lowbit(i);
	}
	ans1*=(x+1); 
	i=x;
	while(i>0)
	{
		ans2+=c2[i];
		i-=lowbit(i);
	}
	ans=ans1-ans2;
	return ans;
}
void add(int x,int val)//a[x]+key
{
	int i=x;
	while(i<=n)
	{
		c1[i]+=val;
		c2[i]+=x*val;//c2[]数组存 c1[i]*i
		i+=lowbit(i);
	}
}
signed main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);
		add(i,a[i]-a[i-1]);
	}
	scanf("%lld",&m);
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		char ch[5];
		scanf("%s",ch);
		
		if(ch[0]=='S')
		{
			scanf("%lld%lld",&x,&y);
			printf("%lld\n",getsum(y)-getsum(x-1));
		}
		if(ch[0]=='A')
		{
			scanf("%lld%lld%lld",&x,&y,&z);
			add(x,z);
			add(y+1,-z);
		}
	}
} 


由于正常的单点查询求区间和会超时,这里根据公式推了一下 求区间[i,j] a[i]=c[i]+...+c[1]; a[i+1]=c[i+1]+c[i]+...+c[1]; ... a[j]=c[j]+...+c[1]; a[i]+a[i+1]+...+a[j]=1*c[j]+2*c[j-1]+...+(j-i+1)*c[i]+...+(j-i+1)*c[1];

到这里,树状数组的基本操作已经学会,区间查询,区间修改,单点查询,单点修改。
接下来看例题吧。

例题

HH的项链

看到这题,我首先想到了用flag[]数组标记,很明显这是必需的。
但随着代码进行,又会遇到一个问题:如1 2 1 2,如果按照正常标记后面两个就不会被标记,那遍历3到4时,结果为0。
顺着思路想,如果我们在用完一次后把它丢掉,重新遍历,这也就导致我们必须要采用一种有序遍历,从而让前面的更新不会影响后面的结果,同时,我们也要每次更新完数据,自动计算结果并保存,后面更新就不再计算(离线)。到这,这道题的大体思路明了。

这里了解到几个新东西。
离线
在第一次更新到这个状态时,计算结果,即使后面再更新数据,也不再改变结果。
在线
每次数据更新,重新计算结果。

接下来就是代码了

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+6;
struct lmy//多个变量排序时可控 
{
    int l,r;
    int id;
    bool operator <(const lmy a)
	{
        return r<a.r;//以右边界大小排序
		//遍历完后处理的数据不受影响 
    }
}q[N];
int a[N];
int mark[N];//mark[i]表示i上一次出现的位置
int c[N];
int ans[N];//保存结果
int n,m;
int lowbit(int x)
{
	return x&-x;
}
void add(int x,int t)
{
    while(x<=n)
	{
        c[x]+=t;
        x+=lowbit(x);
    }
}

int getsum(int x)
{
    int ans=0;
    while(x)
	{
        ans+=c[x];
        x-=lowbit(x);
    }
    return ans;
}


int main()
{
    ios::sync_with_stdio(0);
    cin>>n;
    for(int i=1;i<=n;i++)
	{
        cin>>a[i];
    }
    cin>>m;
    for(int i=1;i<=m;i++)
	{
        cin>>q[i].l>>q[i].r;
        q[i].id=i;
    }
    sort(q+1,q+1+m);
    int last=1;//上次查询的结尾+1
    for(int i=1;i<=m;i++)
	{
        for(int j=last;j<=q[i].r;j++)
		{
            if(mark[a[j]])
			//如果为0,它为第一个数 
			//不为0,则有上一个数 
			{
                add(mark[a[j]],-1);//上一个数置0 
            }
            add(j,1);
            mark[a[j]]=j;//标记为上一个数 
        }
        last=q[i].r+1;
        ans[q[i].id]=getsum(q[i].r)-getsum(q[i].l-1);
    }
    for(int i=1;i<=m;i++)
	{
        cout<<ans[i]<<endl;
    }
    return 0;
}

数星星

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=300000;
int n,c[N],s[N];
struct lmy
{
	int x,y;
}site[N];
int lowbit(int x)
{
	return x&-x;
}
void add(int x,int val)
{
	//因为y有序,只需维护比较x的大小,只要后面的x大它就大 
	while(x<=32001)//用来维护比x小的星数有多少个 
	{
		c[x]+=val;
		x+=lowbit(x);
	}
}
int getsum(int x)
{
	int ans=0;
	while(x)
	{
		ans+=c[x];
		x-=lowbit(x);
	}
	return ans;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&site[i].x,&site[i].y);
	}
	for(int i=1;i<=n;i++)
	{
		int a=site[i].x+1;
		int b=getsum(a);//求它前面有几颗星,即它为几级星 
		s[b]++;//它对应的星级数量+1 
		add(a,1); 
	}
	for(int i=0;i<n;i++)
	{
		printf("%d\n",s[i]);
	}
}


二维树状数组

模板

点击查看代码
int lowbit(int x)
{
	return x&-x;
}
void add(int x,int y,int val)
{
	for(int i=x;i<=n;i+=lowbit(i))
	{
		for(int j=y;j<=n;j+=lowbit(j))
		{
			c[i][j]+=val;
		}
	}
}
int getsum(int x,int y)
{
	int ans=0;
	for(int i=x;i>0;i-=lowbit(i))
	{
		for(int j=y;j>0;j-=lowbit(j))
		{
			ans+=c[i][j];
		}
	}
	return ans;
}

内容与一维大致相同,不再赘述。

看例题吧

移动电话

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int N=2000;
int n,c[N][N];
int op,x,y,z,l,b,r,t;
int lowbit(int x)
{
	return x&-x;
}
void add(int x,int y,int val)
{
	for(int i=x;i<=n;i+=lowbit(i))
	{
		for(int j=y;j<=n;j+=lowbit(j))
		{
			c[i][j]+=val;
		}
	}
}
int getsum(int x,int y)
{
	int ans=0;
	for(int i=x;i>0;i-=lowbit(i))
	{
		for(int j=y;j>0;j-=lowbit(j))
		{
			ans+=c[i][j];
		}
	}
	return ans;
}
signed main()
{
	while(1)
	{
		scanf("%d",&op);
		if(op==3)
		{
			exit(0);
		}
		if(op==0)
		{
			scanf("%d",&n);
		}
		if(op==1)
		{
			scanf("%d%d%d",&x,&y,&z);
			x++;
			y++;
			add(x,y,z);
		}
		if(op==2)
		{
			scanf("%d%d%d%d",&l,&b,&r,&t);
			l++;
			b++;
			r++;
			t++;
			printf("%d\n",getsum(r,t)-getsum(l-1,t)-getsum(r,b-1)+getsum(l-1,b-1));
		}
	}
	return 0;
}


这道题十分简单,不再多说,主要是关注一个小点,由于一维和二维中可能会出现0的情况,0的lowbit()为0,这导致在add()函数中x始终无法更新,在这时程序就会陷入死循环,于是我们在存入数据的时候就要++,来避免这种情况。

总结
树状数组是对数据存储和调用的一个优化,同时也是线段树的基础,难度适中,可放心食用。
到这了,拜拜

posted @ 2024-02-18 17:48  zhengchenxi  阅读(41)  评论(0)    收藏  举报