题解 [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 3000\)
显然水量 \(\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\)
通分:
两式相减:
当 \(x\geq 3\) 时可以归纳证。
由上可知分开合并不会比一起合并更劣,所以我们尽量地分开合并,同时也告诉我们, \(k\leq 10^9\) 的数据范围是吓人的。
设 \(dp_{i,j}\) 表示合并前 \(i\) 个储水罐,花费 \(j\) 次操作的答案,用 \(sumh_i\) 表示 \(h_i\) 的前缀和,可以得到转移方程:
这个方程复杂度是 \(O(n^2kp)\) 的(高精度小数类的计算还有一个 \(p\) ),无法接受,考虑优化。
这个式子显然是一个斜率优化的形式,把点 \((i-1,sumh_i-dp_i)\) 插入到一个下凸壳中,用三分查找最优点,时间复杂度降为 \(O(nkp\log_3n)\) ,仍然还要优化。
发现决策具有单调性。证明:
- 对于当前点 \((i,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\) 是单调不增的。证明比较简单:
- 设
- 则有
- 通分
- 发现
与假设矛盾,故对于
有
所以合并区间长度单调不增。
有了这个性质,可以发现合并次数不会超过 \(\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;
}
一个坑点:题面中给的高精度小数类的精度是 \(10^{-2100}\) 。