【学习笔记】进阶算法——树状数组 & 线段树
树状数组
单点修改,区间查询。
核心在于 lowbit(
总之,就是用一个 \(c\) 数组去存储。\(c_i\) 的定义?含有 lowbit!(懒得说,反正定义也不重要
看一下最重要的两个函数:
//两个函数的时间复杂度都是 log 级别的
//其中的 x&-x 这一部分其实上就是所谓的 lowbit
void upd(int x,int k){while(x<=n)c[x]+=k,x+=x&-x;return;}
//upd(x,k) 表示对 x 这个点的数值加上 k
int ask(int x){int as=0;while(x)as+=c[x],x-=x&-x;return as;}
//ask(x) 返回的是 1~x 这个区间的所有数的和
//如果需要算 l~r 这个区间的和可以使用 ask(r)-ask(l-1)
然后就结束了。
很简单对吧,但是树状数组的运用很广。
这里放一个习题。
这个题很好玩的。可以用线段树做,这点我不否认,但更强的法子是用树状数组。而且它维护的是一个差分数组!也就是说,你查询 ask(x),就是查到了第 \(x\) 个数的真实数值!是不是超强的?毕竟树状数组代码短,常数小,而线段树嘛……难写又难调,代码不简洁,常数还大得吓人呢!
说了这么多,还是贴一下这个习题的代码吧:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5+5;LL n,m,c[N];
void upd(int x,LL k){while(x<=n)c[x]+=k,x+=x&-x;return;}
LL ask(int x){LL as=0;while(x)as+=c[x],x-=x&-x;return as;}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){int x;cin>>x;upd(i,x),upd(i+1,-x);}
while(m--){
LL u;cin>>u;u++;LL x=ask(u);
upd(u,-x),upd(u+1,x);upd(1,x/n),upd(n+1,-x/n);
upd(u+1,1),upd(min(n,u+x%n)+1,-1);
if(u+x%n>n)upd(1,1),upd(u+x%n-n+1,-1);
}
for(int i=1;i<=n;i++)cout<<ask(i)<<" ";
return 0;
}
你看你看,你瞧你瞧,这才 \(18\) 行代码。但要是你写线段树啊,那不是怎么压行都至少有个 \(50\) 多行代码吗?树状数组多方便啊!所以说,线段树不是个好东西,千万别用
线段树
线段树,码量大,常数大,还难写难调,不是什么蛮好的算法。但是用处广呐!区间修改区间查询全能一套整上!时间复杂度 \(O(n \log n)\),也还好。
来,上例题!
显然的区修区查。
线段树的精髓在于 LazyTag,即懒标记。
哎由于这不是讲题的学习笔记所以不细说了。
上代码!(很丑)
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 1e5+5;
int n,m;LL a[N],s[N*4],t[N*4];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void write(LL x){if(x>9)write(x/10);putchar(x%10+'0');}
int ls(int x){return (x<<1);}int rs(int x){return (x<<1|1);}
void push_up(int x){s[x]=s[ls(x)]+s[rs(x)];return;}
void build(int x,int l,int r){
if(l==r){s[x]=a[l];return;}
int mid=(l+r)>>1;
build(ls(x),l,mid);build(rs(x),mid+1,r);
push_up(x);t[x]=0;return;
}
void add(int x,int l,int r,LL k){s[x]+=(r-l+1)*k;t[x]+=k;return;}
void push_down(int x,int l,int r){
if(!t[x])return;int mid=(l+r)>>1;
add(ls(x),l,mid,t[x]);add(rs(x),mid+1,r,t[x]);t[x]=0;return;
}
void change(int x,int l,int r,int L,int R,LL k){
if(r<L||R<l)return;
if(L<=l&&r<=R){add(x,l,r,k);return;}
push_down(x,l,r);int mid=(l+r)>>1;
change(ls(x),l,mid,L,R,k);
change(rs(x),mid+1,r,L,R,k);
push_up(x);return;
}
LL ask(int x,int l,int r,int L,int R){
if(r<L||R<l)return 0;if(L<=l&&r<=R)return s[x];
push_down(x,l,r);int mid=(l+r)>>1;
return ask(ls(x),l,mid,L,R)+ask(rs(x),mid+1,r,L,R);
}
int main(){
n=read(),m=read();for(int i=1;i<=n;i++)a[i]=read();build(1,1,n);
while(m--){
int opt=read();
if(opt==1){int x=read(),y=read();LL k=read();change(1,1,n,x,y,k);}
else{int x=read(),y=read();write(ask(1,1,n,x,y));cout<<"\n";}
}
return 0;
}
算是压行压得很厉害的了。只有 \(47\) 行!
那么这就是线段树啦。
顺便贴上线段树的另一个模版题链接。这个就是多加了个乘法!加乘混合,LazyTag 就麻烦一些啦。
代码……请原谅,懒人儿就要有懒样儿,所以懒得贴了。(
接下来上习题!等一下我去找题
行哈!找到了。
这个题倒是蛮有意思的。调也调了半天
主要就是你的线段树需要维护四个东西,最大的数,最大数的个数,次大的数,次大数的个数。然后合并的时候,也就是我们的 push_up,需要一个比较复杂的东西。有搁那一个个比较的,但是马良巨大,因此我用的是无聊 map 以及无聊 priority_queue,我真是个 STL 高手,就写个合并用俩 STL。怎么说呢,整个的细节还是很多的,超难调。
上个代码吧。码风奇异,当时调来调去的写得也有点乱,将就看吧。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5+5;int n,Q;LL a[N];
struct node{int num1,num2,cnt1,cnt2;}s[N*4];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int ls(int x){return (x<<1);}int rs(int x){return (x<<1|1);}
void push_up(int x){
map<int,int> mp;mp.clear();
priority_queue<int> q;while(!q.empty())q.pop();
q.push(s[ls(x)].num1),q.push(s[ls(x)].num2);
q.push(s[rs(x)].num1),q.push(s[rs(x)].num2);
mp[s[ls(x)].num1]+=s[ls(x)].cnt1,mp[s[ls(x)].num2]+=s[ls(x)].cnt2;
mp[s[rs(x)].num1]+=s[rs(x)].cnt1,mp[s[rs(x)].num2]+=s[rs(x)].cnt2;
if(mp.size()==1){
s[x].num1=q.top(),s[x].num2=q.top();
s[x].cnt1=mp[s[x].num1],s[x].cnt2=s[x].cnt1;return;
}
s[x].num1=q.top(),s[x].cnt1=mp[q.top()];while(q.top()==s[x].num1)q.pop();
s[x].num2=q.top(),s[x].cnt2=mp[q.top()];q.pop();return;
}
void build(int x,int l,int r){
if(l==r){s[x]={a[l],0,1,0};return;}
int mid=(l+r)>>1;build(ls(x),l,mid);
build(rs(x),mid+1,r);push_up(x);
return;
}
void change(int x,int l,int r,int LR,LL k){
if(l==LR&&r==LR){s[x]={k,0,1,0};return;}int mid=(l+r)>>1;
if(LR<=mid)change(ls(x),l,mid,LR,k);
else change(rs(x),mid+1,r,LR,k);push_up(x);return;
}
node SeCon(node x,node y){
map<int,int> mp;mp.clear();node A;
priority_queue<int> q;while(!q.empty())q.pop();
q.push(x.num1),q.push(x.num2);
q.push(y.num1),q.push(y.num2);
mp[x.num1]+=x.cnt1,mp[x.num2]+=x.cnt2;
mp[y.num1]+=y.cnt1,mp[y.num2]+=y.cnt2;
if(mp.size()==1){
A.num1=q.top(),A.num2=q.top();
A.cnt1=mp[A.num1],A.cnt2=A.cnt1;return A;
}
A.num1=q.top(),A.cnt1=mp[q.top()];
while(q.top()==A.num1)q.pop();
A.num2=q.top(),A.cnt2=mp[q.top()];q.pop();return A;
}
node ask(int x,int l,int r,int L,int R){
if(L<=l&&r<=R)return s[x];int mid=(l+r)>>1;
if(R<=mid)return ask(ls(x),l,mid,L,R);
if(L>mid)return ask(rs(x),mid+1,r,L,R);
return SeCon(ask(ls(x),l,mid,L,R),ask(rs(x),mid+1,r,L,R));
}
int main(){
n=read(),Q=read();for(int i=1;i<=n;i++)a[i]=read();build(1,1,n);
while(Q--){
int opt=read();
if(opt==1){int x=read();LL k=read();change(1,1,n,x,k);}
else{
int x=read(),y=read();
node A=ask(1,1,n,x,y);
cout<<(A.num2==A.num1?0:A.cnt2)<<"\n";
}
}
return 0;
}
这也应该有个七八十行代码吧,细节又堆积成山。因此尽量还是别用线段树,难写难调。唉,但谁要它用处大呢!所以还是得学呐!
总结
树状数组是个好东西,线段树不是个好东西,嗯然后没然后了
这总结得挺对,划掉干啥(
所以说,树状数组的话其实上随你用的,没细节,码量又小得可怜,多亲切啊。线段树的话,如果考场上遇到,一定不要上来就写,最后留个一个小时左右攻克就行。一遍过了倒算,要是出了什么小小的 bug,把自己心态调崩了,那做后面的题可就没心情了,而且还浪费了很多时间。
线段树啊!要完全掌握你,第一步是学会调试呐!

浙公网安备 33010602011771号