适用范围
ST表:
静态区间查询,并且仅仅适用于max,min,gcd等类型的区间查询。
树状数组
可以进行单点修该,区间查询(或者利用差分进行区间修改,单点查询)
常用于求前缀和。
线段树
都可以,但是我目前只会单点修改,区间查询。
ST表
天才的记忆
从前有个人名叫 WNB,他有着天才般的记忆力,他珍藏了许多许多的宝藏。
在他离世之后留给后人一个难题(专门考验记忆力的啊!),如果谁能轻松回答出这个问题,便可以继承他的宝藏。
题目是这样的:给你一大串数字(编号为 1
到 N,大小可不一定哦!),在你看过一遍之后,它便消失在你面前,随后问题就出现了,给你 M 个询问,每次询问就给你两个数字 A,B,要求你瞬间就说出属于 A 到 B
这段区间内的最大数。
一天,一位美丽的姐姐从天上飞过,看到这个问题,感到很有意思(主要是据说那个宝藏里面藏着一种美容水,喝了可以让这美丽的姐姐更加迷人),于是她就竭尽全力想解决这个问题。
但是,她每次都以失败告终,因为这数字的个数是在太多了!
于是她请天才的你帮他解决。如果你帮她解决了这个问题,可是会得到很多甜头的哦!
主要思路:
ST表模板题
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=2e5+10;
int maxa[N][30];
int a[N];
int n,m;
void init()
{
int k=floor((double)log(n)/log(2.0));
for(int j=1;j<=k;j++)
{
for(int i=1;i+(1<<j)-1<=n;i++)
{
maxa[i][j]=max(maxa[i][j-1],maxa[i+(1<<(j-1))][j-1]);
}
}
}
int query(int l,int r)
{
int k=floor((double)log(r-l+1)/log(2.0));
return max(maxa[l][k],maxa[r-(1<<k)+1][k]);
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
maxa[i][0]=a[i];
}
init();
cin>>m;
while(m--)
{
int l,r;
cin>>l>>r;
cout<<query(l,r)<<endl;
}
return 0;
}
与众不同
A 是某公司的 CEO,每个月都会有员工把公司的盈利数据送给 A,A 是个与众不同的怪人,A 不注重盈利还是亏本,而是喜欢研究「完美序列」:一段连续的序列满足序列中的数互不相同。
A 想知道区间 [L,R]
之间最长的完美序列长度。
主要思路:
这个问题有三个主要知识点:
dp,ST,二分。
利用简单的dp:
f
[
i
]
=
m
a
x
(
b
o
o
k
[
a
[
i
]
]
+
1
,
f
[
i
−
1
]
)
f[i]=max(book[a[i]]+1,f[i-1])
f[i]=max(book[a[i]]+1,f[i−1])
来维护以i为结尾的完美序列的端点坐标。
处理完以后利用ST表预处理一下,方便O(1)的查询。
至于为什么要二分呢?
因为以l为结尾的端点可能不在查询范围内,会使答案错误。
设las[x]表示盈利x最近出现位置。
设st[i]表示以第i个数结尾的最长完美序列的起始位置。
st[i]=max(st[i−1],las[a[i]+1])
设f[i]表示以第i个数结尾的最长完美序列的长度
f[i]=i−st[i]+1
由st的递推式可知,st的值是一个非递减的序列。
对于一个询问区间[li,ri],该区间内的st值可能会有两种情况:
左边一部分的st值不在区间内,即<li
右边一部分的st值不在区间内,即≥li
由于st的值具有单调性,所以这个边界可以通过二分得到。设求出的边界为mid_i,可得:
st[li…midi−1]<li
st[midi…ri]≥li
那么整个区间[li,ri]的最长完美序列的长度可以分两部分来求。
左边:很显然为midi−li
右边:MAX(mi…ri)
所以右边的长度要使用ST表,即RMQ来求。
整个问题的时间复杂度:
O((M+N)×logN)
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<map>
using namespace std;
const int N=2e5+10;
int f[N][50];
int st[N];
int n,m;
map<int,int> book;
void init()
{
int k=floor((double)log(n)/log(2));
for(int j=1;j<=k;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
int find(int l,int r)
{
int ll=l;
if(st[l]==l) return l;//恰好开头是开头
if(st[r]<l) return r+1;//没有完整的
while(l<r)
{
int mid=(l+r)>>1;
if(st[mid]<ll) l=mid+1;
else r=mid;
}
return l;
}
int query(int l,int r)
{
int k=floor((double)log(r-l+1)/log(2));
return max(f[l][k],f[r-(1<<k)+1][k]);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
st[i]=max(st[i-1],book[x]+1);
f[i][0]=i-st[i]+1;
book[x]=i;
}
init();
while(m--)
{
int l,r;
cin>>l>>r;
l++,r++;
int m=find(l,r);
int ans=0;
if(m>l) ans=m-l;
if(m<=r)
{
int cnt=query(m,r);
ans=max(ans,cnt);
}
cout<<ans<<endl;
}
}
树状数组
楼兰图腾
主要思路:
利用树状数组快速求解出前缀和。
首先将两个符号抽象化,V即为分别求每个点左边大于他的节点数*右边大于的节点数 的和。
那么重点就在于如何求解需要的个数。
乍一看感觉有点单调栈的感觉,但是再进一步思考,这个题目与直方图那道题还是有很大的不同的,
直方图要求连续,所以可以用单调栈维护,但是该题的点可以是离散的,所以正向逆向两边树状数组求解该问题。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=2e5+10;
ll a[N],tr[N];
ll ggreater[N],lower[N];
int 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 ans=0;
for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
return ans;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
int y=a[i];
ggreater[i]=sum(n)-sum(y);//右边
lower[i]=sum(y-1);
add(y,1);
}
ll ans1=0,ans2=0;
memset(tr,0,sizeof(tr));
for(int i=n;i>0;i--)
{
int y=a[i];
ans1+=ggreater[i]*(ll)(sum(n)-sum(y));
ans2+=lower[i]*(ll)sum(y-1);
add(y,1);
}
cout<<ans1<<' '<<ans2<<endl;
return 0;
}
一个简单的整数问题
给定长度为 N 的数列 A,然后输入 M行操作指令。
第一类指令形如 C l r d,表示把数列中第 l∼r个数都加 d。
第二类指令形如 Q x,表示询问数列中第 x个数的值。
对于每个询问,输出一个整数表示答案。
主要思路:
利用差分+树状数组轻松求解该题。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
typedef long long ll;
ll tr[N];
ll a[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;
}
ll sum(int x)
{
ll ans=0;
for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
add(i,a[i]-a[i-1]);
}
while(m--)
{
char op;
cin>>op;
if(op=='C')
{
int l,r,d;
cin>>l>>r>>d;
add(l,d);
add(r+1,-d);
}
else
{
int x;
cin>>x;
cout<<sum(x)<<endl;
}
}
return 0;
}
一个简单的整数问题2
给定一个长度为 N 的数列 A,以及 M条指令,每条指令可能是以下两种之一:
C l r d,表示把 A[l],A[l+1],…,A[r]都加上 d。
Q l r,表示询问数列中第 l∼r个数的和。
对于每个询问,输出一个整数表示答案。
主要思路:
这个问题一看,是区间和的查询,感觉不能再使用树状数组来求解了,但是仔细分析题目可以发现:
a
1
=
b
i
a_1=b_i
a1=bi
a
2
=
b
1
+
b
2
a_2=b_1+b_2
a2=b1+b2
a
3
=
b
1
+
b
2
+
b
3
a_3=b_1+b_2+b_3
a3=b1+b2+b3
a
4
=
b
1
+
b
2
+
b
3
+
b
4
a_4=b_1+b_2+b_3+b_4
a4=b1+b2+b3+b4
缺失的部分再加上一行,缺失部分的值为:
i
∗
b
i
+
(
i
−
1
)
∗
b
i
−
1
…
…
i*b_i+(i-1)*b_{i-1}……
i∗bi+(i−1)∗bi−1……
所以可以再维护一个
i
∗
b
i
i*b_i
i∗bi的树状数组,求出前缀和即为缺失的部分。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10;
typedef long long ll;
int a[N];
ll tr1[N],tr2[N];
int n,m;
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 ans=0;
for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
return ans;
}
ll get_sum(int x)
{
ll ans=0;
ans=sum(tr1,x)*(x+1)-sum(tr2,x);
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
add(tr1,i,a[i]-a[i-1]);
add(tr2,i,(ll)i*(a[i]-a[i-1]));
}
while(m--)
{
int l,r,x;
char op;
cin>>op;
if(op=='Q')
{
cin>>l>>r;
ll ans=get_sum(r)-get_sum(l-1);
cout<<ans<<endl;
}
else
{
cin>>l>>r>>x;
add(tr1,l,x);
add(tr1,r+1,-x);
add(tr2,l,(ll)l*x);
add(tr2,r+1,(ll)(-x)*(r+1));
}
}
return 0;
}
谜一样的牛
有 n 头奶牛,已知它们的身高为 1∼n且各不相同,但不知道每头奶牛的具体身高。
现在这 n头奶牛站成一列,已知第 i 头牛前面有 Ai 头牛比它低,求每头奶牛的身高。
主要思路:
首先需要仔细分析题目,如何确定一个牛的身高。
思考后发现,最后一个牛的最低身高是可以确定的。
因为前边有x个比他矮的,所以他的身高为x+1.
那么这个问题是如何利用树状数组求解的呢?
可以先把1~n上边的每一个数都加上1,然后从后边开始遍历。
每个数都代表他前边含有多少个比他矮的牛,直接二分,寻找前缀和
≥
m
i
d
+
1
\geq mid+1
≥mid+1的点,然后将该点的值-1,一直往前搜索,然后存储一下每头牛的身高即可。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int tr[N];
int n;
int a[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 ans=0;
for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
return ans;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
add(i,1);
for(int i=2;i<=n;i++)
cin>>a[i];
for(int i=n;i>0;i--)
{
int l=1,r=n;
while(l<r)
{
int mid=(l+r)>>1;
if(sum(mid)>=a[i]+1) r=mid;
else l=mid+1;
}
a[i]=l;
add(l,-1);
}
for(int i=1;i<=n;i++)
{
cout<<a[i]<<endl;
}
return 0;
}
线段树
最大数
给定一个正整数数列 a1,a2,…,an,每一个数都在 0∼p−1之间。
可以对这列数进行两种操作:
添加操作:向序列后添加一个数,序列长度变成 n+1;
询问操作:询问这个序列中最后 L个数中最大的数是多少。
程序运行的最开始,整数序列为空。
一共要对整数序列进行 m次操作。
写一个程序,读入操作的序列,并输出询问操作的答案。
主要思路:
最最最简单的线段树。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=2e5+10;
int m,p;
struct node
{
int l,r;
int v;
}t[4*N];
void pushup(int u)
{
t[u].v=max(t[u<<1].v,t[u<<1|1].v);
}
void build(int u,int l,int r)
{
t[u]={l,r};
if(l==r) return;
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
}
void modify(int u,int x,int v)
{
if(t[u].l==x&&t[u].r==x) t[u].v=v;
else
{
int mid=t[u].l+t[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
int query(int u,int l,int r)
{
if(t[u].l>=l&&t[u].r<=r) return t[u].v;
int v=0;
int mid=t[u].l+t[u].r>>1;
if(l<=mid) v=query(u<<1,l,r);
if(r>mid) v=max(v,query(u<<1|1,l,r));
return v;
}
int main()
{
int n=0,last=0;
cin>>m>>p;
build(1,1,m);
char op;
int x;
while(m--)
{
cin>>op>>x;
if(op=='Q')
{
last=query(1,n-x+1,n);
cout<<last<<endl;
}
else
{
n++;
modify(1,n,(last+x)%p);
}
}
return 0;
}
GSS1
给出了序列 A[1],A[2],…,A[N]A[1],A[2],…,A[N] 。 (a[i]≤15007,1≤N≤50000a[i]≤15007,1≤N≤50000 )。查询定义如下: 查询 ( x , y ) = max { a [ i ] + a [ i + 1 ] + . . . + a [ j ] ; x ≤ i ≤ j ≤ y } ( x , y ) = m a x a [ i ] + a [ i + 1 ] + . . . + a [ j ] ; x ≤ i ≤ j ≤ y (x,y)=\max\{a[i]+a[i+1]+...+a[j];x≤i≤j≤y\}(x,y)=max{a[i]+a[i+1]+...+a[j];x≤i≤j≤y} (x,y)=max{a[i]+a[i+1]+...+a[j];x≤i≤j≤y}(x,y)=maxa[i]+a[i+1]+...+a[j];x≤i≤j≤y。 给定M个查询,程序必须输出这些查询的结果。
主要思路:
找最大子段和,主要就是pushup函数的编写。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=5e5+10;
struct node
{
int l,r;
int sum,lmax,rmax,tmax;
}t[N*4];
int w[N];
int n,m;
void pushup(node &u,node &l,node &r)//用子更新父
{
u.sum=l.sum+r.sum;
u.lmax=max(l.lmax,l.sum+r.lmax);
u.rmax=max(r.rmax,r.sum+l.rmax);
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
pushup(t[u],t[u<<1],t[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r) t[u]={l,r,w[l],w[l],w[l],w[l]};
else
{
t[u]={l,r};
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int x,int v)
{
if(t[u].l==x&&t[u].r==x) t[u]={x,x,v,v,v,v};
else
{
int mid=t[u].l+t[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
node query(int u,int l,int r)
{
if(t[u].l>=l&&t[u].r<=r) return t[u];
else
{
int mid=t[u].l+t[u].r>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
node left=query(u<<1,l,r);
node right=query(u<<1|1,l,r);
node ans;
pushup(ans,left,right);
return ans;
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>w[i];
build(1,1,n);
cin>>m;
while(m--)
{
int x,y;
cin>>x>>y;
if(x>y) swap(x,y);
cout<<query(1,x,y).tmax<<endl;
}
return 0;
}
GSS3
给定长度为 N 的数列 A,以及 M条指令,每条指令可能是以下两种之一:
1 x y,查询区间 [x,y]中的最大连续子段和,即
m
a
x
x
≤
l
≤
r
≤
y
∑
i
=
l
r
A
[
i
]
maxx≤l≤r≤y{∑i=lrA[i]}
maxx≤l≤r≤y∑i=lrA[i]。
2 x y,把 A[x]改成 y 。
对于每个查询指令,输出一个整数表示答案。
主要思路:
比1多了一个单点修改。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=5e5+10;
struct node
{
int l,r;
int sum,lmax,rmax,tmax;
}t[N*4];
int w[N];
int n,m;
void pushup(node &u,node &l,node &r)//用子更新父
{
u.sum=l.sum+r.sum;
u.lmax=max(l.lmax,l.sum+r.lmax);
u.rmax=max(r.rmax,r.sum+l.rmax);
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
pushup(t[u],t[u<<1],t[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r) t[u]={l,r,w[l],w[l],w[l],w[l]};
else
{
t[u]={l,r};
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int x,int v)
{
if(t[u].l==x&&t[u].r==x) t[u]={x,x,v,v,v,v};
else
{
int mid=t[u].l+t[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
node query(int u,int l,int r)
{
if(t[u].l>=l&&t[u].r<=r) return t[u];
else
{
int mid=t[u].l+t[u].r>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
node left=query(u<<1,l,r);
node right=query(u<<1|1,l,r);
node ans;
pushup(ans,left,right);
return ans;
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i];
build(1,1,n);
while(m--)
{
int k,x,y;
cin>>k>>x>>y;
if(k==1)
{
if(x>y) swap(x,y);
cout<<query(1,x,y).tmax<<endl;
}
else
{
modify(1,x,y);
}
}
return 0;
}
GSS5
给定一个序列。查询左端点在
[
x
1
,
y
1
]
[x_1, y_1]
[x1,y1] 之间,且右端点在
[
x
2
,
y
2
]
[x_2, y_2]
[x2,y2] 之间的最大子段和,
数据保证
x
1
≤
x
2
,
y
1
≤
y
2
x_1\leq x_2,y_1\leq y_2
x1≤x2,y1≤y2 ,但是不保证端点所在的区间不重合。
主要思路:
难点是查询时要分情况讨论。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e4+10;
struct node
{
int l,r;
int sum,lmax,rmax,tmax;
}t[N*4],tree;
int n,m;
int w[N];
void pushup(node &u,node &l,node &r)
{
u.sum=l.sum+r.sum;
u.lmax=max(l.lmax,l.sum+r.lmax);
u.rmax=max(r.rmax,r.sum+l.rmax);
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
pushup(t[u],t[u<<1],t[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r) t[u]={l,r,w[l],w[l],w[l],w[l]};
else
{
t[u]={l,r};
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int x,int v)
{
if(t[u].l==x&&t[u].r==x) t[u]={x,x,v,v,v,v};
else
{
int mid=t[u].l+t[u].r>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
node query(int u,int l,int r)
{
if(l>r) return tree;
if(t[u].l>=l&&t[u].r<=r) return t[u];
int mid=t[u].l+t[u].r>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
else
{
node left=query(u<<1,l,r);
node right=query(u<<1|1,l,r);
node ans;
pushup(ans,left,right);
return ans;
}
}
int main()
{
int t;
cin>>t;
while(t--)
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>w[i];
build(1,1,n);
cin>>m;
while(m--)
{
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
int ans=0;
if(r1<l2)
{
ans+=query(1,r1,l2).sum;
ans+=max(0,query(1,l1,r1-1).rmax);
ans+=max(0,query(1,l2+1,r2).lmax);
}
else
{
ans=query(1,l2,r1).tmax;
ans=max(ans,query(1,l1,l2-1).rmax+query(1,l2,r2).lmax);
ans=max(ans,query(1,l1,r1).rmax+query(1,r1+1,r2).lmax);
ans=max(ans,query(1,l2,r1).sum+max(0,query(1,l1,l2-1).rmax)+max(0,query(1,r1+1,r2).lmax));
}
cout<<ans<<endl;
}
}
return 0;
}