題解 P1721 [NOI2016]國王飲水記
題解 P1721 [NOI2016]國王飲水記
題意描述:
有 \(n\) 個水量為 \(h_i\) 的儲水罐,每次操作可以選任意多個儲水罐 \(a_1\sim a_x\) ,使這些儲水罐的水量全部變為 \(\frac{\sum\limits_{i=1}^x h_{a_i}}{x}\) ,最多進行 \(k\) 次,問 \(1\) 號儲水罐水量最多有多少,答案精度誤差不大於 \(10^{-p}\) 。
\(n\leq 8000,k\leq 10^9,p\leq 2000\)
顯然水量 \(\leq h_1\) 的儲水罐沒有任何貢獻,所以先按 \(h_i\) 從小到大排序,再以 \(1\) 為起點計算。
考慮怎樣合併儲水罐答案最大:
- 方案一:直接合並所有要合併的儲水罐,不妨設合併 \(3\) 個,答案為 \(\frac{h_1+h_2+h_3}{3}\) 。
- 方案二:分多次合併儲水罐,不妨設每次合併 \(2\) 個,答案為 \(\frac{\frac{h_1+h_2}{2}+h_3}{2}\)
這裡由於我們已經排序,所以 \(h_1<h_2<h_3\)
通分:
\[\begin{aligned}\frac{h_1+h_2+h_3}{3}&=4\times(h_1+h_2+h_3)\\&=4\times h_1+4\times h_2+4\times h_3\end{aligned} \]\[\begin{aligned}\frac{\frac{h_1+h_2}{2}+h_3}{2}&=6\times(\frac{h_1+h_2}{2}+h_3)\\&=3\times(h_1+h_2)+6\times h_3\\&=3\times h_1+3\times h_2+6\times h_3\end{aligned} \]兩式相減:
當 \(x\geq 3\)
由上可知分開合併不會比一起合併更劣,所以我們儘量地分開合併,同時也告訴我們, \(k\leq 10^9\) 的資料範圍是嚇人的。
設 \(dp_{i,j}\) 表示合併前 \(i\) 個儲水罐,花費 \(j\) 次操作的答案,用 \(sumh_i\) 表示 \(h_i\) 的字首和,可以得到轉移方程:
\[dp_{i,j}=\max_{1\leq k<i}\{\frac{dp_{k,j-1}+h_i-h_k}{i-k+1}\} \]這個方程複雜度是 \(O(n^2kp)\) 的(高精度小數類的計算還有一個 \(p\) ),無法接受,考慮優化。
這個式子顯然是一個斜率優化的形式,把點 \((i-1,sumh_i-dp_i)\) 插入到一個下凸殼中,用三分查詢最優點,時間複雜度降為 \(O(nkp\log_3n)\) ,仍然還要優化。
發現決策具有單調性。證明:
- 對於當前點 \((i-1,sumh_i)\) ,有一個最優的轉移點 \((j-1,sumh_j-dp_j)\) 由於合併後平均水量一定增加,所以如果對於一個次優的轉移點 \((k-1,sumh_k-dp_k)\) ,有:
- \(\because 1\leq h_i\leq 10^5\), \(h_i\) 互不相同
- \(\therefore sumh_i\geq i-1\)
- 根據下凸殼性質,得到 \(sumh_j-dp_j\geq j-1,sumh_k-dp_k\geq k-1\)
- \(\therefore\) 下一個決策點至少為 \((i+1,sumh_i+i)\)
證畢。
此時可以不需要三分,複雜度降至 \(O(nkp)\) ,但對於 \(8000\times 8000\times 2000\) 的範圍還是無法通過。
發現 \(n\) 和 \(p\) 是搞不掉了,只能在 \(k\) 上面動刀子,這時就要想一想合併的 \(h\) 有什麼性質了。由於 \(h_i\) 互不相同,所以排序後 \(h\) 一定是一個上升序列,對於上升序列中的一段區間的平均值,我們發現後面比前面取的更少會更優,即決策的 \(\Delta i\) 是單調不增的。證明可以假設 \(+\) 推式子:
- 設
- 則有
- 通分
- 發現
與假設矛盾,故對於
\[y-x+1>z-y+1 \]\[p-x+1\leq z-p+1 \]有
\[\frac{\frac{\sum\limits_{i=x}^{y}h_i}{y-x+1}+\sum\limits_{i=y+1}^{z}h_i}{z-x+1}\leq \frac{\frac{\sum\limits_{i=x}^{p}h_i}{p-x+1}+\sum\limits_{i=p+1}^{z}h_i}{z-p+1} \]所以合併區間長度單調不增。
有了這個性質,可以發現合併次數不會超過 \(\log \frac{n\times h}{\min(h_i-h_{i-1})}\) 次,約為 \(14\) 次,這時問題就迎刃而解了。
核心程式碼:
Decimal ans;
int n,K,p,cnt;
int H[N],Sum[N];
double dp[N][25];
int pos[N][25];
int q[N],hd,tl;
vector<pair<double,double> >t;
double Slope(int i,int j){return(double)(t[j].se-t[i].se)/(t[j].fi-t[i].fi);}
Decimal print(int cur,int stp){
if(stp==0)return H[1];
return(print(pos[cur][stp],stp-1)+Sum[cur]-Sum[pos[cur][stp]])/(cur-pos[cur][stp]+1);
}
signed main(){
n=read();K=read();p=read();H[++cnt]=read();
for(int i=2;i<=n;i++){
int x=read();
if(x>H[1])H[++cnt]=x;
}
n=cnt;
sort(H+1,H+n+1);
for(int i=1;i<=n;i++)Sum[i]=Sum[i-1]+H[i];
for(int i=1;i<=n;i++)dp[i][0]=H[1];
K=min(K,n);
int lim=min(K,14);
for(cnt=1;cnt<=lim;cnt++){
hd=1;tl=0;
q[++tl]=1;
t.clear();t.pb(mp(0,0));
for(int i=1;i<=n;i++)t.pb(mp(i-1,Sum[i]-dp[i][cnt-1]));
for(int i=2;i<=n;i++){
t.pb(mp(i,Sum[i]));
while(hd<tl&&Slope(n+1,q[hd])<Slope(n+1,q[hd+1]))hd++;
t.pop_back();
pos[i][cnt]=q[hd];
dp[i][cnt]=1.*(dp[q[hd]][cnt-1]+Sum[i]-Sum[q[hd]])/(i-q[hd]+1);
while(hd<tl&&Slope(q[tl-1],q[tl])>Slope(q[tl],i))tl--;
q[++tl]=i;
}
}
cnt=n-K+lim;
int id=0;
for(int i=0;i<=lim;i++)
if(dp[cnt][i]>dp[cnt][id])id=i;
ans=print(cnt,id);
for(int i=cnt+1;i<=n;i++)ans=(ans+H[i])/2;
cout<<ans.to_string(p<<1)<<endl;
return 0;
}