UOJ223 NOI2016 国王饮水记

Problem

UOJ

Solution

经过 瞎猜 思考可以得到以下性质:

  • 所有高度小于首都的水箱肯定不会参与联通。
  • 选择联通的水箱必然是从某一个水箱开始的连续的知道选到最高的水箱。因为如果中间有间隔,那么完全可以把前面的水箱舍弃掉最小的,然后换成间隔的较大的。
  • 如果要联通多次水箱,必然是先与相对较低的水箱联通,再与相对较高的水箱联通。否则交换顺序更优,可以推推式子证明。
  • k k k 最大取 n − 1 n-1 n1 即可,就是每次仅联通一个更大的水箱。

由此就可以设状态了,设 f [ i ] [ j ] f[i][j] f[i][j] 表示第 i i i 次联通最后一个联通到 j j j 的最大平均高度。记 s [ i ] s[i] s[i] 表示前缀高度和,

f [ i ] [ j ] = max ⁡ k = i − 1 j − 1 ( f [ i − 1 ] [ k ] + s [ j ] − s [ k ] j − k + 1 ) f[i][j]=\max_{k=i-1}^{j-1}\biggl(\frac {f[i-1][k]+s[j]-s[k]} {j-k+1}\biggr) f[i][j]=k=i1maxj1(jk+1f[i1][k]+s[j]s[k])

这长得很像一个斜率表达式,把点看做 ( k − 1 , s [ k ] − f [ i − 1 ] [ k ] ) (k-1,s[k]-f[i-1][k]) (k1,s[k]f[i1][k]) ,那么就是对 ( j , s [ j ] ) (j,s[j]) (j,s[j]) 找一个斜率最大的点转移。那么我们可以维护一个下凸壳,同时由于我们按高度进行排序了,也就是说点 j j j 到点 j + 1 j+1 j+1 的斜率也是单调递增的,那么在凸包上的切点也是单调的,因此用双端队列维护即可做到 O ( n k p ) O(nkp) O(nkp)了。

到这里你就能拿到85分了,然后可能就可以跑路了,因为后面用到的两个结论好难。。然而boshi表示用一个技巧就可以拿到100分了。就是我们可以先用double做一次转移,保存决策点,然后再用高精度小数库算答案即可。这样的时间复杂度为 O ( n 2 + n p ) O(n^2+np) O(n2+np)

Code

由于高精度小数库太长了,所以我就截掉了,如果要编译的话就把高精度小数库贴在这份代码前就好了。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=8010;
const double INF=1e15;
template <typename Tp> inline int getmin(Tp &x,Tp y){return y<x?x=y,1:0;}
template <typename Tp> inline int getmax(Tp &x,Tp y){return y>x?x=y,1:0;}
template <typename Tp> inline void read(Tp &x)
{
    x=0;int f=0;char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9')) ch=getchar();
    if(ch=='-') f=1,ch=getchar();
    while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
    if(f) x=-x;
}
struct vec{double x,y;}tmp,pt[maxn];
int n,k,p,tot,c,l,r,h[maxn],q[maxn];
ll s[maxn];
double f[2][maxn];
short g[maxn][maxn];
Decimal ans;
string str;
double slope(vec a,vec b){return (b.y-a.y)/(b.x-a.x);}
void input()
{
	int x;
	read(n);read(k);read(p);
	read(h[1]);tot=1;
	for(int i=2;i<=n;i++)
	{
		read(x);
		if(x>h[1]) h[++tot]=x;
	}
	sort(h+1,h+tot+1);
	getmin(k,tot-1);n=tot;
	for(int i=2;i<=n;i++){h[i]-=h[1];s[i]=s[i-1]+h[i];}
}
Decimal dfs(int i,int j)
{
	if(i==0) return Decimal(h[1]);
	int pos=g[i][j];
	Decimal res=dfs(i-1,pos),tmp;
	tmp=res+(s[j]-s[pos]);
	res=tmp/(j-pos+1);
	return res;
}
int main()
{
	input();
	for(int i=1;i<=k;i++)
	{
		c^=1;q[l=r=1]=i;
		pt[i].x=i-1;pt[i].y=s[i]-f[c^1][i];
		for(int j=i+1;j<=n;j++)
		{
			tmp.x=j;tmp.y=s[j];
			while(l+1<=r&&slope(pt[q[l]],tmp)<slope(pt[q[l+1]],tmp)) ++l;
			f[c][j]=slope(pt[q[l]],tmp);
			g[i][j]=q[l];
			pt[j].x=j-1;pt[j].y=s[j]-f[c^1][j];
			while(l+1<=r&&slope(pt[q[r-1]],pt[q[r]])>slope(pt[q[r]],pt[j])) --r;
			q[++r]=j;
		}
	}
	//printf("%.6lf\n",f[k&1][n]+h[1]);
	for(int i=2;i<=n;i++){h[i]+=h[1];s[i]=s[i-1]+h[i];}
	ans=dfs(k,n);
	str=ans.to_string(p);
	cout<<str;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值