题目
n(1<=n<=1e5)的数组a[],第i个数为ai(0<=ai<=1e9),每次操作你可以选以下三种中的一种
①选择i,令ai+1,代价为A
②选择i,令ai-1,代价为R
③选择i,j,令其中一个-1,另一个+1,代价为M
0<=A,R,M<=1e4,求最终的数组所有值都相等的最小代价
思路来源
https://www.cnblogs.com/1024-xzx/p/12926595.html
https://www.cnblogs.com/Kaike/p/12918815.html
题解
如果只有A和R,显然可以从一边往中间推,第i个向第i+1个转移时,
增量为i*A,减量为(n-i)*R,如果可以推就推,这样就能推到最小点,是开口向上的函数
类似的,若M合适,则最终会只使用AM或RM,同样是两个开口向上的函数
而最终的答案,是这三个函数取min,容易发现也是存在一个最小值点的,可三分
因为最小值只有一个点,这里记录一下三分的两种写法吧
官方题解给了一个口胡的证明,设上升个数p和下降个数q,分p<=q和p>q讨论
再设当前高度h,设当前小于h的个数为x,则大于等于的为x+1,
考虑h到h+1的变化,发现代价变化量为x的一次函数,而x发生变化当且仅当在初始高度分界点
说明当p<=q时为若干段分段线性函数,p>q同理,
题解还说明当二者之间变化时还会出现一个极值点(没太看懂),和所有初始分界点(极值点)取小即可
另外,通过以上分析,可以判断是一个连续函数导数线性变化,最多变号一次的单峰函数,
那么分段函数也有如此特性,结合题目性质发现开口向上,果断三分
代码1
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int n,add,sub,mix,h[N],l,r;
ll cal(int x){
ll y=0,z=0,sm,v;//add sub
for(int i=1;i<=n;++i){
if(h[i]<x)y+=x-h[i];
else z+=h[i]-x;
}
sm=min(y,z);y-=sm;z-=sm;
v=y*add+z*sub+sm*mix;
//printf("x:%d cal:%lld\n",x,v);
return v;
}
int main()
{
scanf("%d",&n);
scanf("%d%d%d",&add,&sub,&mix);
mix=min(mix,add+sub);
l=1e9+1;r=-1;
for(int i=1;i<=n;++i){
scanf("%d",&h[i]);
l=min(l,h[i]);r=max(r,h[i]);
}
while(r-l>2){//考虑[1,3]时m=mm 但1为最小点
int m=(l+r)/2,mm=(m+r)/2;//m=mm时有r-l<=2
if(cal(m)<cal(mm))r=mm-1;
else l=m+1;
}
ll ans=cal(l);
for(int i=l+1;i<=r;++i){
ans=min(ans,cal(i));
}
printf("%lld\n",ans);
return 0;
}
代码2
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int n,add,sub,mix,h[N],l,r;
ll cal(int x){
ll y=0,z=0,sm,v;//add sub
for(int i=1;i<=n;++i){
if(h[i]<x)y+=x-h[i];
else z+=h[i]-x;
}
sm=min(y,z);y-=sm;z-=sm;
v=y*add+z*sub+sm*mix;
//printf("x:%d cal:%lld\n",x,v);
return v;
}
int main()
{
scanf("%d",&n);
scanf("%d%d%d",&add,&sub,&mix);
mix=min(mix,add+sub);
l=1e9+1;r=-1;
for(int i=1;i<=n;++i){
scanf("%d",&h[i]);
l=min(l,h[i]);r=max(r,h[i]);
}
while(l<r){
int lm=l+(r-l)/3,rm=r-(r-l)/3;//m=mm时有r-l<=2
if(cal(lm)<cal(rm))r=rm-1;
else l=lm+1;
}
printf("%lld\n",cal(l));
return 0;
}