[NOI2016] 国王饮水记(luogu P1721)(超长慎入)

在跳蚤国,国王面临的挑战是解决首都的饮水问题。每个城市都有不同高度的水箱,国王可以使用地下连通系统最多k次,使得首都水箱的水位达到最大。问题转化为找到最佳的连通策略,确保1号城市水箱的水位最大化。通过数学归纳法和动态规划,可以确定最优策略并计算出最高水位。

Description

跳蚤国有 n 个城市,伟大的跳蚤国王居住在跳蚤国首都中,即 1 号城市中。跳蚤国最大的问题就是饮水问题,由
于首都中居住的跳蚤实在太多,跳蚤国王又体恤地将分配给他的水也给跳蚤国居民饮用,这导致跳蚤国王也经常喝
不上水。于是,跳蚤国在每个城市都修建了一个圆柱形水箱,这些水箱完全相同且足够高。一个雨天后,第 i 个
城市收集到了高度为 hi 的水。由于地理和天气因素的影响,任何两个不同城市收集到的水高度互不相同。跳蚤国
王也请来蚂蚁工匠帮忙,建立了一个庞大的地下连通系统。跳蚤国王每次使用地下连通系统时,可以指定任意多的
城市,将这些城市的水箱用地下连通系统连接起来足够长的时间之后,再将地下连通系统关闭。由连通器原理,这
些城市的水箱中的水在这次操作后会到达同一高度,并且这一高度等于指定的各水箱高度的平均值。由于地下连通
系统的复杂性,跳蚤国王至多只能使用 k 次地下连通系统。跳蚤国王请你告诉他,首都 1 号城市水箱中的水位最
高能有多高?

Input
输入的第一行包含 3 个正整数 n,k,p分别表示跳蚤国中城市的数量,跳蚤国王能使用地下连通系统的最多次数,
以及你输出的答案要求的精度。p 的含义将在输出格式中解释。接下来一行包含 n 个正整数,描述城市的水箱在
雨后的水位。其中第 i 个 正整数 hi 表示第 i 个城市的水箱的水位。保证 hi 互不相同,1≤hi≤10^5
对于所有数据,满足3≤p≤3000,1≤n≤8000,1≤k≤10^9

Output
仅一行一个实数,表示 1 号城市的水箱中的最高水位。这个实数只可以包含非负整数部分、小数点和小数部分。
其中非负整数部分为必需部分,不加正负号。若有小数部分,则非负整数部分与小数部分之间以一个小数点隔开。
若无小数部分,则不加小数点。你输出的实数在小数点后不能超过 2p 位,建议保留至少 p 位。数据保证参考答
案与真实答案的绝对误差小于 10^-2p。你的输出被判定为正确当且仅当你的输出与参考答案的绝对误差小于 10^-
p

Sample Input
3 1 3
1 4 3
Sample Output
2.666667

Explanation
由于至多使用一次地下连通系统,有以下 5 种方案:

  1. 不使用地下连通系统:此时 1 号城市的水箱水位为 1。
  2. 使用一次连通系统,连通 1、2 号:此时 11 号城市的水箱水位为 5/2。
  3. 使用一次连通系统,连通 1、3 号:此时 1 号城市的水箱水位为 2/2。
  4. 使用一次连通系统,连通 2、3号:此时 1 号城市的水箱水位为 1。
  5. 使用一次连通系统,连通 1、2、3号:此时 11 号城市的水箱水位为 8/3。

正解:决策单调性+斜率优化+DP
解题报告:
  Picks学长出的NOI2016神题

首先列出所有要用到的定理:
   定理一:所有水量小于等于1号城市水量的城市都不对答案产生贡献。

证明:这个应该是显然的吧…

(下面所有定理均略去“在最优方案中”辣)
  定理二: 除一号城市外,每个城市最多被连通一次。 
  定理三: 每一次连通都一定和1号城市连通。

证明:首先可以发现两个定理等价,嘿嘿嘿。
      定理二推导定理三: 一次连接如果未和1号城市连接,则之后这些城市就没有意义了。可以删去这次操作。
      定理三推导定理二: 一个城市若和1号城市连通一次,此时其水量等于1号城市,不会出现第二次。
      
考虑使用数学归纳法证明:
当只有一次操作时显然。不妨假设对于最后m个操作,他们均成立,接下来只要考虑倒数第m+ 1个操作。

若该操作包含了1号城市,则由等价性可直接得出结论。

若该操作不包含1号城市,注意到在这次操作之后,与本次操作相关联的水杯都变为了同一高度,

即我们可以任意交换他们在之后的编号。

假设存在一些城市在之后也用到了。

由于后m个操作都满足这两个定理,我们可以发现:

通过交换编号,可以使得这些城市的水量自出现顺序从后向前而由大到小变化。

且此时删去第m + 1次操作所得答案更优。(就是没有用辣)

定理四:当k → ∞时,最优方案为排序后由小到大将比1号城市大的城市与1号城市依次相接。
     
    证明:注意到有无穷多次操作时,任意一个操作都可以拆成无数次某城市与1号城市的连接。
       此时只需要考虑所有选择1号城市和某城市的操作即可。
       显然一个城市连接完之后就废了,且任意两个城市一定是水量小的先连接。那么只要按顺序连接一下就好了。
       (关于这里说水量小的先连接,可以感性认识一下呀…
        如果和大的先连接,那么小的相较之下最后也会被拔高一点点,就会产生“浪费”)

定理五:每次操作选择的城市的最小水量一定大于前一次操作所选择的城市的最大水量。

证明:这个应该反证法很好证呀。只要存在不满足的情况的话,我们交换之后,答案肯定更优。
       可以通过列交换前后的高度变化的不等式来证明这样是肯定更优的。

定理六:每次操作选择的城市均是选择排序后连续的一段城市(选择一个区间)。
  
    证明:我会反证法!嘿嘿嘿
       考虑如果去掉水量最小的城市,再选择一个两段的间隔处(或者叫做断点处)的一座城市。  
       这样答案必然比之前更优。

定理七:任意两次相邻操作选择的区间之间不存在城市(排列紧密)。

证明:将靠左侧的区间右移一位,显然更优。

定理五、六、七一出,题目模型就可以转化辣,是不是有一种熟悉的感觉,然后就很资瓷了啊!
 
  我们很快可以得到一个区间DP的常用转移式:
  令fi,j表示到第i个城市,用了j次连通器后1号城市的最高水位,则
    
      fi,j=maxk<i(fk,j−1+Si−Ski−k+1)
      其中,Si表示水量前缀和
    
    因为这个高精度小数库是O§的,所以这个复杂度是:O(n2Kp)

然而这个式子显然可以斜率优化,那么我们再把这个式子变变形:
  
    fi,j=maxk<i(Si−(Sk−fk,j−1)i−(k−1))
      
    就可以维护一个凸包,然后每次在上一层的凸包上三分即可。
    复杂度:O(nkplogn)

然后似乎有决策单调性呀!打个表,发现确实是这样的…
  之后就是常规的单调性斜率DP的优化,不赘述辣…要证的话其实还是可以证明的!只是很难打公式的…窝这么懒,还是算了吧…
  
    然后复杂度就变成了:O(nkp)

就如Picks的讲题PPT中说的,我们似乎有个条件没用:所有水量高度互不相同!
  呀!我们又可以得到两个新的结论:
  
  1、每一次操作的区间长度一定不比上一次操作的区间长度长!
  2、在所有水量高度互不相同的情况下,长度大于1的区间仅有O(lognhH)个,其中H=min(hi−hi−1)

第一个结论可以很快证明出来,因为反证法同样可以证明呀…
  假设存在,然后列一列交换前后的答案的计算式,会发现不等关系显然成立。
 
  第二个结论,窝好不容易搞懂了,看还是看得懂得…然而睡了一觉起来又忘了…看来以后搞懂之后必须马上写下来。
  但是也并不好推…
  那怎么办呢?

打表!找规律!

会发现其实很多层转移之后,每次的操作区间很快就会变成长度为1的区间。
  大概发现最多14层,之后就不会再有大于1的区间出现。这种结论肯定是在考场上打表和猜猜猜+对拍!这比证明要快得多…
  那么我们DP14层之后就可以直接计算答案了。
  最后的总复杂度为O(nplog(nh))
  
  ps:这道题如果在知道结论之后就变得异常好做了…然而如果要完整证明所有的定理…GG…    
    事实证明:打表大法好!找规律大法妙!

补充(一些细节的说明):
    显然我不能每次计算答案,因为高精度小数库的复杂度同样很高,
    我需要把每次的决策点,和从哪里转移过来的记录下来,最后再来跑一遍就可以了。
    注意要把高精度小数库的位数改成3000!
    考虑最多有14个长度大于1的区间,那么我剩下的操作都是只选一个。
   所以我们就需要枚举必须只选1个的位置之前,一共选了多少次(选的多不一定优),
   根据我记录下来的转移位置,递归计算即可。

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <ctime>
#include <vector>
#include <queue>
#include <map>
#include <set>
#include <string>
#include <complex>
using namespace std;
typedef long long LL;
 
// ---------- decimal lib start ----------
const int PREC = 3000;//!!!
class Decimal {
   
   
    public:
        Decimal();
        Decimal(const std::string &s);
        Decimal(const char *s);
        Decimal(int x);
        Decimal(long long x);
        Decimal(double x);
         
        bool is_zero() const;
         
        // p (p > 0) is the number of digits after the decimal point
        std::string to_string(int p) const;
        double to_double() const;
         
        friend Decimal operator + (const Decimal &a, const Decimal &b);
        friend Decimal operator + (const Decimal &a, int x);
        friend Decimal operator + (int x, const Decimal &a);
        friend Decimal operator + (const Decimal &a, long long x);
        friend Decimal operator + (long long x, const Decimal &a);
        friend Decimal operator + (const Decimal &a, double x);
        friend Decimal operator + (double x, const Decimal &a);
         
        friend Decimal operator - (const Decimal &a, const Decimal &b);
        friend Decimal operator - (const Decimal &a, int x);
        friend Decimal operator - (int x, const Decimal &a);
        friend Decimal operator - (const Decimal &a, long long x);
        friend Decimal operator - (long long x, const Decimal &a);
        friend Decimal operator - (const Decimal &a, double x);
        friend Decimal operator - (double x, const Decimal &a);
         
        friend Decimal operator * (const Decimal &a, int x);
        friend Decimal operator * (int x, const Decimal &a);
         
        friend Decimal operator / (const Decimal &a, int x);
         
        friend bool operator < (const Decimal &a, const Decimal &b);
        friend bool operator > (const Decimal &a, const Decimal &b);
        friend bool operator <= (const Decimal &a, const Decimal &b);
        friend bool operator >= (const Decimal &a, const Decimal &b);
        friend bool operator == (const Decimal &a, const Decimal &b);
        friend bool operator != (const Decimal &a, const Decimal &b);
         
        Decimal & operator += (int x);
        Decimal & operator += (long long x);
        Decimal & operator += (double x);
        Decimal & operator += (const Decimal &b);
         
        Decimal & operator -= (int x);
        Decimal & operator -= (long long x);
        Decimal & operator -= (double x);
        Decimal & operator -= (const Decimal &b);
         
        Decimal & operator *= (int x);
         
        Decimal & operator /= (int x);
         
        friend Decimal operator - (const Decimal &a);
         
        // These can't be called
        friend Decimal operator * (const Decimal &a, double x);
        friend Decimal operator * (double x, const Decimal &a);
        friend Decimal operator / (const Decimal &a, double x);
        Decimal & operator *= (double x);
        Decimal & operator /= (double x);
         
    private:
        static const int len = PREC / 9 + 1;
        static const int mo = 1000000000;
         
        static void append_to_string(std::string &s, long long x);
         
        bool is_neg;
        long long integer;
        int data[len];
         
        void init_zero();
        void init(const char *s);
};
 
Decimal::Decimal() {
   
   
    this->init_zero();
}
 
Decimal::Decimal(const char *s) {
   
   
    this->init(s);
}
 
Decimal::Decimal(const std::string &s) {
   
   
    this->init(s.c_str());
}
 
Decimal::Decimal(int x) {
   
   
    this->init_zero();
     
    if (x < 0) {
   
   
        is_neg = true;
        x = -x;
    }
     
    integer = x;
}
 
Decimal::Decimal(long long x
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值