題目
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;
}