b站复习:noip线段树与树状数组_哔哩哔哩_bilibili
问题一:
有一个长度为 为n的序列,m次操作,将某个数修改为v,询问下标区间[l,r]的所有数之和?
l和r表示该节点代表的区间,v表示维护这个区间的信息,如区间和
struct node{
int l,r,v;
}sum[4*N];
注:一般要开到4N
更新信息:
void update(int x)
{
sum[x]=sum[x<<1]+sum[x<<1+1];
}
建立线段树 (自顶向下)
void build(int l,int r,int x)
{
if(l==r){//叶子节点
sum[x]=a[l];
return ;
}
int mid=(l+r)>>1;
build(l,mid,x<<1);
build(mid+1,r,x<<1+1);//表示左右子树的信息已经被更新好了
update(x); //更新信息
}
查询
int query(int A,int B,int l,int r,int x)
{
if(A<=l&&r<=B)//l到r完全落在要查询区间内
return sum[x];
int mid=(l+r)>>1,ans=0;
if(A<=mid)
ans+=query(A,B,l,mid,x<<1);
if(mid<B)
ans+=query(A,B,mid+1,r,x<<1+1);
return ans;
}
单点修改
把pos这个位置节点值改成v,l,r,表示当前节点对应区间,x表示当前节点编号
void change(int pos,int v,int l,int r,int x)
{
if(l==r)
{
sum[x]=v;
return ;
}
int mid=(l+r)>>1;
if(pos<=mid) change(pos,v,l,mid,x<<1);
else change(pos,v,mid+1,r,x<<1+1);
update(x);
}
给定一个序列,求该序列逆序对的数量,其中0<=a[i]<=10^9
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=500010;
int a[N],b[N];
int sum[4*N];
void update(int x)
{
sum[x]=sum[x*2]+sum[x*2+1];
}
void build(int l,int r,int x)
{
if(l==r){
sum[x]=0;
return ;
}
int mid=(l+r)>>1;
build(l,mid,x*2);
build(mid+1,r,x*2+1);
update(x);
}
int query(int A,int B,int l,int r,int k)
{
if(A<=l&&r<=B) return sum[k];
int mid=(l+r)>>1,ans=0;
if(A<=mid) ans+=query(A,B,l,mid,k*2);
if(mid<B) ans+=query(A,B,mid+1,r,k*2+1);
update(k);
return ans;
}
void change(int pos,int l,int r,int k)
{
if(l==r)
{
sum[k]++;
return ;
}
int mid=(l+r)>>1;
if(pos>mid) change(pos,mid+1,r,k*2+1);
else change(pos,l,mid,k*2);
update(k);
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
//离散化
int cnt=0;
for(int i=1;i<=n;i++)
b[i]=a[i];
sort(b+1,b+n+1);
cnt=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+cnt+1,a[i]) -b;
build(1,n,1);
long long ans=0;
for(int i=1;i<=n;i++)
{
ans+=query(a[i]+1,n,1,n,1);
change(a[i],1,n,1);
}
cout<<ans<<endl;
return 0;
}
unique:将重复元素排在序列末尾,保证前面都是不重复元素,返回第一个不重复元素地址
lower_bound:返回序列中不小于a[i]的最小的元素的地址
例题
P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define ls (x<<1)
#define rs (x<<1|1)
const int N=100010;
long long sum[4*N],tag[4*N],a[N];
void update(int x)
{
sum[x]=sum[ls]+sum[rs];
}
void build(int l,int r,int x)
{
if(l==r)
{
sum[x]=a[l];
return ;
}
int mid=(l+r)>>1;
build(l,mid,ls);
build(mid+1,r,rs);
update(x);
}
void down(int l,int r,int x)
{
int mid=(l+r)>>1;
if(tag[x]!=0)
{
tag[ls]+=tag[x];
tag[rs]+=tag[x];
sum[ls]+=(mid-l+1)*tag[x];
sum[rs]+=(r-mid)*tag[x];
tag[x]=0;
}
}
void add(int A,int B,int v,int l,int r,int x)
{
if(A<=l&&r<=B)
{
tag[x]+=v;
sum[x]+=v*(r-l+1);
return ;
}
down(l,r,x);
int mid=(l+r)>>1;
if(A<=mid) add(A,B,v,l,mid,ls);
if(mid<B) add(A,B,v,mid+1,r,rs);
update(x);
}
long long query(int A,int B,int l,int r,int x)
{
if(A<=l&&r<=B)
return sum[x];
down(l,r,x);
long long mid=(l+r)>>1;
long long ans=0;
if(A<=mid) ans+=query(A,B,l,mid,ls);
if(mid<B) ans+=query(A,B,mid+1,r,rs);
return ans;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
build(1,n,1);
while(m--)
{
int t,x,y,k;
scanf("%d",&t);
if(t==1){
scanf("%d%d%d",&x,&y,&k);
add(x,y,k,1,n,1);
}
else{
scanf("%d%d",&x,&y);
cout<<query(x,y,1,n,1)<<endl;
}
}
return 0;
}
问题二:
有一个长度为 为n的序列,m次操作,将某个区间修改为v,询问下标区间[l,r]的所有数之和?
定义ls,rs分别为左右子树的编号
#define ls(x<<1)
#define rs(x<<1|1)
更新信息
void update(int x)
{
sum[x]=sum[ls]+sum[rs];
}
标记下传
(需要的时候才修改)O(logn)
tag[x]不为0则表示以下节点需要修改
void down(int l,int r,int x)
{
int mid=(l+r)>>1;
if(tag[x]>0){
tag[ls]=tag[rs]=tag[x];
sum[ls]=(mid-l+1)*tag[x];
sum[rs]=(r-mid)*tag[x];
tag[x]=0;//表示修改完成
}
}
修改区间[A,B]为v
void change(int A,int B,int l,int r,int x,int v)
{
if(A<=l&&r<=B)
{
tag[x]=v;
sum[x]=v*(r-l+1);
return ;
}
down(l,r,x);//在修改前检查是否要下传标记
int mid=(l+r)>>1;
if(A<=mid) change(A,B,v,l,mid,ls);
if(mid<B) change(A,B,v,mid+1,r,rs);
update(x);
}
查询[A,B]的区间和
int query(int A,int B,int l,int r,int x)
{
if(A<=l&&r<=B)
return sum[x];
down(l,r,x);//在修改前检查是否要下传标记
int mid(l+r)>>1,ans=0;
if(A<=mid) ans+=query(A,B,l,mid,ls);
if(mid<B) ans+=query(A,B,mid+1,r,rs);
//update(x);//下传标记不影响这里的值
return ans;
}
改编1(区间加)
有一个长度为 为n的序列,m次操作,将某个区间加上v,询问下标区间[l,r]的所有数之和?
只需修改两个函数:
void down(int l,int r,int x)
{
int mid=(l+r)>>1;
if(tag[x]>0){
tag[ls]+=tag[x];
tag[rs]+=tag[x];
sum[ls]+=(mid-l+1)*tag[x];
sum[rs]+=(r-mid)*tag[x];
tag[x]=0;//表示修改完成
}
}
区间[A,B]+v :
void change(int A,int B,int l,int r,int v,int x)
{
if(A<=l&&r<=B)
{
tag[x]+=v;
sum[x]+=v*(r-l+1);
return ;
}
down(l,r,x);//在修改前检查是否要下传标记
int mid=(l+r)>>1;
if(A<=mid) change(A,B,v,l,mid,ls);
if(mid<B) change(A,B,v,mid+1,r,rs);
update(x);
}
例题:P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define ls (x<<1)
#define rs (x<<1|1)
const int N=100010;
long long sum[4*N],tag[4*N],a[N];
void update(int x)
{
sum[x]=sum[ls]+sum[rs];
}
void build(int l,int r,int x)
{
if(l==r)
{
sum[x]=a[l];
return ;
}
int mid=(l+r)>>1;
build(l,mid,ls);
build(mid+1,r,rs);
update(x);
}
void down(int l,int r,int x)
{
int mid=(l+r)>>1;
if(tag[x]!=0)
{
tag[ls]+=tag[x];
tag[rs]+=tag[x];
sum[ls]+=(mid-l+1)*tag[x];
sum[rs]+=(r-mid)*tag[x];
tag[x]=0;
}
}
void add(int A,int B,int v,int l,int r,int x)
{
if(A<=l&&r<=B)
{
tag[x]+=v;
sum[x]+=v*(r-l+1);
return ;
}
down(l,r,x);
int mid=(l+r)>>1;
if(A<=mid) add(A,B,v,l,mid,ls);
if(mid<B) add(A,B,v,mid+1,r,rs);
update(x);
}
long long query(int A,int B,int l,int r,int x)
{
if(A<=l&&r<=B)
return sum[x];
down(l,r,x);
long long mid=(l+r)>>1;
long long ans=0;
if(A<=mid) ans+=query(A,B,l,mid,ls);
if(mid<B) ans+=query(A,B,mid+1,r,rs);
return ans;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
build(1,n,1);
while(m--)
{
int t,x,y,k;
scanf("%d",&t);
if(t==1){
scanf("%d%d%d",&x,&y,&k);
add(x,y,k,1,n,1);
}
else{
scanf("%d%d",&x,&y);
cout<<query(x,y,1,n,1)<<endl;
}
}
return 0;
}
改编2(区间加+查区间最小值)
有一个长度为 为n的序列,m次操作,将某个区间加上v,询问下标区间[l,r]的最小值?
在改编一上再修改:
只需修改两个函数:
void update(int x)
{
Min[x]=min(Min[ls]+Min[rs]);
}
void down(int l,int r,int x)
{
int mid=(l+r)>>1;
if(tag[x]>0){
tag[ls]+=tag[x];
tag[rs]+=tag[x];
Min[ls]+=tag[x];
Min[rs]+=tag[x];
tag[x]=0;//表示修改完成
}
}
void change(int A,int B,int l,int r,int x)
{
if(A<=l&&r<=B)
{
tag[x]+=v;
Min[x]+=v;
return ;
}
down(l,r,x);//在修改前检查是否要下传标记
int mid=(l+r)>>1;
if(A<=mid) change(A,B,v,l,mid,ls);
if(mid<B) change(A,B,v,mid+1,r,rs);
update(x);
}
查询的时候ans=0x3f3f3f3f
改编3(区间加+查区间最小值个数)
有一个长度为 为n的序列,m次操作,将某个区间加上v,询问下标区间[l,r]的最小值的个数?
更新信息
void update(int x)
{
Min[x]=min(Min[ls]+Min[rs]);
if(Min[ls]==Min[rs]){
Min[x]=Min[ls];
cnt[x]=cnt[ls]+cnt[rs];
}
else{
if(Min[ls]<Min[rs])
Min[x]=Min[ls],cnt[x]=cnt[ls];
else
Min[x]=Min[rs],cnt[x]=cnt[rs];
}
}
//查询[A,B]的区间和
pair<int,int> query(int A,int B,int l,int r,int x)
{
if(A<=l&&r<=B)
return make_pair(Min[x],cnt[x]);
down(l,r,x);//在修改前检查是否要下传标记
int mid(l+r)>>1;
ans=make_pair(0x3f3f3f3f,0);
if(A<=mid) ans=min(ans,query(A,B,l,mid,ls))
if(mid<B)
{
pair<int,int> tmp=min(ans,query(A,B,mid+1,r,rs));
if(tmp.first==ans.first) ans.second+=tmp.second;//最小值相等
else if(tmp.first<ans.first) ans=tmp;
}
return ans;
}
make_pair:无需写出型别,直接生成pair对象
其余同改编2