[COCI2015-2016#6] KRUMPIRKO 題解

XuYueming發表於2024-08-26

前言

題目連結:洛谷

題意簡述

給出長度為 \(n\) 的序列 \(c\)\(a\),選出一個大小為 \(l\) 下標集合 \(\mathcal{T}\),使 \(i \in \mathcal{T}\)\(c_i\) 之和比上 \(a_i\) 之和,乘上 \(i \not \in \mathcal{T}\)\(c_i\) 之和比上 \(a_i\) 之和最小。形式化地說,求:

\[\large \min _ {\mathcal{T} \subseteq \{ i \} _ {i = 1} ^ {n} \land |\mathcal{T}| = l} \cfrac{\sum \limits _ {i \in \mathcal{T}} c_i}{\sum \limits _ {i \in \mathcal{T}} a_i} \cfrac{\sum \limits _ {i \not \in \mathcal{T}} c_i}{\sum \limits _ {i \not \in \mathcal{T}} a_i} \]

\(1 \leq l \lt n \leq 100\)\(1 \leq a_i \leq 100\)\(1 \leq c_i \leq 10^6\)\(\sum a_i \leq 500\)

題目分析

發現要用揹包,具體地,我們 DP 當前選到第幾個,\(\mathcal{T}\) 中已經有了幾個元素的某些資訊。如果直接記錄 \(\cfrac{\sum c_i}{\sum a_i}\) 並不正確,也不能轉移。發現 \(a_i\) 之和很小,題目再暗示我們可以列舉它,把它記錄到狀態裡。那麼我們 DP 揹包的價值就是有關 \(\sum c_i\) 的資訊。

應該找一找性質,推一推式子。不妨記 \(A = \sum \limits _ {i = 1} ^ n a _ i\)\(C = \sum \limits _ {i = 1} ^ n c _ i\),選出的 \(c_i\)\(a_i\) 之和分別為 \(C'\)\(A'\)。我們答案就是 \(\min \cfrac{C'(C - C')}{A'(A - A')}\)。對於一個列舉了的 \(A'\),分母顯然是定值,我們求的就是分子 \(C'(C - C')\) 的最小值。發現,這是關於 \(C'\) 的二次函式,我們希望求 \(\min f(C') = -C'^2 + CC'\)。這個函式開口朝下,沒有最小值,取到最小值當然就在最小的 \(C'\) 或者最大的 \(C'\) 時取到。這樣我們 DP 記的值就是選出的 \(C'\) 的最大 / 最小值。為了方便,我們可以欽定我們求的這部分 \(C' < C - C'\),即欽定 \(l < n - l\),這樣只用記錄最小值。

總結一下,我們可以記 \(f_{i, j, k}\) 表示考慮前 \(i\) 個,選出了 \(j\) 個,\(A' = k\)\(C'\) 的最小值。有兩種轉移:

  1. 不放到 \(\mathcal{T}\) 中。
    易得 \(f_{i, j, k} = \min \{ f_{i, j, k}, f_{i - 1, j, k} \}\)
  2. 放到 \(\mathcal{T}\) 中。
    易得 \(f_{i, j, k} = \min\{ f_{i, j, k}, f{i - 1, j - 1, k - a_i} \}\)

邊界顯然是 \(f_{0, 0, 0} = 0\),其他 \(f_{i, j, k} = \infty\)

答案就是 \(\min \cfrac{f_{n, l, A'}(C - f_{n, l, A'})}{A'(A - A')}\)

時空複雜度:\(\Theta(nlA)\)。滾一滾,空間最佳化到 \(\Theta(nA)\)

程式碼

不小心跑到最優解

#include <cstdio>
#include <cstring>

const int N = 105;

inline int min(int a, int b) { return a < b ? a : b; }
inline int max(int a, int b) { return a > b ? a : b; }

int n, m, A[N], C[N];
int dp[N][505];

long long son = 1, mon = 0;
inline void check(long long a, long long b) {
    if (a * mon < b * son) son = a, mon = b;
}

int sum1, sum2;

signed main() {
    scanf("%d%d", &n, &m), m = min(m, n - m);
    for (int i = 1; i <= n; ++i) scanf("%d", &A[i]), sum1 += A[i];
    for (int i = 1; i <= n; ++i) scanf("%d", &C[i]), sum2 += C[i];
    memset(dp, 0x3f, sizeof dp);
    dp[0][0] = 0;
    for (int i = 1, s = 0; i <= n; ++i) {
        s += A[i];
        for (int t = min(m, i), ted = max(1, m - (n - i)); t >= ted; --t)
        for (int j = A[i]; j <= s; ++j)
            dp[t][j] = min(dp[t][j], dp[t - 1][j - A[i]] + C[i]);
    }
    for (int k = 1; k <= sum1 - 1; ++k) {
        if (dp[m][k] == 0x3f3f3f3f) continue;
        check(1ll * dp[m][k] * (sum2 - dp[m][k]), 1ll * k * (sum1 - k));
    }
    printf("%.3lf", 1. * son / mon);
    return 0;
}

後記 & 反思

一道很水的揹包題,從設計狀態到思考記錄什麼的過程,再到最後的轉移方程都十分自然。但是考場上不知道為什麼沒去推只用記錄 \(C'\) 的最值,而是用了 bitset 瞎搞。