题目传送门:http://acm.hdu.edu.cn/showproblem.php?pid=6070
题意:给你长度为n的序列,求某个区间[l,r]使得区间内的数字种类/区间长度最小输出这个最小值
官方题解:
思路详解:
首先从答案入手,二分答案
每次二分的答案我们要找是否存在区间满足size(l,r)+mid*l<=mid*(r+1),如果存在的话继续往小的方向二分,如果不存在的话继续往大的方向二分
然后我们从左往右枚举r,每次就相当于插入一个新数,然后我们需要更新被这个数影响的那些区间的size值,然后线段树区间维护
size(l,r)+mid*l的最小值就可以了
tag数组:存的是每个区间的size值
v数组:存的是该区间中size(l,r)+mid*l的值
标程代码:
#include<cstdio>
const int N=60010,M=131100;
int Case,n,i,a[N],ap[N],tag[M];
double v[M],t,L,R,MID;
inline double min(double a,double b)
{
return a<b?a:b;
}
inline void tag1(int x,int p)
{
tag[x]+=p;
v[x]+=p;
}
inline void pb(int x)
{
if(tag[x])
{
tag1(x<<1,tag[x]);
tag1(x<<1|1,tag[x]);
tag[x]=0;
}
}
void build(int x,int a,int b)
{
v[x]=MID*a,tag[x]=0;//初始时size()为0,v[x]初始化为mid*l
if(a==b)return;
int mid=(a+b)>>1;
build(x*2,a,mid),build(2*x+1,mid+1,b);
}
void change(int x,int a,int b,int c,int d)
{
if(c<=a&&b<=d)
{
tag[x]+=1;
v[x]+=1;
return;
}
pb(x);//以[c,d]间的数为左端点,以d为右端点区间全是需要更新的区间
int mid=(a+b)>>1;
if(c<=mid)change(x<<1,a,mid,c,d);
if(d>mid)change(x<<1|1,mid+1,b,c,d);
v[x]=min(v[x<<1],v[x<<1|1]);
}
void ask(int x,int a,int b,int d)
{
if(b<=d)//只要这个查询区间的右端点小于目前枚举的右端点即可记录答案
//这样可以不必一一枚举区间的左端点
{
if(t>v[x])t=v[x];
return;
}
pb(x);
int mid=(a+b)>>1;
ask(x<<1,a,mid,d);
if(d>mid)ask(x<<1|1,mid+1,b,d);
}
int main()
{
scanf("%d",&Case);
while(Case--)
{
scanf("%d",&n);
for(i=1; i<=n; i++)scanf("%d",&a[i]);
L=0,R=1;
for(int _=20; _; _--)//二分的次数,保证精度
{
MID=(L+R)/2;
build(1,1,n);
//ap[i]为i上一次出现的位置坐标
for(i=1; i<=n; i++)ap[i]=0;
for(i=1; i<=n; i++)
{
change(1,1,n,ap[a[i]]+1,i);//更新那些被插入的a[i]影响的区间
t=1e9;
ask(1,1,n,i);//查找所有以i为右端店的区间的size(l,r)+mid*l的最小值
if(t-MID*(i+1)<=0)break;
ap[a[i]]=i;
}
if(i<=n)R=MID;
else L=MID;
}
printf("%.10f\n",(L+R)/2);
}
return 0;
}