Overview
求区间不同数的个数。
N≤50000
,
M≤200000
Analysis
1. 莫队算法
多个区间询问,有在线和离线的方法。先考虑离线吧。
按照块排序,使用莫队算法可以轻松解决。
时间复杂度: O(nn−√)
代码:
#include <cstdio>
#include <cmath>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=65536;
const int M=200010;
const int D=1000001;
int n,a[N];
int m,unit;
struct Q
{
int l,r,id;
friend inline int operator < (Q qa,Q qb)
{
return qa.l/unit!=qb.l/unit?qa.l/unit<qb.l/unit:qa.r<qb.r;
}
}q[M];
inline int Read(void)
{
int x=0; char c=getchar();
for (;!isdigit(c);c=getchar());
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x;
}
int vis[D],cnt;
int ans[M];
int main(void)
{
n=Read();
for (int i=1;i<=n;i++) a[i]=Read();
m=Read();
for (int i=1;i<=m;i++) q[i].l=Read(),q[i].r=Read(),q[i].id=i;
unit=(int)sqrt(n);
sort(q+1,q+m+1);
int l=1,r=0;
for (int i=1;i<=m;i++)
{
for (;l>q[i].l;l--)
{
vis[a[l-1]]++;
if (vis[a[l-1]]==1) cnt++;
}
for (;l<q[i].l;l++)
{
vis[a[l]]--;
if (!vis[a[l]]) cnt--;
}
for (;r<q[i].r;r++)
{
vis[a[r+1]]++;
if (vis[a[r+1]]==1) cnt++;
}
for (;r>q[i].r;r--)
{
vis[a[r]]--;
if (!vis[a[r]]) cnt--;
}
ans[q[i].id]=cnt;
}
for (int i=1;i<=m;i++)
printf("%d\n",ans[i]);
return 0;
}
2. 另一个离线算法
其实还可以想一想其他的离线算法。
按照左端点排序行不行?尝试一下吧。
排完序后,固定左端点 l=1 ,可以在 O(n) 求出此时 [1,r] 的不同数的个数 f[i] 。
接下来,我们考虑
l
变成
①若
[l+1,r]
中不存在
al
,那么
f[r]−−
;
②若
[l+1,r]
中存在
al
,那么
f[r]
不变。
我们只要找到下一个与
al
相同的坐标
nextl
,那么将区间
[l+1,nextl−1]
中的所有
f[i]
减
1
即可。
区间更改,单点求值,考虑使用区间数据结构。
树状数组就可以了,应该会跑得很快的。
这个算法不知道网上有没有,自己想出来的233333。
时间复杂度:
代码:实测 916ms
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=65536;
const int M=200010;
const int D=1000001;
int n;
int a[N];
int tag[D];
int nxt[N];
int m;
struct Ques
{
int l,r,id;
friend inline int operator < (Ques qa,Ques qb)
{
return qa.l<qb.l;
}
}q[M];
int ans[M];
inline int read(void)
{
int x=0; char c=getchar();
for (;!isdigit(c);c=getchar());
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x;
}
int cnt[N];
struct TreeArray
{
int t[N];
inline int lowbit(int i)
{
return i&-i;
}
inline void ins(int i,int add)
{
for (;i<=n;i+=lowbit(i)) t[i]+=add;
}
inline int query(int i)
{
int cnt=0;
for (;i;i-=lowbit(i)) cnt+=t[i];
return cnt;
}
}tr;
int main(void)
{
n=read();
for (int i=1;i<=n;i++) a[i]=read();
for (int i=1;i<=n;i++) tag[a[i]]=n+1;
for (int i=n;i>=1;i--) nxt[i]=tag[a[i]],tag[a[i]]=i;
m=read();
for (int i=1;i<=m;i++) q[i].id=i,q[i].l=read(),q[i].r=read();
sort(q+1,q+m+1);
for (int i=1;i<=n;i++) tag[a[i]]=0;
for (int i=1;i<=n;i++)
{
cnt[i]=cnt[i-1];
if (!tag[a[i]]) cnt[i]++; tag[a[i]]=1;
}
int now=1;
for (int i=1;i<=m;i++)
{
for (;now<q[i].l;now++) tr.ins(now+1,-1),tr.ins(nxt[now],1);
ans[q[i].id]=tr.query(q[i].r)+cnt[q[i].r];
}
for (int i=1;i<=m;i++)
printf("%d\n",ans[i]);
return 0;
}
3. 可持久化数据结构的在线解法
不要从离线算法的排序入手,考虑从问题的解决入手。
区间 [l,r] 的不同数个数,就是每个在区间中最后一次出现的数的个数,也就是在 [l,r] 中满足 next[i]>r 的个数,即 [1,r] 中满足 next[i]>r 的个数减去 [1,l−1] 中满足 next[i]>r 的个数。
以权值为点,坐标为链建立可持久化线段树在线解决问题!!!
代码:实测 1956ms
#include <cstdio>
#include <cctype>
const int N=65536;
const int S=2097152;
const int D=1048576;
int n;
int a[N];
inline int Read(void)
{
int x=0; char c=getchar();
for (;!isdigit(c);c=getchar());
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x;
}
int pre[N],tmp[D];
struct Sgt
{
struct Node
{
int lc,rc;
int l,r;
int cnt;
}tr[S];
int tot;
int Build(int l,int r)
{
int now=++tot;
tr[now].l=l;
tr[now].r=r;
if (l!=r)
{
int mid=l+r>>1;
tr[now].lc=Build(l,mid);
tr[now].rc=Build(mid+1,r);
}
return now;
}
int Ins(int vtx,int w)
{
int now=++tot;
tr[now].l=tr[vtx].l;
tr[now].r=tr[vtx].r;
tr[now].cnt=tr[vtx].cnt+1;
if (tr[now].l!=tr[now].r)
{
int mid=tr[now].l+tr[now].r>>1;
if (w<=mid)
{
tr[now].lc=Ins(tr[vtx].lc,w);
tr[now].rc=tr[vtx].rc;
}
else
{
tr[now].lc=tr[vtx].lc;
tr[now].rc=Ins(tr[vtx].rc,w);
}
}
return now;
}
int Query(int vtx,int w)
{
if (tr[vtx].l==tr[vtx].r) return tr[vtx].cnt;
int mid=tr[vtx].l+tr[vtx].r>>1;
return w<=mid?Query(tr[vtx].lc,w):tr[tr[vtx].lc].cnt+Query(tr[vtx].rc,w);
}
}sgt;
int root[N];
void Init(void)
{
n=Read();
for (int i=1;i<=n;i++) a[i]=Read();
for (int i=1;i<=n;i++)
{
pre[i]=tmp[a[i]];
tmp[a[i]]=i;
}
root[0]=sgt.Build(0,n);
for (int i=1;i<=n;i++)
root[i]=sgt.Ins(root[i-1],pre[i]);
}
int m;
int x,y;
void Work(void)
{
m=Read();
for (int i=1;i<=m;i++)
{
x=Read(),y=Read();
printf("%d\n",sgt.Query(root[y],x-1)-(x-1));
}
}
int main(void)
{
Init();
Work();
return 0;
}
跑得更慢了啊……
继续继续,一定还可以优化!
4. 改进算法三——又一个离线算法
求在 [l,r] 中满足 next[i]>r 的个数,区间减法不仅可以用 [1,r]−[1,l−1] ,还可以用 [l,n]−[r+1,n] ,又注意到 [r+1,n] 的个数就是 (n−r) 个,那么只用求出 [l,n] 中 next[i]>r 的个数即可。
普遍的想法是把询问排序,然后通过 l 的移动和权值线段树或者树状数组的更新完成询问。
先贴两个代码:
代码1:线段树(
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=65536;
const int S=2097152;
const int D=1048576;
const int M=S;
int n,m;
int a[N];
struct Q
{
int l,r;
int id;
friend inline int operator < (Q qa,Q qb)
{
return qa.r<qb.r;
}
}q[M];
int pre[N],tmp[D];
inline int Read(void)
{
int x=0; char c=getchar();
for (;!isdigit(c);c=getchar());
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x;
}
void Init(void)
{
n=Read();
for (int i=1;i<=n;i++) a[i]=Read();
for (int i=1;i<=n;i++)
{
pre[i]=tmp[a[i]];
tmp[a[i]]=i;
}
m=Read();
for (int i=1;i<=m;i++)
{
q[i].l=Read();
q[i].r=Read();
q[i].id=i;
}
sort(q+1,q+m+1);
}
struct Sgt
{
struct Node
{
int l,r;
int cnt;
}tr[S];
void Build(int now,int l,int r)
{
tr[now].l=l;
tr[now].r=r;
if (l!=r)
{
int mid=l+r>>1;
Build(now<<1,l,mid);
Build(now<<1|1,mid+1,r);
}
}
void Ins(int now,int w)
{
tr[now].cnt++;
if (tr[now].l==tr[now].r) return;
int mid=tr[now].l+tr[now].r>>1;
if (w<=mid) Ins(now<<1,w); else Ins(now<<1|1,w);
}
int Query(int now,int w)
{
if (tr[now].l==tr[now].r) return tr[now].cnt;
int mid=tr[now].l+tr[now].r>>1;
return w<=mid?Query(now<<1,w):tr[now<<1].cnt+Query(now<<1|1,w);
}
}sgt;
int now;
int ans[M];
void Work(void)
{
sgt.Build(1,0,n-1);
for (int i=1;i<=n;i++)
{
sgt.Ins(1,pre[i]);
for (;now<m&&q[now+1].r==i;now++)
ans[q[now+1].id]=sgt.Query(1,q[now+1].l-1)-(q[now+1].l-1);
}
for (int i=1;i<=m;i++) printf("%d\n",ans[i]);
}
int main(void)
{
Init();
Work();
return 0;
}
代码2:树状数组(实测 876ms )
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=65536;
const int S=2097152;
const int D=1048576;
const int M=S;
int n,m;
int a[N];
struct Q
{
int l,r;
int id;
friend inline int operator < (Q qa,Q qb)
{
return qa.r<qb.r;
}
}q[M];
int pre[N],tmp[D];
inline int Read(void)
{
int x=0; char c=getchar();
for (;!isdigit(c);c=getchar());
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x;
}
void Init(void)
{
n=Read();
for (int i=1;i<=n;i++) a[i]=Read();
for (int i=1;i<=n;i++)
{
pre[i]=tmp[a[i]];
tmp[a[i]]=i;
}
m=Read();
for (int i=1;i<=m;i++)
{
q[i].l=Read();
q[i].r=Read();
q[i].id=i;
}
sort(q+1,q+m+1);
}
struct TreeArray
{
int tr[N];
inline int lowbit(int i)
{
return i&-i;
}
void Ins(int i,int add)
{
for (i++;i<=n;i+=lowbit(i)) tr[i]+=add;
}
int Query(int i)
{
int sum=0;
for (i++;i;i-=lowbit(i)) sum+=tr[i];
return sum;
}
}tr;
int nowq;
int ans[M];
void Work(void)
{
for (int i=1;i<=n;i++)
{
tr.Ins(pre[i],1);
for (;nowq<m&&q[nowq+1].r==i;nowq++)
ans[q[nowq+1].id]=tr.Query(q[nowq+1].l-1)-(q[nowq+1].l-1);
}
for (int i=1;i<=m;i++) printf("%d\n",ans[i]);
}
int main(void)
{
Init();
Work();
return 0;
}
然而,我们可以继续优化!
优化的方法就是想办法把
O(mlogm)
的排序给弄快一些。
排序已经做到最快了,我们能不能不排序?
当然可以,像保存图一样开个链表就行了……
最后一个代码:实测 832ms
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
const int N=65536;
const int M=200010;
const int D=1000001;
int n;
int a[N];
int tag[D];
int nxt[N];
int m;
struct G
{
int r,nxt;
}mp[M];
int tt,hd[N];
int ans[M];
inline int read(void)
{
int x=0; char c=getchar();
for (;!isdigit(c);c=getchar());
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x;
}
inline int ins(int l,int r)
{
mp[++tt].r=r;
mp[tt].nxt=hd[l];
hd[l]=tt;
}
int cnt[N];
struct TreeArray
{
int t[N];
inline int lowbit(int i)
{
return i&-i;
}
inline void ins(int i,int add)
{
for (;i<=n;i+=lowbit(i)) t[i]+=add;
}
inline int query(int i)
{
int cnt=0;
for (;i;i-=lowbit(i)) cnt+=t[i];
return cnt;
}
}tr;
int main(void)
{
n=read();
for (int i=1;i<=n;i++) a[i]=read();
for (int i=1;i<=n;i++) tag[a[i]]=n+1;
for (int i=n;i>=1;i--) nxt[i]=tag[a[i]],tag[a[i]]=i;
int l,r; m=read();
for (int i=1;i<=m;i++) l=read(),r=read(),ins(l,r);
for (int i=n;i>=1;i--)
{
tr.ins(nxt[i]-1,1);
for (int k=hd[i];k;k=mp[k].nxt)
ans[k]=((n-i+1)-tr.query(mp[k].r-1))-(n-mp[k].r);
}
for (int i=1;i<=m;i++)
printf("%d\n",ans[i]);
return 0;
}
Sumarize
①区间问题可以从离线、在线的角度入手,也可以从问题的求解入手。
②区间问题离线的三种处理方法,不一定要排序,可以使用图来解决。