目录
我不会数学证明,但我可以学,会用就行,你知道我听了y总讲了一个小时证明的痛楚吗
树状数组的作用
- 单点增加(时间复杂度为O (logN) )
- 区间查询前缀和(时间复杂度为O (logN) )
- 求逆序对(但是不如归并排序)
- 扩展:差分+公式
相较于原数组a[N],单点增加的时间复杂度为O(1),但区间查询和的时间复杂度为O(N)
相较于前缀和数组pre[N],区间查询和的时间复杂度为O(1),但单点增加的复杂度为O(N)
因此我们选择了折中的办法就是 让这两个操作都为logN的复杂度
(1)树状数组的经典模板
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int a[N];
int tr[N];
int n;//点数
int lowbit(int x)
{
return x & -x;
}
void add(int x,int c)//在x的位置上加上常数c
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)//求x位置前的前缀和
{
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
while(1)
{
cout<<"第一次查询前缀和(1)"<<endl;
for(int i=1;i<=n;i++) cout<<sum(i)<<" ";
cout<<endl;
for(int i=1;i<=n;i++) add(i,1);//在每个位置上都加上1
cout<<"每个位置都加上1后第二次查询前缀和(2)"<<endl;
for(int i=1;i<=n;i++) cout<<sum(i)<<" ";//前缀和
cout<<endl;
cout<<"查询每个点的数值大小(3)"<<endl;
for(int i=1;i<=n;i++) cout<<sum(i)-sum(i-1)<<" ";//查询单点的数值大小
cout<<endl;
break;
}
system("pause");
return 0;
}
(2)关于记忆模板
- 首先先把这张图记住
- 对于add函数,因为树状数组维护的是一部分树的前缀和,那么因此,它只能影响到这个位置及其后面的树状数组,因为需要将后面的树状数组更新,那么以lowbit(i)为单位向后更新
- 对于sum函数,因为树状数组维护的是一部分树的前缀和,因此它向前查询,其中每一段的大小是lowbit(i),用res变量记录每一段树状数组的累计和,返回即可
- 如果还不理解的话,多写几遍,或者回去看证明= - =
楼兰图腾
核心思路:
- (乘法原理)我们可以知道,枚举中间的一个点,统计其左右两边满足条件的数量,那么左右两边的乘积结果就是 一个结果;举个例子:比如我们要求’V‘型的 点的个数,那么可以先预处理出 在第i个位置 ,其左边的点高于中间点的高度的个数,和右边的点高于中间点的高度的个数,然后相乘就是 枚举到的该点作为中间点的个数
- 那么现在的问题就是<在一个区间中 快速地求出 小于或大于这个点的高度的 个数>
- 注意此时因为目标是 数字的个数,而非数字的大小,这启发了我们对于一个位置上的值,它是 数字的个数更为合理;那么对于这个位置而言呢,注意 条件是 <小于或大于这个数>,也就是说数的大小其实是作为 键的,也就是索引
- 问题分析到这里已经有了些许眉目,接下来的细节请看代码+详细注释
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=2000010;
typedef long long ll;
int n;
int a[N],t[N];//t[i]表示树状数组i结点覆盖的范围和
//Lower[i]表示左边比第i个位置小的数的个数
//Greater[i]表示左边比第i个位置大的数的个数
int Lower[N],Greater[N];
//返回非负整数x在二进制表示下最低位1及其后面的0构成的数值
int lowbit(int x)
{
return x&-x;
}
//将序列中第x个数加上k
void add(int x,int k)//向右上方以lowbit(i) 为单位 在t数组中 区间加上k 维护
{
for(int i=x;i<=n;i+=lowbit(i)) t[i]+=k;
}
//查询序列前x个数的和
int ask(int x)//向左上方以lowbit(i) 为单位 查询
{
int sum=0;
for(int i=x;i;i-=lowbit(i)) sum+=t[i];
return sum;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
从左向右,依次统计每个位置左边比第i个数y小的数的个数、以及大的数的个数
for(int i=1;i<=n;i++)
{
int y=a[i];//第i个数
//在前面已加入树状数组的所有数种统计在区间[1,y-1]的数字 的出现次数
Lower[i]=ask(y-1);
//在前面已加入树状数组的所有数中统计在区间[y+1,n]的数字 的出现次数
Greater[i]=ask(n)-ask(y);
将y加入树状数组,即数字y出现1次
add(y,1);//注意这里的y是a[i],也就是数字的大小,因为是已经按位置进行枚举的了
//因此,不需要考虑 位置的先后顺序,而只用考虑数字的大小关系
}
//清空树状数组,从右往左统计每个位置右边比第i个数y小的数的个数、以及大的数的个数
memset(t,0,sizeof t);
ll resA=0,resV=0;
//从右往左统计
for(int i=n;i>=1;i--)
{
int y=a[i];
resA+=(ll)Lower[i]*ask(y-1);//向右查询 [1,y-1]的数字的 总个数
resV+=(ll)Greater[i]*(ask(n)-ask(y));//向右查询 [y+1,n]的数字的 总个数
add(y,1);//同理
}
printf("%lld %lld\n",resV,resA);
return 0;
}
一个简单的整数问题
- 与普通的 树状数组模板题不同的是:这题的操作是:[区间修改]+[单点查询]
- 而普通的 树状数组模板提是:[单点修改]+[区间查询]
- 单点查询其实并不困难:比如在普通的维护前缀和的树状数组中想要知道i点位置对应的大小,那么只需要sum(i)-sum(i-1)即可
- 难点是:区间修改;不管是 原数组的[区间修改],还是前缀和数组的[区间修改],它们所花费的时间复杂度都为O(N)
- 对于区间修改,不难想到 差分数组,差分是前缀和数组的逆运算; 对于[区间修改]:比如想要在[l,r]的范围内的所有数字都加上 常数C 那么就有 tr[l]+=c tr[r+1]-=c <
至于为什么这样写,我只能说回去看基础课> - 对于[单点查询]:想要求<i点位置对应的大小>,就是将i之前(包括i)的所有数组和求和,得到的结果就是就是 <i点位置对应的大小> ,<因此可以维护一个可以计算差分数组前缀和的树状数组> ,来使得O(N)的复杂度 降低到 O(logN)
Code:预处理(包括其本身的差分)
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
typedef long long ll;
const int N=100010;
int n,m;
int a[N];
ll tr[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
ll sum(int x)
{
ll res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]);//树上差分
while(m--)
{
char op[2];
int l,r,d;
scanf("%s%d",op,&l);
if(*op=='C')//区间修改
{
scanf("%d%d",&r,&d);
add(l,d);
add(r+1,-d);
}
else printf("%lld\n",sum(l));
//树状数组中的前缀和维护使得差分数组能更快地求出 1~i的差分数组的和
}
return 0;
}
Code:预处理:只记录变化区间的差分《算法竞赛进阶指南》
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int a[N];
int tr[N];//树上差分
int n,m;
int lowbit(int x)
{
return x & -x;
}
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
while(m--)
{
char op[2];
int l;
scanf("%s%d",op,&l);
if(*op=='C')//区间添加操作
{
int r,d;
scanf("%d%d",&r,&d);
add(l,d),add(r+1,-d);
}
else
{
printf("%d\n",sum(l)+a[l]);
}
}
return 0;
}
一个简单的整数问题2(困难!)
- 问题的核心变为了:[区间修改]+[区间求和]
- [区间修改]还是一样的思路,树上差分即可
- [区间求和]:注意是原数组的区间求和,那么就是在[l,r]范围内对所有的 a[l]~a[r]求和,在上一题中我们已知道了a[i]的求法是 sum(i),因此可以有下图的写法
y总的字有待提高....,我简单模拟一下吧,方便看懂
例子:比如从在区间[1,x]中 求a[]的和
规定:b[]为a[]的差分数组
解法:
朴素解法:a[1]+a[2]+a[3]+...+a[x] ----(1)
那么根据差分数组的特性则有:
a[1]=b[1]
a[2]=b[1]+b[2]
a[3]=b[1]+b[2]+b[3]
a[x]=b[1]+b[2]+b[3]+...+b[x] ----(2)
那么根据(2)式的特性,可以将(1)式改写为:
(b[1])+(b[1]+b[2])+(b[1]+b[2]+b[3])+...+(b[1]+b[2]+b[3]+...+b[x]) ----(3)
不难得出规律,在[1,x]区间中:
有 x-0个 b[1]
有 x-1个 b[2]
有 x-2个 b[3]
.....
有 x-(x-1)个 b[x]
那么y总的思路是:构造下面图片的这个 矩阵
目的是:对于(3)式 而言:就算用前缀和求出了 每一段()的和,但是仍然要用一层循环将其相加
所以实际上的时间复杂度仍然为O(N),那么我们就要考虑是不是可以构造出 *两个前缀和数组来优化*
实际上:图片中蓝色区域中的 数字才是 我们要求的数值
因此根据很简单的填充法:我们可以先求出整个 矩阵的数值,然后再减去红色区域的数值
那么根据图形不难得出如下公式:
矩阵的数值:(b[1]+b[2]+b[3]+...+b[x])*(x+1) ————(4)
红色区域的数值:(1* b[1]+2* b[2]+3* b[3]+...+x*b[x]) ————(5)
观察(4) 和 (5) 式子
不难得出 已经构造出了 两个前缀和数组
(4) 中的前缀和数组是 sum(x) 用tr1维护
(5) 中的前缀和数组是 i*sum(x) 用tr2维护
~~剩下的就与一个简单整数问题一样了~~
看不清的同学:我用截图吧QAQ md格式放到这里就好丑.....
Code:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=100010;
int n,m;
int a[N];
ll tr1[N];//维护b[i]的前缀和
ll tr2[N];//维护b[i]*i的前缀和
int lowbit(int x)
{
return x & -x;
}
void add(ll tr[],int x,ll c)
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
ll sum(ll tr[],int x)
{
ll res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
ll prefix_sum(int x)//数学构造:详细见图片
{
return sum(tr1,x)*(x+1)-sum(tr2,x);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++)//构造树上差分
{
int b=a[i]-a[i-1];
add(tr1,i,b);
add(tr2,i,(ll)b*i);
}
while(m--)
{
char op[2];
int l,r,d;
scanf("%s%d%d",op,&l,&r);
if(*op=='Q')
{
printf("%lld\n",prefix_sum(r)-prefix_sum(l-1));
}
else
{
scanf("%d",&d);
//a[l]+=d
add(tr1,l,d),add(tr2,l,l*d);
//a[r+1]-=d
add(tr1,r+1,-d),add(tr2,r+1,(r+1)*(-d));
}
}
return 0;
}
谜一样的牛
样例解释:
样例解释
- 第一行是索引
- 第二行表示的意思是: 第i个位置前有 第A[i]头牛 的身高比它低
- 特殊解释:因为 在第一个位置的牛,它前面没有牛,因此A[i] 没有给出(本质上是一个不存在的数)
- 从后往前推是更容易知道结果的,这个至于为什么是,,我只能说 显然
- 对于5位置上的值:0,前面没有比它身高低的,所以它就是 身高最低的:1
- 对于4位置上的值:1,也就是说前面 有一个比它身高低的,那么 对于 答案序列 1 2 3 4 5,我们已经用掉了1;因此此时的 答案序列为:
12 3 4 5,在这个序列中 寻找 <最小的第二个数(也就是 (1+1)=2)> :(原谅我语言表达不行....)理解这个意思就好:所以此时的答案是:3 - 对于3位置上的值:2,同理了,此时的答案序列为
1234 5,那么就要寻找 (2+1)=3,最小的第三个数,也就是 5 - 此后以此类推
核心思路:
这道题其实和 楼兰图腾 很像
- 在本题中 数字的大小实际上是作为 比较时的索引,而非真正的值,与楼兰图腾同理,值是这个数的个数;因此 数值的大小作为索引,1则作为val,当这个值被用过了,那么就需要让这个位置的值-1,表示为不可重复使用
- 其实相对于树状数组,本题更容易想到的是:二分,注意上面 中我们推导出 它是第几小的数的规律,是这个 数值的大小+1,那么也就是说,要寻找 第一个满足 “第几小这个性质的”,由左右区间可知,答案必然在 右区间;因此左半边是 不满足的,右半边是满足的,具有二段性,因此可以二分
- 那么 check函数的作用就是 在 序列 1 2 3 ..h[i]+1 ....N,判断从1~x(某个数)中,是否其有效数字的个数,已经恰好大于等于 h[i]+1,如果是则true,right=mid;反之false,left=mid+1
- 对于3中的查询操作,可以使用 树状数组中的 维护前缀和完成,使得效率更高
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int n;
int h[N];
int ans[N];
int tr[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main()
{
scanf("%d",&n);
for(int i=2;i<=n;i++) scanf("%d",&h[i]);
for(int i=1;i<=n;i++) add(i,1);
for(int i=n;i;i--)
{
int k=h[i]+1;
int l=1,r=n;
while(l<r)
{
int mid=l+r>>1;
if(sum(mid)>=k) r=mid;
else l=mid+1;
}
ans[i]=l;
add(l,-1);
}
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}