[NOI2016] 国王饮水记

CXLVII.[NOI2016] 国王饮水记

首先,我们一定可以舍去那些高度比 \(h_1\) 还小的城市,并且将剩余的高度比 \(h_1\) 大的城市排序,使得 \(h_1\)\(h_n\) 递增。

我们不妨从三座城市想起。假如可以合并两次,应该怎么合并?

先合并 \((1,2)\),再合并 \((2,3)\),因为这样更多的水被贡献给了 \(1\),对吧?

由此我们得出了第一个Obeservation:越早合并的城市,高度越低。这也就意味着,我们每次合并的城市,必定是高度递增的一段,且前一次合并的所有城市的水量都低于后一次合并的城市。

那如果只能合并一次呢?

首先,\((1,3)\) 肯定是要合并的。那么,\(2\) 要不要并进去呢?这就要看 \((1,3)\) 合并后,\(2\) 是否比 \(1,3\) 的高度高了。

于是我们得到了用一次合并来合并城市 \(1\) 与某段城市 \([l,r]\) 的策略:先合并城市 \(1\)\(r\),然后依次合并 \(r-1,r-2,\dots,l\),直到并进去会使得平均值减小。

有了这两个观察,我们就可以开始DP了:设 \(f_{i,j}\) 表示前 \(i\) 个位置合并 \(j\) 次的最优答案。则 \(f_{i,j}=\max\limits_{k<i}\left\{\dfrac{f_{k,j-1}+s_i-s_k}{i-k+1}\right\}\)

同时,为了处理上面“合并 \([l,r]\) 时实际上是合并了 \([l,r]\) 中一段最优的后缀 \([l',r]\)”的结论,\(f_{i,j}\) 还要与 \(f_{i-1,j},f_{i-2,j},\dots,f_{1,j}\)\(\max\),用来表示位置 \(i\) 不合并到某段区间中。

这种“合并 \(k\) 次”的DP,要优化肯定要按照合并次数DP。于是我们设 \(f_i\) 表示当前的DP数组,\(g_i\) 表示上一轮DP(合并次数少一)的DP数组。

则式子修改为 \(f_i=\max\limits_{j<i}\left\{\dfrac{g_j+s_i-s_j}{i-j+1}\right\}\)

这时候,如果再观察到“合并次数 \(m\) 可以与 \(n-1\)\(\min\)”的话,就可以 \(O(n^3)\) 地直接用 double 暴力DP了。

可以取得 \(58\) 分的好成绩。代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,p,h[8010];
double f[8010],g[8010],s[8010];
void solve(){
	for(int i=1;i<=n;i++)f[i]=-0x3f3f3f3f3f3f3f3f;
	for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)f[i]=max(f[i],(g[j]+s[i]-s[j])/(i-j+1));
	for(int i=1;i<=n;i++)f[i]=max(f[i],f[i-1]);
}
int main(){
	scanf("%d%d%d",&n,&m,&p);
	for(int i=1;i<=n;i++)scanf("%d",&h[i]);
	sort(h+2,h+n+1),reverse(h+2,h+n+1);
	while(h[n]<=h[1])n--;
	reverse(h+2,h+n+1);
//	for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
	for(int i=1;i<=n;i++)s[i]=s[i-1]+h[i];
	m=min(m,n-1);
	for(int i=1;i<=n;i++)f[i]=h[1];
	while(m--){
		for(int i=1;i<=n;i++)g[i]=f[i];
		solve();
	}
	printf("%lf\n",f[n]);
	return 0;
}

然后,有一个推论是“合并 \([l,r]\) 时实际上是合并了 \([l,r]\) 中一段最优的后缀 \([l',r]\)”这个东西实际上是没有必要的,因为若 \([l,r]\) 只合并了 \([l',r]\),而 \([l,l'-1]\) 未被合并,但是以 \(l-1\) 结尾的一段区间却被合并了的话,我们完全可以将它向右移至以 \(l'-1\) 结尾,并使得答案更大。因此,这个结论变换为,所有被合并过的位置是 \(h\) 数组的一段后缀。(虽然这个结论是我在写题解时才发现的,因此代码中完全没有用到这个结论)

这之后,我们掏出转移式 \(f_i=\max\limits_{j<i}\left\{\dfrac{g_j+s_i-s_j}{i-j+1}\right\}\)

它可以被解释为 \((s_i,i+1)\)\((s_j-g_j,j)\) 两点间的斜率。

因为我们已经将 \(h\) 排过序,所以 \(s\) 就不止有递增的性质——它还是的!

尽管 \((s_j-g_j,j)\) 可能在平面上随机分布,但是因为 \(s\) 的凹性,其就具有决策单调性(随着 \(i\) 的增大,每个点到它的斜率都在变大(这是由凹性决定的),而越大的 \(j\) 变大的速度就越快(因为所有的 \(j\) 的分子增长速度都是一致的,但是分母增长速度是 \(j\) 越大的越慢),终将在什么时候超越小的 \(j\)),因此我们可以对其进行斜率优化!

具体而言,发现最优决策点 \(j\) 只有可能在下凸壳上,于是就维护下凸壳,然后从下凸壳的队首进行转移即可。

明显单次复杂度就做到了 \(O(n)\)。这样子,若仍然用 double 暴力转移,复杂度是 \(O(n^2)\),可以取得 \(64\) 分的好成绩。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef pair<double,double> pdd;
int n,m,p,h[8010],l,r;
double f[8010],g[8010],s[8010];
pdd dat[8010];
int q[8010];
double slope(pdd x,pdd y){return (x.second-y.second)/(x.first-y.first);}
void solve(){
	l=r=0;
	for(int i=1;i<=n;i++){
		dat[i]=make_pair(i,s[i]-g[i]);
		while(r-l>=2&&slope(dat[q[r-2]],dat[q[r-1]])>slope(dat[q[r-1]],dat[i]))--r;
		q[r++]=i;
		pdd I=make_pair(i+1,s[i]);
		while(r-l>=2&&slope(dat[q[l]],I)<slope(dat[q[l+1]],I))l++;
		f[i]=(g[q[l]]+s[i]-s[q[l]])/(i-q[l]+1);
	}
	for(int i=1;i<=n;i++)f[i]=max(f[i],f[i-1]);
}
int main(){
	scanf("%d%d%d",&n,&m,&p);
	for(int i=1;i<=n;i++)scanf("%d",&h[i]);
	sort(h+2,h+n+1),reverse(h+2,h+n+1);
	while(h[n]<=h[1])n--;
	reverse(h+2,h+n+1);
//	for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
	for(int i=1;i<=n;i++)s[i]=s[i-1]+h[i];
	m=min(m,n-1);
	for(int i=1;i<=n;i++)f[i]=h[1];
	while(m--){
		for(int i=1;i<=n;i++)g[i]=f[i];
		solve();
	}
	printf("%lf\n",f[n]);
	return 0;
}

到这里,优化似乎已经到头了——单次DP已经压到底线 \(O(n)\) 了。因此要优化只能从合并次数 \(m\) 这一维下手了。

因为水量互不相等,所以肯定是越晚合并的区间长度越短,不然我们一定可以将一个原本在后面合并的元素移到前面并使得答案增加。

依据这个结论,有个性质是若 \(k\) 较大时,决策的区间长度会很快收敛到 \(1\)。而又有一个结论是长度大于 \(1\) 的区间仅有最多 \(\log nh\) 个,约 \(14\) 个。

于是我们只需DP \(14\) 层,剩下的部分全部选长度为 \(1\) 的区间就行了。

(证明?打表就行了!)

还有一个由“互不相同”带来的结论是,比较大小关系时精度只需要十几位就行了,这是显然的。于是我们DP时直接用普通 double 存,然后记录转移点,在DP结束后再用高精度小数按图索骥复原出完整的精度即可。

时间复杂度 \(O(n\log n+np)\)

代码(省略模板部分):

#include<bits/stdc++.h>
using namespace std;
typedef pair<double,double> pdd;
int n,m,p,h[8010],l,r,las[17][8010];
double f[8010],g[8010],s[8010];
pdd dat[8010];
int q[8010];
double slope(pdd x,pdd y){return (x.second-y.second)/(x.first-y.first);}
void solve(int ID){
	l=r=0;
	for(int i=1;i<=n;i++){
		dat[i]=make_pair(i,s[i]-g[i]);
		while(r-l>=2&&slope(dat[q[r-2]],dat[q[r-1]])>slope(dat[q[r-1]],dat[i]))--r;
		q[r++]=i;
		pdd I=make_pair(i+1,s[i]);
		while(r-l>=2&&slope(dat[q[l]],I)<slope(dat[q[l+1]],I))l++;
		f[i]=(g[q[l]]+s[i]-s[q[l]])/(i-q[l]+1),las[ID][i]=q[l];
	}
	for(int i=1;i<=n;i++)if(f[i]<f[i-1])f[i]=f[i-1],las[ID][i]=-1;
}
Decimal calc(int i,int j){
	if(!i)return h[1];
	if(las[i][j]==-1)return calc(i,j-1);
	return (calc(i-1,las[i][j])+s[j]-s[las[i][j]])/(j-las[i][j]+1);
}
int main(){
	scanf("%d%d%d",&n,&m,&p);
	for(int i=1;i<=n;i++)scanf("%d",&h[i]);
	sort(h+2,h+n+1),reverse(h+2,h+n+1);
	while(h[n]<=h[1])n--;
	reverse(h+2,h+n+1);
//	for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
	for(int i=1;i<=n;i++)s[i]=s[i-1]+h[i];
	m=min(m,n-1);
	int lim=min(m,14);
	for(int i=1;i<=n;i++)f[i]=h[1];
	for(int j=1;j<=lim;j++){
		for(int i=1;i<=n;i++)g[i]=f[i];
		solve(j);
	}
	Decimal res=calc(lim,n-m+lim);
	for(int i=n-m+lim+1;i<=n;i++)res=(res+h[i])/2;
	cout<<res.to_string(p<<1)<<endl;
	return 0;
}

posted @ 2021-03-31 15:29  Troverld  阅读(91)  评论(0编辑  收藏  举报