[CQOI2012]组装(贪心+数学)
题意:有n种零件,m个生产车间,给出每个车间的坐标xi和生产零件的种类pi,求组装车间的坐标,使得dis(1)+dis(2)+...+dis(n)最小,其中dis(x)表示生产第x种零件的生产车间到组装车间距离的平方的最小值.
分析:不难想到,我们最后的最优答案一定是从所有m 个生产车间中选出了n个,使得一个生产车间恰好生产一种零件,这样一定是最优的.(贪心策略)
设组装车间的坐标为x,选出的n个生产车间坐标为a1,a2...an,则有:
\(ans=\sum_{i=1}^n(x-a[i])^2\)
把完全平方式拆开可得:
\(ans=\sum n*x^2 - 2\sum_{i=1}^na[i]*x+\sum_{i=1}^n a[i]^2\)
根据上式,我们只要枚举x(组装车间),同时维护\(\sum_{i=1}^na[i]\)和\(\sum_{i=1}^n a[i]^2\)这两个值就好了.
考虑如何枚举和维护?我们先假设组装车间x在所有生产车间的左边,则对于每种零件的生产车间,其对答案产生贡献的一定是最左边的那个生产车间.于是我们可以预处理出此时的答案,然后组装车间不断向右挪.
考虑组装车间不断向右挪的过程中,每种零件对答案产生贡献的生产车间如何变化?显然,对于同种零件的相邻两个生产车间,当组装车间越过它们的中点时,则对答案产生贡献的生产车间将会发生变化.
于是我们可以开一个结构体,按照同种零件相邻两个生产车间的中点坐标从小到大,存下转移前后的两个车间的坐标(这一小段,我知道我没有表述清楚,看不懂就直接看代码吧)
int n,m,tot;
long long x1,x2;//十年一场空,博主建议都开ll和db
double anspos,ans;
vector<int> b[20005];
struct battle{
int x,p;
}a[200005];//记录生产车间坐标和生产零件种类的结构体
bool cmp(battle a,battle b){
return a.x<b.x;
}
struct Battle{
int Old,New;
double mid;
}c[200005];//记录每一次转移的结构体
bool Cmp(Battle a,Battle b){
return a.mid<b.mid;
}
void add(int Old,int New){
c[++tot].Old=Old;
c[tot].New=New;
c[tot].mid=(Old+New)*1.0/2;
}
double Get(double x){return n*x*x-2*x1*x+x2;}
void turn(int Old,int New){
x1-=Old;x1+=New;
x2-=1LL*Old*Old;x2+=1LL*New*New;
}//类似于莫队的转移
int main(){
n=read();m=read();//n种零件,m个生产车间
for(int i=1;i<=m;i++){
a[i].x=read();//坐标
a[i].p=read();//生产零件的种类
}
sort(a+1,a+m+1,cmp);//按照坐标从小到大排序
for(int i=1;i<=m;i++)
b[a[i].p].push_back(a[i].x);
//此时每种零件的生产车间的坐标从小到大递增,存入数组b
for(int i=1;i<=n;i++)
for(int j=1;j<=b[i].size()-1;j++)
add(b[i][j-1],b[i][j]);
//每一次存入同一种零件的相邻两个生产车间,并记录中点
sort(c+1,c+tot+1,Cmp);//按照中点坐标从小到大
for(int i=1;i<=n;i++){
x1+=b[i][0];
x2+=b[i][0]*b[i][0];
}
//先假设组装车间在所有生产车间的左边
//所以计算每种零件最左边的生产车间对此时答案的贡献
anspos=x1*1.0/n;ans=Get(anspos);
//anspos组装车间的坐标,等于所有生产车间坐标的平均值
//得到组装车间的坐标后,ans就可以直接用公式计算
for(int i=1;i<=tot;i++){
turn(c[i].Old,c[i].New);
if(Get(x1*1.0/n)<ans){//更新最小值
ans=Get(x1*1.0/n);
anspos=x1*1.0/n;
}
}
printf("%.4lf\n",anspos);
return 0;
}