1. 程式人生 > 其它 >P1721 [NOI2016] 國王飲水記

P1721 [NOI2016] 國王飲水記

傳送門


這題只能說太妙了,於是直接賀掉了

這邊建議移步到這裡,有更為嚴謹詳細的證明

下面的定理都是 PPT 上的定理,但證明略有不同,有一些自己的理解在裡面


基礎定理

  1. 對於所有水量低於 \(h_1\) 的城市,肯定不在我們的考慮範圍內

不然的話還要拿 \(h_1\) 的水去分給這些城市,那不是白給嗎

這也說明了 \(h_1\) 是不斷增大的

注:後面的證明都預設不再考慮小於 \(h_1\) 的城市

  1. 除了城市 \(1\),每個城市最多隻會被連線一次,而且每次操作都包含城市 \(1\)

假設我們先連線了某些城市,但不包括城市 \(1\),設這個城市集為 \(C\)

如果我們第二次操作是 \(C\)

加上城市 \(1\),那麼這顯然是與第一次直接連線 \(C\) 中的所有城市和城市 \(1\) 是等價的

如果第二次操作是 \(C\) 中的某些城市(假設選了 \(k\) 個城市)加上城市 \(1\),顯然不可能優於第一次直接連線 \(C\) 中最高的前 \(k\) 個城市和城市 \(1\)

而對於每個城市,如果它已經和城市 \(1\) 連線過了,那麼它以後的 \(h_i\) 都是小於等於 \(h_1\) 的,再次連線顯然不優

  1. 如果操作次數足夠,最優方案為:將所有城市從小到大排序,然後與 \(h_1\) 依次 連線

從小到大:由 定理2,我們得知每次城市用過一次後都會作廢,那我們要讓更多的城市的水量流入 \(h_1\)

,那顯然是水量越小的越靠前,才能讓它們的水量流入 \(h_1\)

依次:假如現在有 \(h_1<h_2<h_3\) ,如果一次性連線的話最後水量為 \(\frac{h_1+h_2+h_3}{3}\),依次連線的話最後水量為 \(\frac{h_1+h_2}{4}+\frac{h_3}{2}\)

現在我們將這兩個結果作差,得到 \(\frac{h_1}{12}+\frac{h_2}{12}-\frac{h_3}{6}<0\),因此“依次連線”是更優的;多個城市的時候同理


進階定理

注:以下的證明涉及的城市均不含 \(h_1\),且城市已經排好序

讓我們回到運算元 \(<\)

城市數的情況,這時候說明我們需要同時連線多個城市了

  1. 每次操作的最低水量一定會大於上一次操作的最大水量

證明和定理3從小到大是相似的,如果兩個相鄰的操作中存在逆序的水量,顯然將它們交換一下會更優

  1. 每次操作一定是連續的一段城市

否則我們一定可以將這次操作中水量最小的城市替換成中間斷點的某個城市,顯然更優

  1. 相鄰兩次操作的區間一定相鄰(即操作緊密

否則我們可以將前一次操作的區間整體向右移動,直到與後一次操作的區間相鄰,這樣會更優

到這裡,我們就可以發現一個 dp 做法了,我們令 \(sum[k] = \sum_{i=1}^k h[i]\),那麼有轉移方程為:

\[dp_{i,j}=max\{\frac{dp_{k,j-1}+sum[i]-sum[k]}{i-k+1}\} \]

其中 \(i\) 表示第 \(i\) 個城市,\(j\) 表示操作次數(分段數),\(k\) 表示轉移點

這個顯然是一個斜率方程,也就是我們要最大化過點 \((i, sum[i])\) 的直線的斜率,我們只需要三分凸殼上的最低點即可

到這裡複雜度是 \(O(nkp\ logn)\)

  1. 如果城市 \(i\) 的決策點 \(k\) 優於 決策點 \(s<k\),那麼城市 \(j>i\) 的決策點 \(k\) 同樣優於 \(s\) ,也就是決策單調

說實話這個我真的不可能想得到

此時應有不等式

\[(dp_{k,j-1}+sum[i]-sum[k])(i - s + 1)\ge(dp_{s,j-1}+sum[i]-sum[s])(i - k + 1) \]

經過漫長的整理,得:

\[(k-s)(sum_i - sum_k + dp_{k,j - 1})\ge(i - k + 1)(sum_k - sum_s+dp_{s,j - 1}-dp_{k,j - 1}) \]

由定義可得,\(dp_{k,j - 1}\ge dp_{l,j - 1}\),且 \(h_{i+1}>h_{i}>...>h_k>...>h_l\),等式兩遍加上 \((k-l)h_{i+1}\)

\[\begin{aligned} (k-s)(sum_{i+1} - sum_k + dp_{k,j - 1}) &\ge (i - k + 1)h_k+(k-s)h_{i+1}-(i - k + 1)(dp_{k,j - 1}-dp_{k - 1, j - 1})\\ &\ge (i - k + 1)(h_k-(dp_{k,j - 1} - dp_{k - 1, j - 1})) \end{aligned} \]

證畢 (說實話我有點看不懂QAQ)

到這我們就可以將每次尋找決策點的過程均攤為 \(O(1)\)

時間複雜度為 \(O(nkp)\)


最終定理

也就是我不會證明的定理

  1. 每次操作的區間長度不會超過上一次的長度

如果有一次操作的區間大於上一次操作的區間,我們就將這次的區間的第一個城市移動到上一次操作中,方案會更優

至於為什麼,我不知道,自己打表驗證吧......

  1. 所有水量互不相同的情況下,操作區間長度大於 \(1\) 的不會超過 \(log\ \frac{nh}{min(h_{i+1} - h_i)}\),上界為 \(14\)

不會證明,需要的移步講課PPT


程式碼:

這題的坑點就是,他幫你寫的定點高精的小數部分陣列只開了 \(234\) 位,但是他最後一個點要求輸出 \(3000\) 位,所以還要自己手動增大......(把我坑得wa了5次)

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<cstring>
#include<queue>
#include<map>
#include<set>
#include<bitset>
#define LL long long
inline int reads()
{
    int sign = 1, re = 0; char c = getchar();
    while(c < '0' || c > '9'){if(c == '-') sign = -1; c = getchar();}
    while('0' <= c && c <= '9'){re = re * 10 + (c - '0'); c = getchar();}
    return sign * re;
}
struct Point
{
    double x, y;
}p[8005];
inline double slope(Point a, Point b) {return (a.y - b.y) / (a.x - b.x);}
int n, k, P, h[8005], tot, sum[8005];
int q[8005], he, ta, fin, pos;
double dp[8005][20], Mx; int fo[8005][20];
Decimal ans;
Decimal get_de(int i, int j)
{
    if(!j) return h[1];
    return (get_de(fo[i][j], j - 1) + sum[i] - sum[fo[i][j]]) / (i - fo[i][j] + 1);
}
signed main()
{
#ifndef ONLINE_JUDGE
    freopen("test.in", "r", stdin);
    freopen("test.out", "w", stdout);
#endif
    n = reads(), k = reads(), P = reads(); h[tot = 1] = reads();
    for(int i = 2; i <= n; i++)
    {
        int a = reads();
        if(a > h[1]) h[++tot] = a;
    }
    std::sort(h + 1, h + 1 + tot); k = std::min(k, tot);
    for(int i = 1; i <= tot; i++) sum[i] = sum[i - 1] + h[i], dp[i][0] = h[1];
    for(int j = 1; j <= std::min(k, 14); j++)
    {
        q[he = ta = 1] = 1;
        for(int i = 1; i <= tot; i++) p[i] = (Point){i - 1, sum[i] - dp[i][j - 1]};
        for(int i = 2; i <= tot; i++)
        {
            Point now = (Point){i, sum[i]};
            while(ta > he && slope(now, p[q[he]]) < slope(now, p[q[he + 1]])) he++;
            dp[i][j] = (dp[q[he]][j - 1] + sum[i] - sum[q[he]]) / (i - q[he] + 1); fo[i][j] = q[he];
            while(ta > he && slope(p[q[ta - 1]], p[q[ta]]) >= slope(p[i], p[q[ta]])) ta--;
            q[++ta] = i;
        }
    }
    fin = tot - k + std::min(k, 14);
    for(int i = 0; i <= std::min(k, 14); i++)
        if(dp[fin][i] > Mx) Mx = dp[fin][i], pos = i;
    ans = get_de(fin, pos);
    for(int i = fin + 1; i <= tot; i++) ans = (ans + h[i]) / 2;
    std::cout << ans.to_string(P << 1);
    return 0;
}