【寻迹#7】树状数组
树状数组
一、简介
树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。
普通树状数组维护的信息及运算要满足 结合律 且 可差分,如加法(和)、乘法(积)、异或等。
事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小。
树状数组能快速求解信息的原因:我们总能将一段前缀 \([1, n]\)拆成 不多于 \(\log n\) 段区间,使得这段区 \(\log n\) 间的信息是已知的。
1.管辖区间
树状数组其中的一位会管辖原数组的一位或多位并维护相应的信息
那么每一位具体的管辖区间是多少?
树状数组中,规定 \(c[x]\) 管辖的区间长度为 \(2^k\) ,其中:
- 设二进制最低位为第 \(k\) 位,则 \(k\) 恰好为 \(x\) 二进制表示中,最低位的
1
所在的二进制位数; - \(2^k\) (\(c[x]\) 的管辖区间长度)恰好为 \(x\) 二进制表示中,最低位的
1
以及后面所有0
组成的数。
我们记 \(x\) 二进制最低位 1
以及后面的 0
组成的数为 \(\operatorname{lowbit}(x)\) ,那么 \(c[x]\) 管辖的区间就是 \([x-\operatorname{lowbit}(x)+1,x]\) 。
这里注意:\(\operatorname{lowbit}(x)\) 指的不是最低位 1
所在的位数 \(k\) ,而是这个 1
和后面所有 0
组成的 \(2^k\) 。
怎么计算 lowbit
?根据位运算知识,可以得到 lowbit(x) = x & -x
。‘’
1.1怎么计算 \(\operatorname{lowbit}(x)\) ?
首先我们要知道负数在存储的时候是用的补码,也就是反码 \(+1\) ,
设原先 \(x\) (正数)的二进制编码是 \((...)10...00\) ,全部取反后得到 \([...]01...11\) ,再加一得到 \([...]10...00\) ,即 \(-x\) 的补码,这两个做一下与运算,前面的 \(()\) 与 \([]\) 里的部分都变成 \(0\) ,后面的部分正好为 \(10...00\) ,即 \(\operatorname{lowbit}(x)\)
2.区间查询
任何一个区间查询都可以这么做:查询 \(a[l...r]\) 的和,就是的 \(a[1...r]\) 和减去 \(a[1...l-1]\) 的和 ,从而把区间问题转化为前缀问题,更方便处理。
我们可以写出查询 \(a[1...x]\) 的过程:
- 从 \(c[x]\) 开始往前跳,有 \(c[x]\) 管辖 \(a[x-\operatorname{lowbit}(x)+1...x]\) ;
- 令 \(x \gets x - \operatorname{lowbit}(x)\) ,如果 \(x=0\) 说明已经跳到尽头了,终止循环;否则回到第一步。
- 将跳到的 \(c\) 合并。
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
3.树状数组与其树形态的性质
具体树状数组的其它性质可以参见OI-WIKI这篇文章
我们在这里就可以把树状数组的树形态理解为 \(x\) 向 \(x+\operatorname{lowbit}(x)\) 连边得到的图,其中 \(x+\operatorname{lowbit}(x)\) 是 \(x\) 的父亲。
4.单点修改
目标是快速正确地维护 \(c\) 数组,所以我们只需要遍历管辖了 \(a[x]\) 的所有 \(c[y]\) ,
管辖 \(a[x]\) 的 \(c[y]\) 一定包含了 \(c[x]\) ,在树形态上, \(y\) 是 \(x\) 的祖先。因此我们可以从 \(x\) 开始不断跳父亲,直到跳得超过了原数组长度为止。
设 \(n\) 表示 \(a\) 的大小,单点修改过程如下:
- 初始令 \(x'=x\) 。
- 修改 \(c[x']\) 。
- 令 \(x'\gets x'+\operatorname{lowbit}(x')\) ,如果 \(x'>n\) 说明已经跳到尽头了,终止循环;否则回到第二步。
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
5.建树
也就是根据最开始给出的序列,将树状数组建出来( \(c\) 全部预处理好)。
一般可以直接转化为 \(n\) 次单点修改,时间复杂度 \(O(n\log n)\)
但是也有 \(O(n)\) 建树的方法,如下
inline void build()
{
for(int i=1;i<=n;i++)
{
t[i]+=a[i];
int j=i+lowbit(i);
if(j<=n) t[j]+=t[i];
}
}
6.权值树状数组
6.1权值数组
首先来了解权值数组的定义,一个数组 \(a\) 的权值数组 \(b\) ,满足 \(b[x]\) 表示 \(x\) 在 \(a\) 中的出现次数,显然 \(b\) 的大小和 \(a\) 的值域有关,若原数据过大,可以将原数组离散化后再建立权值数组。
6.2 单点修改,查询全局第 \(k\) 小
运用权值树状数组,我们可以解决一些经典问题。
首先这个问题还是只需要数据之间的相对大小,所以可以离散化。只不过需要注意的是,要把单点修改中涉及到的值一起离散化。
对于单点修改,可以转化为对权值数组的单点修改,即当原数组 \(a[x]\) 从 \(y\) 变成 \(z\) 的时候,转化为对权值数组 \(b\) 的单点修改,即 \(b[y]\) 单点减一, \(b[z]\) 单点加一。
对于查询第 \(k\) 小,考虑二分 \(x\) ,查询权值数组中 \([1,x]\) 的前缀和,找到 \(x_0\) 使得 \([1,x_0]\) 的前缀和 \(<k\) 而 \([1,x_0+1]\) 的前缀和 \(\geq k\) ,则第 \(k\) 大的数是 \(x_0+1\) ,但这样做的复杂度是 \(O(\log^2n)\) 的。
考虑一个更快的做法——倍增。
设 \(x=0,sum=0\) ,枚举 \(i\) 从 \(\log_2n\) 降为 \(0\) :
- 查询权值数组中 \([x+1...x+2^i]\) 的区间和 \(t\) 。
- 如果 \(sum+t<k\) ,则拓展成功, \(x\gets x+2^i\) , \(sum\gets sum+t\) ;否则拓展失败不操作
这样得到的 \(x\) 是满足 \([1...x]\) 的前缀和 \(<k\) 的最大值,所以最终 \(x+1\) 就是答案。
为什么这样做更快?
因为这里的前缀和只需要访问 \(c[x+2^i]\) 的值即可。
考虑 \(\operatorname{lowbit}(x+2^i)\) 一定等于 \(2^i\) ,因为 \(i\) 是从 \(\log_2n\) 递减下来的,也就是说 \(x\) 之前只累加过 \(2^j\) 满足 \(j>i\) ,因此 \(c[x+2^i]\) 表示的区间就是 \([x+1...x+2^i]\) 。
这样的话直接访问 \(c[x+2^i]\) 复杂度变为 \(O(1)\) ,所以倍增下时间复杂度就变为 \(O(\log n)\) 。
二、题单
1. 【模板】树状数组 1
思路:
经典的单点修改,区间查询,用模板即可
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 500050
int n,m,a[N];
int t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void build()
{
for(int i=1;i<=n;i++)
{
t[i]+=a[i];
int j=i+lowbit(i);
if(j<=n) t[j]+=t[i];
}
}
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read();
build();
for(int i=1;i<=m;i++)
{
int opt,x,y;
opt=read();x=read();y=read();
if(opt==1) add(x,y);
else cout<<query(y)-query(x-1)<<endl;
}
return 0;
}
2.【模板】树状数组 2
思路:
本题关键在于转化,想到我们已经会了单点修改区间查询,那就要往这上面转化,
考虑维护一个 \(a\) 数组的差分数组 \(b\) ,那么 \(b\) 的前缀和就是 \(a\) 中对应的元素,
于是我们发现单点查询已经搞定了(已经变成了区间和),
那单点修改呢,
我们发现如果维护一个与 \(b\) 有关的树状数组,
那么对于 \(a\) 数组,要修改区间 \([l,r]\) 内的元素时,其实只需要更改 \(b\) 中 \(b[l]\) 和 \(b[r+1]\) 即可。也把区间修改转化为了单点修改
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 500050
int n,m,a[N];
int b[N],t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void build()
{
for(int i=1;i<=n;i++)
{
t[i]+=b[i];
int j=i+lowbit(i);
if(j<=n) t[j]+=t[i];
}
}
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
m=read();
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
for(int i=1;i<=n;i++)
{
a[i]=read();
b[i]=a[i]-a[i-1];
}
build();
for(int i=1;i<=m;i++)
{
int opt,x,y,k;
opt=read();x=read();
if(opt==1)
{
y=read();k=read();
add(x,k);add(y+1,-k);
}
else cout<<query(x)<<endl;
}
return 0;
}
3.简单题
思路:
诶这道题其实就是上面的模板题的小变式, \(0\) 变 \(1\) , \(1\) 变 \(0\) 的操作很容易想到异或,
然后自己推导一下,发现异或运算满足结合律,并且可差分,
那就说明可以用树状数组维护,
又看到是区间修改,单点查询,那直接维护一个差分树状数组即可,
只不过这里的差分并不是加法的逆运算,而是异或的逆运算。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,m,a[N];
int t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x)
{
while(x<=n)
{
t[x]^=1;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=t[x];x-=lowbit(x);
while(x>0)
{
sum^=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();m=read();
memset(a,0,sizeof(a));
for(int i=1;i<=m;i++)
{
int opt,x,y;
opt=read();x=read();
if(opt==1)
{
y=read();
add(x);add(y+1);
}
else printf("%d\n",query(x));
}
return 0;
}
4.约瑟夫问题
方法一:
思路:有一个用链表做的比较好想的思路,就是开一个 \(Next\) 数组记他的后继,当这个人出局以后,就把他的后继改成他后继的后继。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1050
int m,n;
int Next[N];
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++) Next[i]=i+1;
Next[n]=1;
int p=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<m;j++) p=Next[p];
cout<<Next[p]<<" ";
Next[p]=Next[Next[p]];
}
return 0;
}
方法二:
思路:
用权值树状数组维护,初始所有权值数组均为 \(1\) ,都出现了一次,
然后去查全局第 \(k\) 小,只不过 \(k\) 在变化,
每次查完 \(k\) ,就接着去查 \(k+m-1\) 即可,注意取模,
取模可以写成 \([(k+m-1-1)\mod (n-i)]+1\) (注意模数不能为 \(0\) !),这样去掉了模出来是 \(0\) 的结果,
取模完把这个人删掉即可。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 150
int n,m;
int t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int kth(int k)
{
int x=0,sum=0;
for(int i=log2(n);i>=0;i--)
{
if(x+(1<<i)>n||sum+t[x+(1<<i)]>=k) continue;
x+=(1<<i);
sum+=t[x];
}
return x+1;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) add(i,1);
int k=m;
for(int i=1;i<=n;i++)
{
int x=kth(k);
cout<<x<<" ";
if(i==n) break;
add(x,-1);
k=(k+m-1-1)%(n-i)+1;
}
return 0;
}
5.Sleepy Cow Sorting
思路:
首先先考虑最小操作次数,发现是从右向左第一个上升的位置(手玩一下不难发现),
然后考虑前面每一个数的操作数,
由题意可知第 \(i\) 头牛的排名就是 \(a[i]\) ,
我们用 \(cnt\) 记录已排序的数的个数,那么未排序的个数为 \(n-cnt\) ,
每次移动队列最前面的那头牛要考虑,
前面有多少头比这头牛编号小的已经排好序了,
那么操作数就是 \(n-cnt+query(a[i])-1\) 。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,a[N];
int t[N],cnt,tot,ans[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=n;i>=1;i--)
{
add(a[i],1);cnt++;
if(a[i]<a[i-1]) break;
}
tot=n-cnt;
for(int i=1;i<=tot;i++)
{
ans[i]=n-cnt+query(a[i])-1;
add(a[i],1);
cnt++;
}
cout<<tot<<endl;
for(int i=1;i<=tot;i++) cout<<ans[i]<<" ";
return 0;
}
6.逆序对
思路:
一道权值树状数组题目。
想了好久终于想明白了,首先看到数据范围比较大,
并且我们要维护的是逆序对,所以数字实际大小其实没什么意义,只需要相对大小就可以了,那我们也可以离散化了,
离散化以后,每个数就变成了它所在数组中的排名,
以从小到大排序为例,题目中的 \(、、、、、5、4、2、6、3、1\) 就变为 \(、、、、、5、4、2、6、3、1\) 确实没啥变化,但在这里每个数是它的相对排名,
然后我们考虑建树,
从左向右,将树状数组中 \(t[x]+1\) ,\(x\) 为所有的排名,
然后我们去查询,
查询是向下查询的,即 \(x'=x-\operatorname{lowbit}(x)\) ,它的排名 \(x'\) 要小于 \(x\) 。
如果 \(t[x']\) 不为空,那么它一定是在之前进入的树状数组,那么我们就可以推断这两个数不是逆序的(排名小且靠前进入),
当第 \(i\) 个数进入树状数组时它会与前面 \(i-1\) 个数产生 \(i-1\) 个数对,这其中有 \(\left(query(x)-1\right)\) 个不是逆序对的(要去掉刚加进去的自己),两者相减即为逆序对个数,即 \(i-query(x)\) 个。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 500050
typedef long long ll;
int n,a[N];
int t[N],tmp[N],len;
ll ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read(),tmp[i]=a[i];
sort(tmp+1,tmp+1+n);
len=unique(tmp+1,tmp+1+n)-(tmp+1);
for(int i=1;i<=n;i++)
a[i]=lower_bound(tmp+1,tmp+len+1,a[i])-(tmp);
for(int i=1;i<=n;i++)
{
add(a[i],1);
ans+=i-query(a[i]);
}
printf("%lld",ans);
return 0;
}
实际上,逆序对求解部分还有另一种写法如下:
for(int i=n;i>=1;i--)
{
ans+=query(a[i]-1);
add(a[i],1);
}
从 \(n\) 到 \(1\) 倒着枚举,
如果 \(t[x']\) 不为空,那么它一定是在之前进入的树状数组,因为是倒序相当于更靠后,那么小且靠后就是一个逆序对。
7.递增
思路:
正常的 dp 比较好想,设 \(f[i]\) 表示截止到第 \(i\) 个位置,最长上升子序列的长度,
\(O(n^2)\) 的做法就是对于每一个位置 \(i\) ,枚举其前面的位置 \(j\) ,若 \(a[j]<a[i]\) ,那么 \(f[i]=\max(f[i],f[j]+1)\) ,但是对于这道题的数据范围肯定过不了。
虽然树状数组维护区间最大值是 \(O(\log^2n)\) 的,但是维护前缀最大值是 \(O(\log n)\) 的,
所以我们对数据离散化以后,直接树状数组维护前缀最大值即可,
简单说说 \(f[i]\) 更新的过程,离散化后 \(a[i]\) 中存储的是在原数组中的排名
所以 \(f[i]=query(a[i]-1)+1\) ,因为查询的是比 \(a[i]\) 排名小的并且先进入树状数组的也就是说, \(a[i]\) 可以与他们构成最大上升子序列,而 \(query\) 返回的又是最大值,所以直接 \(+1\) 即可, 然后再 \(add(a[i],f[a[i]])\) 即可,表示 \(a[i]\) 也已经进入树状数组。
最后答案就是 \(n-f[n]\) 。时间复杂度 \(O(n\log n)\) 。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,a[N];
int len,tmp[N];
int f[N],t[N],ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]=max(t[x],k);
x+=lowbit(x);
}
}
inline int query(int x)
{
int maxx=0;
while(x>0)
{
maxx=max(maxx,t[x]);
x-=lowbit(x);
}
return maxx;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read(),tmp[i]=a[i];
sort(tmp+1,tmp+1+n);
len=unique(tmp+1,tmp+1+n)-tmp-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(tmp+1,tmp+1+len,a[i])-tmp;
for(int i=1;i<=n;i++)
{
f[a[i]]=query(a[i]-1)+1;
add(a[i],f[a[i]]);
}
ans=n-query(n);
cout<<ans<<endl;
return 0;
}
8.火柴排队
思路:
首先还是发现,题目中我们只需要相对高度,所以还是先对两列火柴离散化,
然后对两列火柴分别操作和只对一列火柴操作是等价的,
所以我们不妨把第一列火柴看作是“模板” ,
因为每次只能交换相邻的火柴,
所以只需要求第二列火柴在第一列火柴意义下的逆序数(自己起的名字)即可。
下面我们以样例 \(2\) 为例,
第一列火柴为: \(、、、1、3、4、2\) ,离散化后数值不变
第二列火柴为: \(、、、1、7、2、4\) ,离散化后变为 \(、、、1、4、2、3\) ,
在第一列火柴意义下的逆序数就是把第一列火柴看为 \(、、、1、2、3、4\) ,
那么第二列火柴就被看为 \(、、、1、3、4、2\) ,此时第二列火柴中的逆序对个数为 \(2\) ,所以操作数为 \(2\) 。
所以如果第一列火柴用 \(a[i]\) 表示,那么我们可以离散化后 \(id[a[i]]=i\) ,然后求逆序对的时候查询 \(id[b[i]]\) ,至此本题结束。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
#define MOD 99999997
typedef long long ll;
int n,a[N],b[N];
int len1,tmp1[N],len2,tmp2[N];
int q[N],t[N];
ll ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline ll query(int x)
{
ll sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read(),tmp1[i]=a[i];
for(int i=1;i<=n;i++) b[i]=read(),tmp2[i]=b[i];
sort(tmp1+1,tmp1+1+n);sort(tmp2+1,tmp2+1+n);
len1=unique(tmp1+1,tmp1+1+n)-tmp1-1;
len2=unique(tmp2+1,tmp2+1+n)-tmp2-1;
for(int i=1;i<=n;i++)
{
a[i]=lower_bound(tmp1+1,tmp1+1+len1,a[i])-tmp1;
b[i]=lower_bound(tmp2+1,tmp2+1+len2,b[i])-tmp2;
}
for(int i=1;i<=n;i++) q[a[i]]=i;
for(int i=n;i>=1;i--)
{
ans=(ans%MOD+query(q[b[i]]-1)%MOD)%MOD;
add(q[b[i]],1);
}
printf("%lld\n",ans);
return 0;
}
9.Promotion Counting
思路:
题意简化为就树上的逆序对,还是考虑使用树状数组,
我们可以利用一个 dfs 来求解,从根开始遍历,一直到叶子节点,经过查询、更新等操作一直往父节点上传,直到求出最后的答案。
在这种情况下 \(\operatorname{query}(i)\) 就是查询比 \(i\) 小还要靠前进入树状数组的,先不考虑树的结构,体现在正常数组里就是所有比 \(i\) 弱的,所以 \(\operatorname{query}(n)-\operatorname{query}(i)\) 就是比 \(n\) 弱的减去比 \(i\) 弱的,也就是所有比 \(i\) 强的。
所以每一次 dfs 我们先把当前节点的答案初始值设为 \(ans1=-(\operatorname{query}(n)-\operatorname{query}(i))=\operatorname{query}(i)-\operatorname{query}(n)\) ,表示初始时有多少是比 \(i\) 强的,此时我们还没有考虑树上的结构,这个时候递归到当前节点的儿子节点,一直到叶子节点,我们开始加入到树状数组中,然后返回的时候统计当前节点加入儿子以后有多少比 \(i\) 强的,记为 \(ans2\) ,两者相减就是考虑了树形结构后,只有 \(i\) 的下属中比 \(i\) 强的个数 \(ans2-ans1\) ,统计完后再将当前节点加入树状数组中
代码:
#include<bits/stdc++.h>
#define N 200050
using namespace std;
typedef long long ll;
int n,p[N];
int len,tmp[N],t[N],ans[N];
int head[N],Next[N],ver[N],tot=-1;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline void add(int x,int y)
{
ver[++tot]=y;
Next[tot]=head[x];
head[x]=tot;
}
inline int lowbit(int x) { return x&-x; }
inline void update(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
void dfs(int x,int fa)
{
ans[x]=-(query(n)-query(p[x])); //初始化一开始有多少比 x 强的
for(int i=head[x];~i;i=Next[i]) //如果叶子节点就不会进 for 循环
{
int y=ver[i];
if(y==fa) continue;
dfs(y,x); //添加下属会一直递归到叶子节点
}
ans[x]+=(query(n)-query(p[x])); //叶子节点或递归回来的父亲节点会执行
//递归回来的父亲节点统计一下加了下属以后比 x 强的有多少 然后累加即可
update(p[x],1); //叶子节点或父亲节点统计完以后添加到树状数组中
}
int main()
{
memset(head,-1,sizeof(head));
n=read();
for(int i=1;i<=n;i++) p[i]=read(),tmp[i]=p[i];
sort(tmp+1,tmp+1+n);
len=unique(tmp+1,tmp+1+n)-tmp-1;
for(int i=1;i<=n;i++) p[i]=lower_bound(tmp+1,tmp+1+len,p[i])-tmp;
for(int i=2;i<=n;i++)
{
int fa=read();
add(fa,i);add(i,fa);
}
dfs(1,-1);
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}
10.上帝造题的七分钟 2 / 花神游历各国
思路:
本题的难点在于如何处理开平方根的操作,
没有数据结构可以维护根号操作,线段树的懒标记也不好操作,
所以我么就考虑最简单的暴力,但是要加点小技巧,
我们发现 \(10^{12}\) 的数开根 \(6\) 次左右就已经到 \(1\) 左右了,所以实际上每个数会被开方的次数很少,
于是我们可以加一个并查集维护, \(fa[i]\) 指向 \(i\) 后面第一个不为 \(1\) 的数的位置,初始值全为 \(i\) ,
所以修改的时候只需要修改 \(f[i]=i\) 的节点即可
代码:
#include<bits/stdc++.h>
#define N 100050
using namespace std;
typedef long long ll;
ll n,m,a[N];
ll k,l,r;
ll t[N],fa[N];
inline ll read()
{
ll x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
int Find(int x)
{
if(x==fa[x]) return x;
return fa[x]=Find(fa[x]);
}
inline ll lowbit(ll x) { return x&-x; }
inline void add(ll x,ll k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline ll query(ll x)
{
ll sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(ll i=1;i<=n;i++)
{
fa[i]=i;
a[i]=read();
add(i,a[i]);
}
fa[n+1]=n+1;
m=read();
while(m--)
{
k=read();l=read();r=read();
if(l>r) l^=r^=l^=r;
if(k) printf("%lld\n",query(r)-query(l-1));
else
{
for(ll i=l;i<=r;)
{
ll tmp=(ll)sqrt(a[i]);
add(i,tmp-a[i]);
a[i]=tmp;
if(a[i]<=1) { fa[i]=i+1;i=Find(fa[i]); }
//先令 fa[i]=i+1 这个时候并不知道 i+1 的具体情况
//然后 i 变为第一个不是 1 的数还能顺便更新 fa[i]
else { fa[i]=i;i++; } //正常 i++ 看下一位
}
}
}
return 0;
}
11.HH的项链
思路:
这道题的树状数组跟前面几道又有点不太一样,首先我们先考虑权值树状数组,但是权值和序列是两个维度(仔细体会一下?)
换句话说,我们没法维护区间权值,这也就是为什么我们没法用树状数组维护区间众数,
那我们换个思路,我们发现对于一次右端点为 \(r\) 的查询,某个贝壳对答案的贡献一定是更靠近右端点的那个,离右端点远的那个同类贝壳是没有意义的,
所以我们可以维护某个位置上是否有数字,当一个贝壳重复出现的时候,把它原来的位置改成没有数字,把当前出现的位置改为有数字,
举个例子现在有 \(3\) 个贝壳: \(、、1、2、1\)
先查询 \([1,2]\) ,那么第一个位置和第二个位置上都有数字,所以现在数组状态为 \(、、1、1、0\) ,此时答案就是 \(\operatorname{query}(2)-\operatorname{query}(0)=2\) ,
再查询 \([1,3]\) ,此时第三个位置又出现了 \(1\) ,所以我们把上一次 \(1\) 出现的位置(第 \(1\) 位)先改为 \(0\) ,然后再把第三位改为 \(1\) ,所以此时数组状态为 \(、、0、1、1\) ,答案就是 \(\operatorname{query}(3)-\operatorname{query}(0)=2\)
所以我们的思路是离线所有查询,将其按照右端点排序,用 \(vis\) 数组记录某一类贝壳上次出现的位置,然后用变量 \(pos\) 表示当前处理到哪一位数字了,也就是上一个查询的右端点 \(+1\)
具体细节看代码
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1000050
int n,m,a[N];
struct node{ int l,r,id; }q[N];
int pos,ans[N],vis[N],t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline bool cmp(node a,node b) { return a.r<b.r; }
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
m=read();
for(int i=1;i<=m;i++)
{
q[i].l=read();
q[i].r=read();
q[i].id=i;//记录每个查询的编号,因为得按题目顺序输出
}
sort(q+1,q+1+m,cmp);
pos=1;
for(int i=1;i<=m;i++)
{
for(int j=pos;j<=q[i].r;j++)//从上一次处理的位置开始
{
if(vis[a[j]]) add(vis[a[j]],-1);//出现过就先把之前的删去
vis[a[j]]=j;//记录本次出现的位置
add(vis[a[j]],1);//把该位置改为 1
}
pos=q[i].r+1;
ans[q[i].id]=query(q[i].r)-query(q[i].l-1);//前缀和查询答案
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
12.上帝造题的七分钟
思路:还没做完后面补上
代码: