1.模板
两种操作:
1.给某个数加x。
2.查询区间和
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[100010];
int tree[4*100010];//节点数不超过叶子的四倍
void build(int p,int l,int r)//p是节点编号
{
if(l==r)
{
tree[p]=a[l];
return;
}
int mid=(l+r)/2;
build(p*2,l,mid);//建左子树;
build(p*2+1,mid+1,r);//建右子树;
tree[p]=tree[p*2]+tree[p*2+1];//建自己;
}
void change(int p,int l,int r,int pos,int num)//pos表示要更改的点,num是改变的数
{
if(l==r)//遇到该点
{
tree[p]+=num;
return;
}
int mid=(l+r)/2;
if(pos<=mid)//如果左子树包括该点,那么左子树的节点也要改变
{
change(p*2,l,mid,pos,num);
}
if(pos>mid)//右子树
{
change(p*2+1,mid+1,r,pos,num);
}
tree[p]=tree[p*2]+tree[p*2+1];//自己也要更新
}
int calc(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)//如果这个区间被包括
{
return tree[p];
}
int mid=(l+r)/2;//分三情况
if(y<=mid)//如果查询区间都在左子树里边
{
return calc(p*2,l,mid,x,y);
}
if(x>mid)//查询区间都在右子树里
{
return calc(p*2+1,mid+1,r,x,y);
}
return calc(p*2,l,mid,x,mid)+calc(p*2+1,mid+1,r,mid+1,y);//如果查询区间在两个子树里都有
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
for(int i=1;i<=m;i++)
{
int x,y,z;
cin>>x>>y>>z;
if(x==1) change(1,1,n,y,z);
else{
cout<<calc(1,1,n,y,z)<<endl;
}
}
}
模板题二:
四种操作:
1.求区间和
2.求区间平方和
3.在区间上每个数乘x
4.在区间上每个数加x
#include<bits/stdc++.h>
using namespace std;
//https://ac.nowcoder.com/acm/problem/19246
#define ll long long
ll sum[4*10005];
ll sum2[4*10005];
ll a[10005];
ll lazyplu[4*10005];
ll lazymul[4*10005];
inline void read(int &data){
int x=0,flag=1;char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') flag=flag*-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
data=x*flag;
}
void build(int p,int l,int r)
{
lazyplu[p]=0;
lazymul[p]=1;
if(l==r)
{
sum[p]=a[l];
sum2[p]=a[l]*a[l];
return;
}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
sum[p]=sum[p*2]+sum[p*2+1];
sum2[p]=sum2[p*2]+sum2[p*2+1];
}
void pushdown(int node,int start,int end){
int mid=start+end>>1;
lazyplu[node<<1]*=lazymul[node];
lazyplu[node<<1|1]*=lazymul[node];
lazymul[node<<1]*=lazymul[node];
lazymul[node<<1|1]*=lazymul[node];
sum[node<<1]*=lazymul[node];
sum[node<<1|1]*=lazymul[node];
sum2[node<<1]*=lazymul[node]*lazymul[node];
sum2[node<<1|1]*=lazymul[node]*lazymul[node];
lazymul[node]=1;
if(lazyplu[node]){
lazyplu[node<<1]+=lazyplu[node];
lazyplu[node<<1|1]+=lazyplu[node];
sum2[node<<1]+=2*lazyplu[node]*sum[node<<1]+(mid-start+1)*lazyplu[node]*lazymul[node];
sum2[node<<1|1]+=2*lazyplu[node]*sum[node<<1|1]+(end-mid)*lazyplu[node]*lazyplu[node];
sum[node<<1]+=lazyplu[node]*(mid-start+1);
sum[node<<1|1]+=lazyplu[node]*(end-mid);
lazyplu[node]=0;
}
}
void changeplu(int p,int l,int r,int x,int y,int add)
{
if(x<=l&&r<=y)
{
lazyplu[p]+=add;
sum2[p]+=sum[p]*2*add+add*add*(r-l+1);
sum[p]+=(r-l+1)*add;
return;
}
int mid=(l+r)/2;
pushdown(p,l,r);
if(x<=mid) changeplu(p*2,l,mid,x,y,add);
if(y>mid) changeplu(p*2+1,mid,r,x,y,add);
sum[p]=sum[p*2]+sum[p*2+1];
sum2[p]=sum2[p*2]+sum2[p*2+1];
}
void changemul(int p,int l,int r,int x,int y,int mul)
{
if(x<=l&&r<=y)
{
sum[p]*=mul;
lazyplu[p]*=mul;//乘的时候要记得b也给乘上
lazymul[p]*=mul;
sum2[p]=mul*mul*sum2[p];
return;
}
int mid=(l+r)/2;
pushdown(p,l,r);
if(x<=mid) changemul(p*2,l,mid,x,y,mul);
if(y>mid) changemul(p*2+1,mid,r,x,y,mul);
sum[p]=sum[p*2]+sum[p*2+1];
sum2[p]=sum2[p*2]+sum2[p*2+1];
}
ll calche(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return sum[p];
}
int mid=(l+r)/2;
pushdown(p,l,r);
ll ans=0;
if(x<=mid) ans+=calche(p*2,l,mid,x,y);
if(y>mid) ans+=calche(p*2+1,mid+1,r,x,y);
return ans;
}
ll calcji(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return sum2[p];
}
int mid=(l+r)/2;
pushdown(p,l,r);
ll ans=0;
if(x<=mid) ans+=calcji(p*2,l,mid,x,y);
if(y>mid) ans+=calcji(p*2+1,mid+1,r,x,y);
return ans;
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
for(int i=1;i<=m;i++)
{
int q;
read(q);
if(q==1)
{
int l,r;
read(l);
read(r);
cout<<calche(1,1,n,l,r)<<endl;
}
if(q==2)
{
int l,r;
read(l);
read(r);
cout<<calcji(1,1,n,l,r)<<endl;
}
if(q==3)
{
int l,r;
int mul;
read(l);
read(r);
read(mul);
changemul(1,1,n,l,r,mul);
}
if(q==4)
{
int l,r;
int add;
read(l);
read(r);
read(add);
changeplu(1,1,n,l,r,add);
}
}
}
例题3:
省赛题:完美主义
#include<bits/stdc++.h>
using namespace std;
//https://ac.nowcoder.com/acm/problem/229502
const int maxn=300010;
const int INF=0x3f3f3f3f;
int a[maxn];
int tree[4*maxn];
void build(int p,int l,int r)
{
if(l==r)
{
tree[p]=a[l]-a[l-1];
//cout<<p<<' '<<tree[p]<<endl;
return;
}
int mid=l+r>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
tree[p]=min(tree[p*2],tree[p*2+1]);//存下负数
}
void change(int p,int l,int r,int pos,int num)
{
if(l==r)
{
// tree[p]=num-a[l-1];
// tree[p+1]=a[l+1]-num;//这样写tree[p+1]的更改没有往上传,所以分开写
//cout<<a[l];
tree[p]+=num;
return;
}
int mid=l+r>>1;
if(pos<=mid) change(p*2,l,mid,pos,num);
else if(pos>mid) change(p*2+1,mid+1,r,pos,num);
tree[p]=min(tree[p*2],tree[p*2+1]);
}
int calc(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return tree[p];
}
int mid=l+r>>1;
int ans=INF;
if(x<=mid) ans=min(ans,calc(p*2,l,mid,x,y));
if(y>mid) ans=min(ans,calc(p*2+1,mid+1,r,x,y));
return ans;
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
// for(int j=1;j<=13;j++) cout<<tree[j]<<' ';
// cout<<endl<<endl;
for(int i=1;i<=m;i++)
{
int q;
cin>>q;
if(q==1)
{
int pos,num;
cin>>pos>>num;
change(1,1,n,pos,-a[pos]);
change(1,1,n,pos+1,a[pos]);
change(1,1,n,pos,num);
change(1,1,n,pos+1,-num);
a[pos]=num;
// for(int j=1;j<=n;j++) cout<<a[j]<<' ';
// cout<<endl;
}
if(q==2)
{
int l,r;
cin>>l>>r;
// for(int j=1;j<=13;j++) cout<<tree[j]<<' ';
// cout<<endl<<endl;
int res=calc(1,1,n,l+1,r);
if(res<0) cout<<"No"<<endl;
else cout<<"Yes"<<endl;
}
}
}
线段树题目特点:
1.关于区间修改与查询
2.两个子问题求出父亲问题(线段树必要条件)
3.考虑能不能用lazy标记(也是必要条件
例如对区间内每个数开根号向下取整
值域线段树
线段树的大小由所给数据范围决定
例题:数星星
天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。如果一个星星的左下方(包含正左和正下)有k颗星星,就说这颗星星是k级的。
例如,上图中星星5是3级的(1,2,4在它左下),星星2,4是1级的。例图中有1个0级,2个1级,1个2级,1个3级的星星。
给定星星的位置,输出各级星星的数目。
一句话题意:给定n个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=32010;
int tree[4*maxn];
int cnt[150001];
void change(int p,int l,int r,int pos)
{
if(l==r)
{
tree[p]++;
return;
}
int mid=(l+r)/2;
if(pos<=mid) change(p*2,l,mid,pos);
if(pos>mid) change(p*2+1,mid+1,r,pos);
tree[p]=tree[p*2]+tree[p*2+1];
}
int calc(int p,int l,int r,int x,int y)
{
// if(x>y) return 0;
// if(x==y&&x==0) return 0;
if(x<=l&&r<=y)
{
return tree[p];
}
int mid=l+r>>1;
int ans=0;
if(x<=mid) ans+=calc(p*2,l,mid,x,y);
if(y>mid) ans+=calc(p*2+1,mid+1,r,x,y);
return ans;
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
int x,y;
cin>>x>>y;
int num;
num=calc(1,1,maxn,0,x);
cnt[num]++;
//cout<<num[i]<<endl;
change(1,1,maxn,x);
}
for(int i=0;i<=n-1;i++)
{
cout<<cnt[i]<<endl;
}
}
边读入数据边change
因为读入顺序是从左到右,从下到上,所以每个星星左下的星星只有可能在它前面(这一点题目给了很大暗示),所以我们可以用线段树来维护这些点,线段树大小开成x的取值范围,然后每读入一个星星,先查询线段树中在x小于等于它的星星个数,是O(logn)复杂度的,这就是线段树的优势,然后查询完再把这个星星加入线段树。
线段树中比较难的好题:
牛牛的最美味和最不美味的零食:
牛牛为了减(吃)肥(好),希望对他的零食序列有更深刻的了解,所以他把他的零食排成一列,然后对每一个零食的美味程度都打了分,现在他有可能执行两种操作:
eat k:吃掉当前的第k个零食。右边的零食全部往左移动一位(编号减一)。
query i j:查询当前第i个零食到第j个零食里面美味度最高的和最低的零食的美味度。
#include<bits/stdc++.h>
using namespace std;
//https://ac.nowcoder.com/acm/contest/26896/1020
const int maxn=1000010;
int a[maxn];
struct node
{
int maxn,minn,num;
}tree[4*maxn];
void build(int p,int l,int r)
{
if(l==r)
{
tree[p].maxn=a[l];
tree[p].minn=a[l];
tree[p].num=1;
return;
}
int mid=l+r>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
tree[p].maxn=max(tree[p*2].maxn,tree[p*2+1].maxn);
tree[p].minn=min(tree[p*2].minn,tree[p*2+1].minn);
tree[p].num=tree[p*2].num+tree[p*2+1].num;
}
void dlt(int p,int l,int r,int pos)
{
if(l==r)
{
tree[p].maxn=-1e9-10;
tree[p].minn=1e9+10;
tree[p].num=0;
return;
}
int mid=l+r>>1;
if(pos<=mid) dlt(p*2,l,mid,pos);
if(pos>mid) dlt(p*2+1,mid+1,r,pos);
tree[p].maxn=max(tree[p*2].maxn,tree[p*2+1].maxn);
tree[p].minn=min(tree[p*2].minn,tree[p*2+1].minn);
tree[p].num=tree[p*2].num+tree[p*2+1].num;
}
node find(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return tree[p];
}
int mid=l+r>>1;
if(y<=mid) return find(p*2,l,mid,x,y);
if(x>mid) return find(p*2+1,mid+1,r,x,y);
node tree1,tree2,ans;
tree1=find(p*2,l,mid,x,mid);
tree2=find(p*2+1,mid+1,r,mid+1,y);
ans.maxn=max(tree1.maxn,tree2.maxn);
ans.minn=min(tree1.minn,tree2.minn);
return ans;
}
int calc(int p,int l,int r,int pos)//还原原始位置
{
if(l==r)
{
return l;
}
int mid=l+r>>1;
if(pos<=tree[p*2].num) return calc(p*2,l,mid,pos);//这里这个条件是本题重点
return calc(p*2+1,mid+1,r,pos-tree[p*2].num);
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
for(int i=1;i<=m;i++)
{
int q;
scanf("%d",&q);
if(q==1)
{
int pos;
scanf("%d",&pos);
int t=calc(1,1,n,pos);
dlt(1,1,n,t);
}
else{
int l,r;
scanf("%d%d",&l,&r);
int a=calc(1,1,n,l);
int b=calc(1,1,n,r);
node ans=find(1,1,n,a,b);
cout<<ans.minn<<' '<<ans.maxn<<'\n';
}
}
}
题目说要左移,而事实上不需要左移,我们只需要能够找到每次访问的那个数的初始位置就好了,删除操作也不是真正的删除掉。
具体如何实现:
用一个num来维护区间内剩多少零食(这是本题精髓)
删除的时候将具体到该零食的叶子的num值修改为0即可
然后判断左子树还是右子树时用tree[p*2].num与pos进行比较就可以了。
本题算是线段树单点修改比较经典的难题了,因为不是区间修改所以不需要lazy标记
好题:线段树+差分(利用线段树可以快速求前缀和的性质)
P1438 无聊的数列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5;
ll n,m;
ll a[maxn];
ll tree[4*maxn];
ll lazy[4*maxn];
ll cha[maxn];
void build(int p,int l,int r)
{
if(l==r)
{
tree[p]=cha[l];
return;
}
int mid=l+r>>1;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
tree[p]=tree[p*2]+tree[p*2+1];
}
void pushdown(int p,int l,int r)
{
int mid=l+r>>1;
tree[p*2]+=(mid-l+1)*lazy[p];
tree[p*2+1]+=(r-mid)*lazy[p];
lazy[p*2]+=lazy[p];
lazy[p*2+1]+=lazy[p];
lazy[p]=0;
}
void change(int p,int l,int r,int x,int y,ll num)
{
if(x<=l&&r<=y)
{
tree[p]+=(r-l+1)*num;
lazy[p]+=num;
return;
}
pushdown(p,l,r);
int mid=l+r>>1;
if(x<=mid) change(p*2,l,mid,x,y,num);
if(y>mid) change(p*2+1,mid+1,r,x,y,num);
tree[p]=tree[p*2]+tree[p*2+1];
}
ll calc(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return tree[p];
}
int mid=l+r>>1;
ll ans=0;
pushdown(p,l,r);
if(x<=mid) ans += calc(p*2,l,mid,x,y);
if(y>mid) return ans += calc (p*2+1,mid+1,r,x,y);
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=n;i>=1;i--) cha[i]=a[i]-a[i-1];
build(1,1,n);
for(int i=1;i<=m;i++)
{
int q;
cin>>q;
if(q==1)
{
ll l,r,k,d;
cin>>l>>r>>k>>d;
change(1,1,n,l,l,k);
if(l+1<=n) change(1,1,n,l+1,r,d);
if(r+1<=n) change(1,1,n,r+1,r+1,-(r-l)*d-k);//坑点
}
if(q==2)
{
int pos;
cin>>pos;
cout<<calc(1,1,n,1,pos)<<endl;
}
}
}
这里很考察对差分的掌握,在学枚举时用差分将区间修改改为端点修改,差分数组完全可以取代原数组,但是查询的时候复杂度还是O(n),然而,将它与线段树结合,查询立马将为O(logn),他们的结合是一个很强大的工具。
再有就是这道题线段树叶子不存原数组而存差分数组,因为等差数列在差分里有个特殊性质就是把等差数列加数列中会发现差分数组端点加等差数列首项,第二项到最后一项每项都加上公差,差分数组最后一项后面减去等差数列最后一项 ,这里尤其注意最后一项和第二项存不存在,写的时候wa了5,6发。
好题:贪婪大陆
P2184 贪婪大陆 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路:
一开始会想怎么用线段树把每个区间的雷数维护出来,会发现这很难,如果一个节点知道它两个儿子的雷数,是很难得到该节点的雷数,所以只能换一个思路,这又涉及到差分和前缀和的思想了,对于每个雷,我们记录它的起始位置和结束位置,然后我们在查询某个区间有多少雷的时候,其实就是:从1到r里面雷起始位置的数量-从1到(l-1)里面雷结束位置的数量,这个在草稿纸上写一下就知道,然后就是经典的线段树加前缀和去查询了。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
struct node
{
int jia,jian;
}tree[400010];
void change(int p,int l,int r,int pos,int num)
{
if(l==r)
{
if(num==1)
tree[p].jia++;
else tree[p].jian++;
return;
}
int mid=l+r>>1;
if(pos<=mid) change(p*2,l,mid,pos,num);
if(pos>mid) change(p*2+1,mid+1,r,pos,num);
tree[p].jia=tree[p*2].jia+tree[p*2+1].jia;
tree[p].jian=tree[p*2].jian+tree[p*2+1].jian;
}
int calcj(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return tree[p].jia;
}
int mid=l+r>>1;
int ans=0;
if(x<=mid) ans+=calcj(p*2,l,mid,x,y);
if(y>mid) ans+=calcj(p*2+1,mid+1,r,x,y);
return ans;
}
int calcji(int p,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return tree[p].jian;
}
int mid=l+r>>1;
int ans=0;
if(x<=mid) ans+=calcji(p*2,l,mid,x,y);
if(y>mid) ans+=calcji(p*2+1,mid+1,r,x,y);
return ans;
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int q;
cin>>q;
if(q==1)
{
int l,r;
cin>>l>>r;
change(1,1,n,l,1);
change(1,1,n,r,-1);
}
if(q==2)
{
int l,r;
cin>>l>>r;
if(l-1>=1) cout<<calcj(1,1,n,1,r)-calcji(1,1,n,1,l-1)<<endl;
else cout<<calcj(1,1,n,1,r)<<endl;
}
}
}
树状数组
树状数组三板斧:
lowbit(找到管辖长度):
int lowbit(int x)
{
return x&(-x);
}
add:
void add(int i,int val)
{
while(i<=n)
{
a[i]+=val;
i+=lowbit(i);
}
}
calc:
ll calc(int i)//返回前缀和
{
ll sum=0;
while(i>0)
{
sum+=a[i];
i-=lowbit(i);
}
return sum;
}
例题1:
思路:此题难点在于当D很小的时候要进行很多次add操作,所以可以将D以根号n为界限分开,当D小于根号n时,用数组ad记下来,让它+=K,然后在查询的时候再把它加上去。大于根号n直接循环去add就可以。
//https://ac.nowcoder.com/acm/contest/26896/1019
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int n,m;
ll ad[1001];
ll a[200010];
int lowbit(int x)
{
return x&(-x);
}
void add(int i,int val)
{
while(i<=n)
{
a[i]+=val;
i+=lowbit(i);
}
}
ll calc(int i)//返回前缀和
{
ll sum=0;
while(i>0)
{
sum+=a[i];
i-=lowbit(i);
}
return sum;
}
int main()
{
scanf("%d%d",&n,&m);
while(lowbit(n)!=n) n+=lowbit(n);
while(m--)
{
int q,x,y;
scanf("%d%d%d",&q,&x,&y);
if(q==1)
{
if(x*x<=n) ad[x]+=y;
else{
for(int j=x;j<=n;j+=x)
{
add(j,y);
}
}
}else{
ll sum=calc(y)-calc(x-1);
for(int i=1;i*i<=n;i++)
{
sum+=(y/i-(x-1)/i)*ad[i];
}
cout<<sum<<endl;
}
}
}