简要题意:
给定一个长度为 (n) 的数组 (a_i),(T) 组询问求 ([l,r]) 区间 随机抽到两个相等的数的概率。
(a_i,n,T leq 5 imes 10^4),时间限制 (100 ext{ms}).
首先,这个题当然可以用分块、线段树维护平方和,用奇怪的数据结构解决。
但是我们智商不够,数据结构也凑不上,所以考虑暴力。
暴力?
基于暴力
大力暴力的话,可以达到 (mathcal{O}(nT)) 的时间复杂度。其具体实现是,把 ([l,r]) 区间的每个数拿出来做成一个桶(哈希),然后对桶内的数进行统计答案。
但是出题人要卡你实在简单。它只需要一直询问 ([1 , 5 imes 10^4]) 这个区间,你的复杂度会卡到满。
那你说了,切,我用记忆化记录 ([l,r]) 的答案好了。
出题人于是把数据改了改:
第 (i) 组询问为 ([i,n]).
你会发现记忆化不行了。暴力也不行了。数据结构也不行了。似乎凉了?
大力转移
但是你会发现,通过 ([l,r] ightarrow [l,r+1]) 只需要 (mathcal{O}(1)) 的统计即可。这样的话,你可以把 对 (l) 点的同一个询问往两侧各做一遍哈希,一个个扩展,可以在 (mathcal{O}(n)) 的时间解决对 (l) 的询问。
但是很显然,出题人给你个随机数据你就撑不住。因为,你把 (mathcal{O}(n^2)) 个状态都枚举一遍是不现实的,而对不同的 (l) 你又无从下手。
这时,莫队就应运而生了。
莫队仅仅是考虑以下四个 (mathcal{O}(1)) 的转移:
莫队历史
我们来聊一聊关于莫队的发明历史吧。
众所周知 ( ext{CodeForces}) 是个非常好的网站,里面的题目质量高,网站也以独特的赛制而闻名。大名鼎鼎的 珂朵莉树(老司机树,( ext{ODT})) 就是从 ( ext{CF}) 的一道赛题中引申的。
在这个高级的圈子里,( ext{CF}) 的高级人士已经发现了类似的算法,并小范围的流传开去。但是很可惜,没有人对它进行系统的总结,因此始终没有大范围传播。
后来 莫涛是第一个对莫队算法进行详细归纳总结的人,当时 “莫涛队长” 简称 “莫队”,他只分析了 普通莫队算法(不带修改的) 的实现,复杂度等。
再后来,( ext{Oiers}) 和 ( ext{Acmers}) 对莫队进行了修改操作上的优化,把莫队的应用推向了顶峰。因此有了树上莫队,回滚莫队等等。
如何实现
诚然莫队的基础是一种暴力。我们把询问的区间按照 ([l,r]) 的 ( ext{pair}) 双关键字排序,然后第一个区间用 (mathcal{O}(n)) 的时间暴力出来,后面的区间则考虑上述的四个转移。
可以证明,该算法的最劣时间复杂度为 (mathcal{O}(n sqrt{n})),具体证明可以去看 ( ext{OI - wiki}) 普通莫队算法 中的证明。
实际上我们只需要维护一个桶,支持插入和删除,这非常简单。
时间复杂度:(mathcal{O}(n sqrt{n})).
实际得分:(100pts).
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e4+1;
inline int read(){char ch=getchar(); int f=1; while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0; while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*f;}
inline void write(int x) {
if(x<0) {putchar('-');write(-x);return;}
if(x<10) {putchar(char(x%10+'0'));return;}
write(x/10);putchar(char(x%10+'0'));
}
int n,m,c[N],cnt[N],b;
ll sum,ans1[N],ans2[N]; //sum 记录临时答案,ans1 和 ans2 记录答案的排序
struct node {
int l,r,id; //id 是询问编号
inline bool operator < (const node &x) const {
if(l/b!=x.l/b) return l<x.l;
return (l/b)&1?r<x.r:r>x.r;
} //排序
} a[N];
inline void add(int x) {sum+=(cnt[x]++);} //插入
inline void del(int x) {sum-=(--cnt[x]);} //删除
inline ll gcd(ll n,ll m) {return m?gcd(m,n%m):n;}
int main() {
n=read(),m=read(); b=sqrt(n);
for(int i=1;i<=n;i++) c[i]=read();
for(int i=1;i<=m;i++) a[i].l=read(),a[i].r=read(),a[i].id=i;
sort(a+1,a+m+1);
for(int i=1,l=1,r=0;i<=m;i++) {
if(a[i].l==a[i].r){
ans1[a[i].id]=0; ans2[a[i].id]=1;
continue;
} //特殊区间按照题目要求更新
while(l<a[i].l) del(c[l++]);
while(l>a[i].l) add(c[--l]);
while(r<a[i].r) add(c[++r]);
while(r>a[i].r) del(c[r--]); //暴力移动
ans1[a[i].id]=sum;
ans2[a[i].id]=ll(r-l+1)*(r-l)/2; //暴力更新
} for(int i=1;i<=m;i++) {
if(ans1[i]) {
ll x=gcd(ans1[i],ans2[i]);
ans1[i]/=x; ans2[i]/=x;
} else ans2[i]=1; //约分
printf("%lld/%lld
",ans1[i],ans2[i]);
}
return 0;
}