树状数组支持单点修改和区间查询,想要区间修改一般需要差分,不过代码简洁。
线段树相比于树状数组,可以利用懒标记进行区间修改,但是代码比较长。
下面是两道最基础的题目:
单点修改查询区间和:https://ac.nowcoder.com/acm/problem/15164
区间修改查询区间和:Tree Recovery (nowcoder.com)
下面并不涉及到可持久化线段树、DDP等。
1.二维树状数组
和一维树状数组类似,只是多了一层循环。
void add(int x,int y,int c)
{
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=n;j+=lowbit(j)) tr[i][j]+=c;
}
int sum(int x,int y)
{
int res=0;
for(int i=x;i;i-=lowbit(i))
for(int j=y;j;j-=lowbit(j)) res+=tr[i][j];
return res;
}
2.区间查询
一般来说,线段树的区间具有可合并性,如左区间的最大值与右区间的最大值中更大的那个是左区间与右区间合并后的最大值。不过在许多题目中合并关系往往没有这么简单,需要进行一些推导。
本题要求求区间元素的平方和,假设修改前的区间为[a,b],区间和为,平方和为。如果先乘c再加上d之后,区间变为[a*c+d,b*c+d],平方和为
因此,我们需要用线段树同时维护区间和与区间平方和,然后按照上述式子合并即可。如果只执行乘操作,那么令d=0即可;同理,如果只进行加操作,令c=1即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=200010;
int n,m,w[N];
struct Node{
int l,r;
ll sum,sqsum,add,mul;
}tr[N*4];
void pushup(int u)
{
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
tr[u].sqsum=tr[u<<1].sqsum+tr[u<<1|1].sqsum;
}
ll query(int u,int l,int r);
void eval(Node &t,int add,int mul)
{
t.sqsum=t.sqsum*mul*mul+2ll*t.sum*add+(ll)(t.r-t.l+1)*add*add;
t.sum=(ll)t.sum*mul+(ll)(t.r-t.l+1)*add;
t.mul=(ll)t.mul*mul;
t.add=(ll)t.add*mul+add;
}
void pushdown(int u)
{
eval(tr[u<<1],tr[u].add,tr[u].mul);
eval(tr[u<<1|1],tr[u].add,tr[u].mul);
tr[u].add=0,tr[u].mul=1;
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],w[r]*w[r],0,1};
else
{
tr[u]={l,r,0,0,0,1};
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int l,int r,int add,int mul)
{
if(tr[u].l>=l&&tr[u].r<=r) eval(tr[u],add,mul);
else
{
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,add,mul);
if(r>mid) modify(u<<1|1,l,r,add,mul);
pushup(u);
}
}
ll query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
ll sum=0;
if(l<=mid) sum=query(u<<1,l,r);
if(r>mid) sum+=query(u<<1|1,l,r);
return sum;
}
ll query2(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sqsum;
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
ll sqsum=0;
if(l<=mid) sqsum=query2(u<<1,l,r);
if(r>mid) sqsum+=query2(u<<1|1,l,r);
return sqsum;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
while(m--)
{
int op,l,r,d;
cin>>op>>l>>r;
if(op==1) cout<<query(1,l,r)<<endl;
else if(op==2) cout<<query2(1,l,r)<<endl;
else if(op==3)
{
cin>>d;
modify(1,l,r,0,d);
}
else if(op==4)
{
cin>>d;
modify(1,l,r,d,1);
}
}
}
本题要求区间正弦函数的和,同理设区间为[a,b],加数为d,原序列结果为,推导得
因此,还需要维护余弦函数的和,其加上d后结果为
#include<bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int N=200010;
ll n,p,m,w[N];
struct Node{
int l,r;
double sinsum,cossum;
ll add;
}tr[N*4];
void pushup(int u)
{
tr[u].sinsum=tr[u<<1].sinsum+tr[u<<1|1].sinsum;
tr[u].cossum=tr[u<<1].cossum+tr[u<<1|1].cossum;
}
void eval(Node &t,ll add)
{
double tsin=t.sinsum,tcos=t.cossum;
t.cossum=t.cossum*cos(add*1.0)-tsin*sin(add*1.0);
t.sinsum=t.sinsum*cos(add*1.0)+tcos*sin(add*1.0);
t.add+=add;
}
void pushdown(int u)
{
eval(tr[u<<1],tr[u].add);
eval(tr[u<<1|1],tr[u].add);
tr[u].add=0;
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,sin(w[r]*1.0),cos(w[r]*1.0),0};
else
{
tr[u]={l,r,0,1,0};
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int l,int r,ll add)
{
if(tr[u].l>=l&&tr[u].r<=r) eval(tr[u],add);
else
{
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,add);
if(r>mid) modify(u<<1|1,l,r,add);
pushup(u);
}
}
double query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sinsum;
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
double sum=0;
if(l<=mid) sum=query(u<<1,l,r);
if(r>mid) sum+=query(u<<1|1,l,r);
return sum;
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++) scanf("%lld",&w[i]);
build(1,1,n);
scanf("%lld",&m);
while(m--)
{
int op,l,r;
ll d;
scanf("%d%d%d",&op,&l,&r);
if(op==1)
{
scanf("%lld",&d);
modify(1,l,r,d);
}
else printf("%.1f\n",query(1,l,r));
}
}
本题要求区间最大子段和,需要维护三个信息:最大前缀和、最大后缀和、最大子段和。
#include<bits/stdc++.h>
using namespace std;
const int N=500010;
int n,m,w[N];
struct Node
{
int l,r,sum,lmax,rmax,tmax;
}tr[4*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({l.tmax,r.tmax,l.rmax+r.lmax});
}
void pushup(int u)
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],w[r],w[r],w[r]};
else
{
tr[u].l=l,tr[u].r=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(tr[u].l==x&&tr[u].r==x) tr[u]={x,x,v,v,v,v};
else
{
int mid=tr[u].l+tr[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(tr[u].l>=l&&tr[u].r<=r) return tr[u];
else
{
int mid=tr[u].l+tr[u].r>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
else
{
auto left=query(u<<1,l,r);
auto right=query(u<<1|1,l,r);
Node res;
pushup(res,left,right);
return res;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
int op,x,y;
while(m--)
{
cin>>op>>x>>y;
if(op==1)
{
if(x>y) swap(x,y);
cout<<query(1,x,y).tmax<<endl;
}
else modify(1,x,y);
}
}
设原数组为[a,b,c,d],结果。经过乘法与加法后变为[ae+f,be+f,ce+f,de+f],结果为
由多项式可知,每两项组合时必定会产生一项,那么一共会产生项;对于每个元素原来的值一共会和除自己以外的项组合,那么通项公式为
同时,在合并两个区间时,左右两个区间与合并后区间的关系为
在代码实现的时候尽量使用1log的查询,否则可能会超时。运算符重载是个不错的方法。
#include<bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int N=100010;
ll t,n,m,w[N],p;
inline void read(ll& a)
{
ll s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') w=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
s=s*10+ch-'0';
ch=getchar();
}
a=s*w;
}
struct Node{
int l,r;
ll sum,sqsum,add,mul;
friend Node operator +(const Node &x,const Node &y){
Node z;
z.sum=(x.sum+y.sum)%p;
z.sqsum=((x.sqsum+y.sqsum)%p+
x.sum*y.sum%p)%p;
return z;
}
}tr[N*4];
void pushup(int u)
{
tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%p;
tr[u].sqsum=(tr[u<<1].sqsum+tr[u<<1|1].sqsum+tr[u<<1].sum*tr[u<<1|1].sum%p)%p;
}
void eval(Node &t,ll add,ll mul)
{
t.sqsum=(t.sqsum*mul%p*mul%p+(ll)(t.r-t.l)*t.sum%p*mul%p*add%p+(ll)((t.r-t.l+1)*(t.r-t.l)/2)%p*add%p*add%p)%p;
t.sum=((ll)t.sum*mul%p+(ll)(t.r-t.l+1)*add%p)%p;
t.mul=(ll)t.mul*mul%p;
t.add=((ll)t.add*mul%p+add)%p;
}
void pushdown(int u)
{
eval(tr[u<<1],tr[u].add,tr[u].mul);
eval(tr[u<<1|1],tr[u].add,tr[u].mul);
tr[u].add=0,tr[u].mul=1;
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],0,0,1};
else
{
tr[u]={l,r,0,0,0,1};
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int l,int r,ll add,ll mul)
{
if(tr[u].l>=l&&tr[u].r<=r) eval(tr[u],add,mul);
else
{
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,add,mul);
if(r>mid) modify(u<<1|1,l,r,add,mul);
pushup(u);
}
}
Node query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r)
return tr[u];
pushdown(u);
int mid=(tr[u].l+tr[u].r)>>1;
Node res;
if(l>mid){
return query(u<<1|1,l,r);
}
else if(r<=mid){
return query(u<<1,l,r);
}
else{
res=query(u<<1,l,r)+query(u<<1|1,l,r);
return res;
}
}
void solve()
{
read(n),read(m),read(p);
for(int i=1;i<=n;i++) read(w[i]),w[i]%=p;
build(1,1,n);
while(m--)
{
ll op,l,r,d;
read(op),read(l),read(r);
if(op==1)
{
read(d);
modify(1,l,r,d,1);
}
else if(op==2)
{
read(d);
modify(1,l,r,0,d);
}
else
{
Node res=query(1,l,r);
printf("%lld\n",res.sqsum%p);
}
}
}
int main()
{
cin>>t;
while(t--) solve();
}
3.懒标记修改优先级
在使用多种懒标记时,更新懒标记的顺序不同可能会产生不同的影响。
https://www.luogu.com.cn/problem/P1253
本题需要两个懒标记:覆盖标记和增加标记
增加标记还是和以前一样,但是需要注意,一旦使用覆盖标记,需要将该区间内的增加标记清空,因为将区间值覆盖以后,相当于覆盖以前做的增加操作就全部作废了。因此,在pushdown函数中,覆盖标记的优先级要高于增加标记。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1000010;
const ll inf=1e18+1;
ll n,m,w[N];
struct Node{
int l,r;
ll v,add,cover;
}tr[N*4];
void pushup(int u)
{
tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}
void coverdown(int u)
{
auto &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
if(root.cover!=inf)
{
left.add=0;
left.v=root.cover;
left.cover=root.cover;
right.add=0;
right.v=root.cover;
right.cover=root.cover;
root.cover=inf;
}
}
void adddown(int u)
{
auto &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
if(root.add)
{
coverdown(u);
left.add+=root.add;
left.v+=root.add;
right.add+=root.add;
right.v+=root.add;
root.add=0;
}
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],0,inf};
else
{
tr[u]={l,r,0,0,inf};
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify1(int u,int l,int r,ll cover)
{
if(tr[u].l>=l&&tr[u].r<=r)
{
tr[u].v=cover;
tr[u].cover=cover;
tr[u].add=0;
}
else
{
coverdown(u);
adddown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify1(u<<1,l,r,cover);
if(r>mid) modify1(u<<1|1,l,r,cover);
pushup(u);
}
}
void modify2(int u,int l,int r,ll add)
{
if(tr[u].l>=l&&tr[u].r<=r)
{
coverdown(u);
tr[u].v+=add;
tr[u].add+=add;
}
else
{
coverdown(u);
adddown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify2(u<<1,l,r,add);
if(r>mid) modify2(u<<1|1,l,r,add);
pushup(u);
}
}
ll query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
coverdown(u);
adddown(u);
int mid=tr[u].l+tr[u].r>>1;
ll ans=-inf;
if(l<=mid) ans=query(u<<1,l,r);
if(r>mid) ans=max(ans,query(u<<1|1,l,r));
return ans;
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&w[i]);
build(1,1,n);
while(m--)
{
ll op,l,r,d;
scanf("%lld%lld%lld",&op,&l,&r);
if(op==1)
{
scanf("%lld",&d);
modify1(1,l,r,d);
}
else if(op==2)
{
scanf("%lld",&d);
modify2(1,l,r,d);
}
else printf("%lld\n",query(1,l,r));
}
}
4.二分
将查询线段树属性改成了查询线段树某个满足条件的点的位置,只需要对查询函数稍做修改即可。
二分查找剩余容量大于等于当前鸡蛋数的区间,由于要找更靠左的篮子,所以优先向左递归寻找线段树的单点。同时用数组辅助记录已经放的堆数,如果堆数满了,就将当前剩余容量置零。
#include<bits/stdc++.h>
using namespace std;
const int N=300010;
int t,n,m,k,a[N];
struct Node{
int l,r,v,sum;
}tr[4*N];
void pushup(int u)
{
tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,m,k};
else
{
tr[u].l=l,tr[u].r=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(tr[u].l==x&&tr[u].r==x)
{
tr[u].v-=v,tr[u].sum--;
return;
}
else
{
int mid=tr[u].l+tr[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 v)
{
if(tr[u].l==tr[u].r) return tr[u].l;
else
{
int res=-1;
if(tr[u<<1].v>=v&&tr[u<<1].sum) res=query(u<<1,v);
else if(tr[u<<1|1].v>=v&&tr[u<<1|1].sum) res=query(u<<1|1,v);
return res;
}
}
void solve()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
for(int i=1;i<=n;i++)
{
int res=query(1,a[i]);
if(res!=-1) modify(1,res,a[i]);
cout<<res<<'\n';
}
}
int main()
{
cin>>t;
while(t--) solve();
}
5.离线处理+扫描线
对于众多区间查询,主席树可以解决很多问题,但是基本上写不完而且可能会爆内存)
因此,就有了离线排序区间后用扫描线处理的方法。
little w and Discretization (nowcoder.com)
给你一个区间,问经过离散化处理后,有多少个数与原来不同?(数组最小值为1)
可以发现,如果元素离散化的值与原来不同,那么它在值域上一定存在一个“空档”。比如[4,1,2,6,5],离散化处理后的序列为[3,1,2,5,4],因为缺少了3,导致大于3的数离散化之后会于原来的值产生差异。而这个“空档”的最小值就是区间mex,现在问题转化为查询区间的两个属性:
(1)区间mex值
(2)区间大于mex的元素的个数
区间mex:
将区间按照右端点排序后在原数组上依次扫描,构建权值线段树,用lastpos[i]表示i最后一次出现的位置,在扫描时更新lastpos数组。同时对于查询的区间[l,r],锁定右端点,对左端点在线段树上进行二分,找到lastpos小于l的最小的数(因为不在[l,r]内的数一定是在区间内没出现过的数,在没出现过的数里面找到最小的那个就是mex)。因为要找最小,所以线段树的属性为最小值。
区间大于某个数的个数:
常用的是找小于等于某个数的个数,不过基本差不多,现在对小于等于的方法进行说明:将区间按照查询的值进行排序,并将原数组排序后进行扫描,每次扫描都在其原位置上更新出现次数,因为查询具有单调性,后面扫描到的数不影响前面已经查询的结果。由于只涉及单点修改,只需要树状数组即可。用区间长度减去其个数就是答案。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=300010;
int n,m,a[N],mex[N],t[N],num[N],ans[N];
struct tree{
int l,r;
int mex;
}tr[N*4];
struct node{
int x,idx;
}b[N];
struct Q{
int l,r,id;
}q[N];
struct Query{
int l,r,x,id;
}query[N];
bool cmp(Q A,Q B)
{
return A.r<B.r;
}
bool cmp2(node A,node B)
{
return A.x<B.x;
}
bool cmp3(Query A,Query B)
{
return A.x<B.x;
}
void build(int p,int l,int r)
{
tr[p].l=l,tr[p].r=r;
if(l==r) return;
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
tr[p].mex=min(tr[p*2].mex,tr[p*2+1].mex);
}
void change(int p,int x,int v)
{
if(tr[p].l==tr[p].r){tr[p].mex=v;return;}
int mid=(tr[p].l+tr[p].r)/2;
if(x<=mid) change(p*2,x,v);
else change(p*2+1,x,v);
tr[p].mex=min(tr[p*2].mex,tr[p*2+1].mex);
}
int ask(int p,int l)
{
if(tr[p].l==tr[p].r) return tr[p].l;
if(tr[p*2].mex<l) return ask(p*2,l);
else return ask(p*2+1,l);
}
int lowbit(int x)
{
return x&-x;
}
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)) t[i]+=c;
}
int sum(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=t[i];
return res;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
b[i].x=a[i],b[i].idx=i;
}
build(1,1,n);
cin>>m;
for(int i=1;i<=m;i++)
{
int l,r;
cin>>l>>r;
q[i]={l,r,i};
}
sort(q+1,q+m+1,cmp);
int pos=0;
for(int i=1;i<=m;i++)
{
int l=q[i].l,r=q[i].r,id=q[i].id;
while(pos<r)
{
pos++;
change(1,a[pos],pos);
}
mex[id]=ask(1,l);
query[id]={l,r,mex[id],id};
}
sort(b+1,b+n+1,cmp2);
sort(query+1,query+m+1,cmp3);
pos=1;
for(int i=1;i<=m;i++)
{
int l=query[i].l,r=query[i].r,x=query[i].x,id=query[i].id;
while(x>=b[pos].x&&pos<=n)
{
add(b[pos].idx,1);
pos++;
}
ans[id]=(r-l+1)-(sum(r)-sum(l-1));
}
for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
}
[SDOI2009]HH的项链 (nowcoder.com)
要求求区间有多少种不同的数,和上题类似,扫描动态更新某个数最后出现的位置。假设数列为[1,2,5,4,1],第一次遇见1时,在下标为1处+1;下一次遇见1时是在下标为5的位置,这时更新lastpos[1],并且将下标1处的值-1以还原,并在下标5处+1来更新。用树状数组即可实现。
#include<bits/stdc++.h>
using namespace std;
int n,m,tr[1000010],w[1000010],pre[1000010],res[1000010];
struct node{
int id,l,r,ans;
}a[1000010];
bool cmp(node x,node y)
{
return x.r<y.r;
}
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 res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
cin>>m;
for(int i=1;i<=m;i++)
{
a[i].id=i;
cin>>a[i].l>>a[i].r;
}
sort(a+1,a+m+1,cmp);
int pos=0;
for(int i=1;i<=m;i++)
{
int l=a[i].l,r=a[i].r,id=a[i].id;
while(pos<r)
{
pos++;
add(pos,1);
if(pre[w[pos]]) add(pre[w[pos]],-1);
pre[w[pos]]=pos;
}
res[id]=sum(r)-sum(l-1);
}
for(int i=1;i<=m;i++) cout<<res[i]<<'\n';
}
P4113 [HEOI2012] 采花 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
上一题的加强版,用pre[i][0]表示i上一次出现的位置,pre[i][1]表示i上两次出现的位置。
#include<bits/stdc++.h>
using namespace std;
int n,m,c,tr[2000010],w[2000010],pre[2000010][2],res[2000010];
struct node{
int id,l,r,ans;
}a[2000010];
bool cmp(node x,node y)
{
return x.r<y.r;
}
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 res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>c>>m;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<=m;i++)
{
a[i].id=i;
cin>>a[i].l>>a[i].r;
}
sort(a+1,a+m+1,cmp);
int pos=0;
for(int i=1;i<=m;i++)
{
int l=a[i].l,r=a[i].r,id=a[i].id;
while(pos<r)
{
pos++;
if(pre[w[pos]][1]) add(pre[w[pos]][0],1),add(pre[w[pos]][1],-1);
else if(pre[w[pos]][0]) add(pre[w[pos]][0],1);
pre[w[pos]][1]=pre[w[pos]][0];
pre[w[pos]][0]=pos;
}
res[id]=sum(r)-sum(l-1);
}
for(int i=1;i<=m;i++) cout<<res[i]<<'\n';
}
6.离散化