caioj 2063& CH 0x40数据结构进阶(0x44 分块)例题4:小Z的袜子

传送门

前言

这一题乍一看——居然比前面的题要简单一些的样子。
之后,我兴致勃勃地打了个代码——耶,只有30分。
时间复杂度:O(nnlog(n))O(n*sqrt{n*log(n)})
n=50000时,复杂度等于44721359
再加上STL常数太大,1s内根本卡不进去.

思路

首先,我们必须知道那个概率怎么算.
设区间[l,r][l,r]总共有kk种颜色,分别为a1,a2ak,num[x]xa_1,a_2……a_k,num[x]表示x颜色的袜子总数
则有:i=1knum[i]=rl+1sum_{i=1}^k num[i]=r-l+1(这不是废话吗)
P=i=1knum[i](num[i]1)(rl+1)(rl)P=dfrac{sum_{i=1}^k num[i]*(num[i]-1)}{(r-l+1)*(r-l)}

然后,我们将介绍一个分块算法的重要形式——对“讯问”进行分块
我们先按左端点进行排序,然后分成nsqrt{n}块,再对右端点进行排序.
(默认m=n)对于每一块左端点调整的复杂度为O(n)O(sqrt{n}),右端点的调整复杂度为O(n)O(n),所以总复杂度约为O(nn)O(n*sqrt{n})(忽略左端点的复杂度)

现在有一个问题:为什么要排两次序呢?
原因很简单.
我们第一次排序完后,同一个块内的左端点的差不超过nsqrt{n},而右端点的差可能接近n.
所以我们再按右端点排序,使得右界直接往右拓展,可以很好地利用之前做的操作.
如果不二次排序复杂度将高达O(n2)O(n^2).

暴 力 代 码 :

#include<cmath>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#define g g()
using namespace std;
const int size=1<<25;
char buf[size],*p1=buf,*p2=buf;
inline char g{return p1==p2&&(p2=(p1=buf)+fread(buf,1,size,stdin),p1==p2)?EOF:*p1++;}
void qr(int &x)
{
	char c=g;x=0;
	while(!('0'<=c&&c<='9'))c=g;
	while('0'<=c&&c<='9')x=x*10+c-'0',c=g;
}
void write(int x)
{
	if(x/10)write(x/10);
	putchar(x%10+'0');
}

const int N=50010,T=910;
int a[N],b[N],C[N],f[T][T],u,v,L[T],R[T],pos[N],t,len,ans,cnt;
//颜色,颜色出现次数,组合数,段间合法情况计数,分子,分母,段的左右界,从属块,块数,块长
vector<int>s[N];//s[x]为颜色为x的位置集合.
bool vis[N];//颜色是否访问过. 
inline int find(int x,int l,int r)
{return upper_bound(s[x].begin(),s[x].end(),r)-lower_bound(s[x].begin(),s[x].end(),l);}
int gcd(int a,int b){return !a?b:gcd(b%a,a);}
inline void ask(int l,int r)//O(n/t*log(n))
{
	u=0;v=C[r-l+1];
	int p=pos[l],q=pos[r];
	int x=0,y=0;memset(vis,0,sizeof(vis));
	if(p+1<=q-1)
		x=p+1,y=q-1;
	if(!x)
	{
		for(int i=l;i<=r;i++)if(!vis[a[i]])
			vis[a[i]]=1,u+=C[find(a[i],l,r)];
	}
	else
	{
		int m,n;
		for(int i=l;i<=R[p];i++)if(!vis[a[i]])
		{
			vis[a[i]]=1;
			m=find(a[i],l,r);
			n=find(a[i],L[x],R[y]);
			u+=C[m-n]+(m-n)*n;
		}
		u+=f[x][y];
		for(int i=L[q];i<=r;i++)if(!vis[a[i]])
		{
			vis[a[i]]=1;
			m=find(a[i],l,r);
			n=find(a[i],L[x],R[y]);
			u+=C[m-n]+(m-n)*n;
		}
	}
	x=gcd(u,v);
	write(u/x);putchar('/');write(v/x);puts("");
}
int main()
{
	int n,m;qr(n);qr(m);
	for(int i=1;i<=n;i++)qr(a[i]),s[a[i]].push_back(i);
	for(int i=2;i<=n;i++)C[i]=C[i-1]+i-1;
	//分块 
	ans=1;cnt=0;
	while(ans<<1 <=n)cnt++,ans=ans<<1;
	int t=sqrt(n*cnt);
	if(t)len=n/t;
	for(int i=1;i<=t;i++)
		L[i]=R[i-1]+1,R[i]=i*len;
	if(R[t]<n)
		t++,L[t]=R[t-1]+1,R[t]=n;
	for(int i=1;i<=t;i++)
		for(int j=L[i];j<=R[i];j++)
			pos[j]=i;
	//预处理
	memset(f,0,sizeof(f));
	for(int i=1;i<=t;i++)//O(t*n)
	{
		 memset(b,0,sizeof(b));
		 for(int j=i;j<=t;j++)
		 {
		 	for(int k=L[j];k<=R[j];k++)
		 		b[a[k]]++;
		 	for(int k=1;k<=n;k++)
		 		f[i][j]+=C[b[k]];
		 }
	}
	int l,r;
	while(m--)//O(m*n/t*log(n))
	{
		qr(l);qr(r);
		ask(l,r);
	}
	return 0;
}
/* t*n=m*n/t*log(n) (假设m=n)
    t =n/t*log(n)
   t^2=n*log(n)
   t  =sqrt(nlog(n)) 
*/

正 解 :

#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define g g()
using namespace std;
typedef long long ll;
const int N=50010;
struct node{int x,y,z;}a[N],b[N];
bool cmp1(node a,node b){return a.x<b.x;}//按左端点排序 
bool cmp2(node a,node b){return a.y<b.y;}//按右端点排序 
int c[N],v[N],tot,n,m,l,r,t,len;
ll fx[N],fy[N],temp;//分子,分母,临时变量 
void calc(int st,int ed,int d)
{
	for(int i=st;i<ed;i++)
	{
		temp-=(ll)v[c[i]]*(v[c[i]]-1);
		v[c[i]]+=d;
		temp+=(ll)v[c[i]]*(v[c[i]]-1);
	}
}
ll gcd(ll a,ll b){return !a?b:gcd(b%a,a);}

//快读,快写 
const int size=1<<24;
char buf[size],*p1=buf,*p2=buf;
inline char g{return p1==p2&&(p2=(p1=buf)+fread(buf,1,size,stdin),p1==p2)?EOF:*p1++;}
void qr(int &x)
{
	char c=g;x=0;
	while(!('0'<=c&&c<='9'))c=g;
	while('0'<=c&&c<='9')x=x*10+c-'0',c=g;
}
void write(ll x)
{
	if(x/10)write(x/10);
	putchar(x%10+'0');
}

int main()
{
	qr(n);qr(m);
	for(int i=1;i<=n;i++)qr(c[i]);
	for(int i=1;i<=m;i++)qr(a[i].x),qr(a[i].y),a[i].z=i,fy[i]=a[i].y-a[i].x+1;
	sort(a+1,a+m+1,cmp1);
	len=sqrt(n*1.0);t=n/len+(n%len!=0);
	for(int i=1,j=1;i<=t;i++)
	{
		for(tot=0;j<=m&&a[j].x<=i*len;j++)b[++tot]=a[j];
		//这样分块,块中元素的个数可能有很大差距,但是左端点的差却能不超过len 
		if(!tot)continue;
		sort(b+1,b+1+tot,cmp2);
		memset(v,0,sizeof(v));temp=0;
		l=b[1].x;r=b[1].x-1;
		for(int k=1;k<=tot;k++)
		{
			if(l<b[k].x)calc(l,b[k].x,-1);
			else if(b[k].x<l)calc(b[k].x,l,1);
			if(r<b[k].y)calc(r+1,b[k].y+1,1);
			l=b[k].x;r=b[k].y;
			fx[b[k].z]=temp;
		}
	}
	for(int i=1;i<=m;i++)
	{
		fy[i]=fy[i]*(fy[i]-1);
		if(!fy[i]){puts("0/1");continue;}
		temp=gcd(fx[i],fy[i]);
		write(fx[i]/temp);putchar('/');write(fy[i]/temp);puts("");
	}
	return 0;
}
	

总结:

当对讯问的操作具有可转移性(比如统计次数,统计和等问题)时,我们可以对左端点排序,分块,再按右端点排序.
这种离线算法叫做莫队.(好像入门题就是黑题,大家学会了就可以收剐黑题了)
其实本算法的精髓在于使Δ(l)+Δ(r)Delta(l)+Delta(r)尽可能小,高度继承前面的操作.

原文地址:https://www.cnblogs.com/zsyzlzy/p/12373896.html