前言
这一题乍一看——居然比前面的题要简单一些的样子。
之后,我兴致勃勃地打了个代码——耶,只有30分。
时间复杂度:
n=50000时,复杂度等于44721359
再加上STL常数太大,1s内根本卡不进去.
思路
首先,我们必须知道那个概率怎么算.
设区间总共有种颜色,分别为
则有:(这不是废话吗)
然后,我们将介绍一个分块算法的重要形式——对“讯问”进行分块。
我们先按左端点进行排序,然后分成块,再对右端点进行排序.
(默认m=n)对于每一块左端点调整的复杂度为,右端点的调整复杂度为,所以总复杂度约为(忽略左端点的复杂度)
现在有一个问题:为什么要排两次序呢?
原因很简单.
我们第一次排序完后,同一个块内的左端点的差不超过,而右端点的差可能接近n.
所以我们再按右端点排序,使得右界直接往右拓展,可以很好地利用之前做的操作.
如果不二次排序复杂度将高达.
暴 力 代 码 :
#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;
}
总结:
当对讯问的操作具有可转移性(比如统计次数,统计和等问题)时,我们可以对左端点排序,分块,再按右端点排序.
这种离线算法叫做莫队.(好像入门题就是黑题,大家学会了就可以收剐黑题了)
其实本算法的精髓在于使尽可能小,高度继承前面的操作.