P1494 [国家集训队]小Z的袜子 题解

CSDN同步

原题链接

简要题意:

给定一个长度为 \(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)\) 的转移:

\[[l,r] \rightarrow [l,r+1] \]

\[[l,r] \rightarrow [l,r-1] \]

\[[l,r] \rightarrow [l-1,r] \]

\[[l,r] \rightarrow [l+1,r] \]

莫队历史

我们来聊一聊关于莫队的发明历史吧。

众所周知 \(\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;
}


posted @ 2020-06-26 21:36  bifanwen  阅读(219)  评论(0编辑  收藏  举报