P1494 [国家集训队]小Z的袜子 题解
简要题意:
给定一个长度为 \(n\) 的数组 \(a_i\),\(T\) 组询问求 \([l,r]\) 区间 随机抽到两个相等的数的概率。
\(a_i,n,T \leq 5 \times 10^4\),时间限制 \(100 \text{ms}\).
首先,这个题当然可以用分块、线段树维护平方和,用奇怪的数据结构解决。
但是我们智商不够,数据结构也凑不上,所以考虑暴力。
暴力?
基于暴力
大力暴力的话,可以达到 \(\mathcal{O}(nT)\) 的时间复杂度。其具体实现是,把 \([l,r]\) 区间的每个数拿出来做成一个桶(哈希),然后对桶内的数进行统计答案。
但是出题人要卡你实在简单。它只需要一直询问 \([1 , 5 \times 10^4]\) 这个区间,你的复杂度会卡到满。
那你说了,切,我用记忆化记录 \([l,r]\) 的答案好了。
出题人于是把数据改了改:
第 \(i\) 组询问为 \([i,n]\).
你会发现记忆化不行了。暴力也不行了。数据结构也不行了。似乎凉了?
大力转移
但是你会发现,通过 \([l,r] \rightarrow [l,r+1]\) 只需要 \(\mathcal{O}(1)\) 的统计即可。这样的话,你可以把 对 \(l\) 点的同一个询问往两侧各做一遍哈希,一个个扩展,可以在 \(\mathcal{O}(n)\) 的时间解决对 \(l\) 的询问。
但是很显然,出题人给你个随机数据你就撑不住。因为,你把 \(\mathcal{O}(n^2)\) 个状态都枚举一遍是不现实的,而对不同的 \(l\) 你又无从下手。
这时,莫队就应运而生了。
莫队仅仅是考虑以下四个 \(\mathcal{O}(1)\) 的转移:
莫队历史
我们来聊一聊关于莫队的发明历史吧。
众所周知 \(\text{CodeForces}\) 是个非常好的网站,里面的题目质量高,网站也以独特的赛制而闻名。大名鼎鼎的 珂朵莉树(老司机树,\(\text{ODT}\)) 就是从 \(\text{CF}\) 的一道赛题中引申的。
在这个高级的圈子里,\(\text{CF}\) 的高级人士已经发现了类似的算法,并小范围的流传开去。但是很可惜,没有人对它进行系统的总结,因此始终没有大范围传播。
后来 莫涛是第一个对莫队算法进行详细归纳总结的人,当时 “莫涛队长” 简称 “莫队”,他只分析了 普通莫队算法(不带修改的) 的实现,复杂度等。
再后来,\(\text{Oiers}\) 和 \(\text{Acmers}\) 对莫队进行了修改操作上的优化,把莫队的应用推向了顶峰。因此有了树上莫队,回滚莫队等等。
如何实现
诚然莫队的基础是一种暴力。我们把询问的区间按照 \([l,r]\) 的 \(\text{pair}\) 双关键字排序,然后第一个区间用 \(\mathcal{O}(n)\) 的时间暴力出来,后面的区间则考虑上述的四个转移。
可以证明,该算法的最劣时间复杂度为 \(\mathcal{O}(n \sqrt{n})\),具体证明可以去看 \(\text{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\n",ans1[i],ans2[i]);
}
return 0;
}