树状数组学习笔记

零、前言

树状数组是用数组表示树的一种结构,在我看来是利用前缀和思想的进一步升级和改进,查询和修改的时间复杂度都是(nlogn) 

前置知识:lowbit(x)   获得一个数二进制最低为1及后面0构成的数

核心:lowbit(x) = x&(-x) 

原理:-x是一个数取反后再加一,将其与原数做和操作时,可消除掉除最低位1外其他的1 

树状数组原理 (结构图)

我们通过观察节点的二进制数,进一步发现,树状数组中节点x的父节点为x+lowbit(x),例如t[2]的父节点为t[4]=t[2+lowbit(2)]

一、树状数组解决的问题

  • 单点修改&&区间查询 

问题:给出一个长度为n的数组,完成以下操作

           1.将第x个数加上k

           2.输出区间[x,y]内每个数的和 

要实现单点修改即对a[1]+k,那么祖先节点t[1],t[2],t[4],t[8]都需要+k更新(因为t[]表示前缀和) ,代码操作如下:

void add(int x,int k)   //代表第x个数加k
{
	for(int i=x;i<=n;i++)
	{
		t[i]+=k;        //t数组代表tree
		i+=lowbit(i);
	}
}

对于区间查询,例sum[7],sum[7]=t[7]+t[6]+t[4],我们很神奇的发现6=7-lowbit(7),4=6-lowbit(6) 

 

int sum(int x)
{
	int summ=0;
	while(i)
	{
		sum+=t[i];  //t数组为全局变量
		i-=lowbit(i);
	}
	return sum;
}
  • 区间修改&&单点查询 

问题:已知一个数列,你需要进行下面两种操作

                  1.将某区间每一个数加上x

                  2.求出某一个数的值 

对于区间修改,则需要构造出差分数组b[],这样对于区间[L,R]+k,只需对差分数组进行add(L,k),add(R+1,-k)操作即可。  

对于单点查询操作,求出差分数组的前缀和即可,即a[x]=b[1[+b[2]+……+b[x] 

  • 区间修改&&区间查询 

建议使用线段树解决 

 

 

二、一丢丢的题目

 [SDOI2009] HH的项链 - 洛谷

 思路:离线+树状数组

1.先将查询右侧r排序(因为对于若干个询问的区间[l,r],如果他们的r都相等的话,那么项链中出现的同一个数字,一定是只关心出现在最右边的那一个的)

2.用t[i]表示从1到i不同贝壳的个数

3.需要另外一个数组来保证收集贝壳无重复,即删除在前面已经加入树状数组的数字然后把当前数字加入树状数组 如果这个数字前面没有出现那么直接加入树状数组

4.最后询问区间r到l-1即可

对于第三步操作:

引入重要的一个数组vis[]以及一个变量nextvis[i]表示第i种贝壳在目前询问到的区间中最后出现的位置(也就是最右端)。变量next指向的位置是尚未修改的区间的左端点,也就是已经修改区间右端点的后一个位置,方便定位未修改的区间。由于我们已经对询问的区间按右端点排好序,所以next的值在不同时刻单调递增,直至末尾。 

有了以上的信息之后就我们可以对区间进行修改了,主要维护两个操作: 

1. 如果某元素i在之前已经出现过,那么将其以前最右端位置的前缀和-1,相当于忽略之前的位置。再在现在i的位置把前缀和+1,并更新vis[i]到当前位置。

2.如果某元素i在之前没有出现,那么直接修改当前位置前缀和即可,并更新vis[i]。

 

 

各数组用途:(看上面的图)

a数组:存储原始贝壳类型

ask数组(结构体):记录询问区间及原始位置

t数组用途和上图一致,但是图中a数组是用vis数组和next进行表示的,图中的a数组在实际中例123->111 1231->0111,即贝壳种类不同时用1表示,重复时最右侧用1表示,其他用0表示,所以用sum(r)-sum(l-1)时可避免重复而相减。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=2e6+5;
int n,m;
int a[maxn],t[maxn],vis[maxn],ans[maxn];
struct node
{
	int l,r,pos;
};
node ask[maxn];
bool cmp(node pp,node ppp)
{return pp.r<ppp.r;}
int lowbit(int x) {return x&(-x);}
void add(int x,int k) //单点添加 
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
int sum(int p)   //区间查询 
{
	int res=0;
	while(p!=0)
	{
		res+=t[p];
		p-=lowbit(p);
	}
	return res;
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	scanf("%d",&a[i]);
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{scanf("%d%d",&ask[i].l,&ask[i].r);
	ask[i].pos=i;}        //存储初始位置
	sort(ask+1,ask+1+m,cmp);
	int next=1;
	for(int i=1;i<=m;i++)
	{
		for(int j=next;j<=ask[i].r;j++)
		{
			if(vis[a[j]])
			add(vis[a[j]],-1);
			add(j,1);
			vis[a[j]]=j;
		}
		next=ask[i].r+1;
		ans[ask[i].pos]=sum(ask[i].r)-sum(ask[i].l-1);
	}
	for(int i=1;i<=m;i++)
	printf("%d\n",ans[i]);
	return 0;
}

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值