Editorial for 人力分配 (APCS 2020-10 初級)


簡潔題意

有 \(n\) 個員工要分給兩個工廠,工廠一分到 \(X_1\) 人、工廠二分到 \(X_2\) 人,每個員工都得分到其中一個工廠(\(X_1 + X_2 = n\)、\(X_1, X_2 \ge 0\),其中一邊分到 \(0\) 人也可以)。兩個工廠的收益分別是 \(Y_1 = A_1 X_1^2 + B_1 X_1 + C_1\) 與 \(Y_2 = A_2 X_2^2 + B_2 X_2 + C_2\),請求出 \(Y_1 + Y_2\) 的最大值(可能是負數)(\(1 \le n \le 100\),六個係數都介於 \(-1000\) 與 \(1000\) 之間)

依正確通過的測試資料筆數給分,其中:

  • 子題組 1(\(50\) 分):\(n = 2\)。
  • 子題組 2(\(50\) 分):無額外限制。

兩個工廠,其實只有一個選擇

題目有 \(X_1\) 和 \(X_2\) 兩個未知數,看起來要兩邊都試。但它們被一句話綁死了:\(X_1 + X_2 = n\)。只要決定了 \(X_1\),\(X_2\) 就只能是 \(n - X_1\),沒有第二種可能。所以真正要做的選擇只有一個——工廠一要分到幾個人。

\(X_1\) 能是 \(0, 1, 2, \ldots, n\),總共 \(n + 1\) 種。題目說 \(n \le 100\),也就是最多 \(101\) 種分法,全部算一遍再挑最大的就好,一個迴圈就夠了。\(101\) 次計算對電腦來說是一瞬間的事(估算方法見 4.9)。

我們是在寫程式,不是在算數學

\(Y_1\)、\(Y_2\) 都是二次式,學過二次函數的人可能會想直接算頂點。但那條路要手算半天,細節還特別多:\(A\) 是負的時候開口向下、最大值才在中間,\(A\) 是正的時候開口向上、最大值反而落在兩個端點;而且 \(X_1\) 只能是整數,頂點算出來是小數,還得往兩邊取整再試一次。少想一種情況,就是一個錯。

讓電腦多花一點時間,把所有情形暴力跑過一遍,這些全都不用管——換來的是很快就能寫出一支正確的程式。

再來是「挑最大的」。這就是 3.9 的打擂台:先讓一個變數 ans 當擂主,每算出一種分法的收益就跟它比一次,比較大就換人當擂主。

擂主的初值要小心:收益可以是負數,所以不能設成 \(0\)。最保險的是直接用 int 的最小值 INT_MIN2.3,開頭要 #include <climits>),任何收益都一定比它大。

寫法一:只用到第四單元為止的語法

如果你語法書才讀到第四單元——變數、if、迴圈都會了,陣列和函式還沒學——這題已經寫得出來了。六個係數就老老實實開六個變數:

#include <climits>
#include <iostream>
using namespace std;

int main() {
    int A1, B1, C1;
    int A2, B2, C2;
    cin >> A1 >> B1 >> C1;
    cin >> A2 >> B2 >> C2;

    int n;
    cin >> n;

    int ans = INT_MIN;                  // 擂台上還沒有人
    for (int x1 = 0; x1 <= n; x1++) {
        int x2 = n - x1;                // 剩下的人全去工廠二
        int sum = (A1 * x1 * x1 + B1 * x1 + C1) + (A2 * x2 * x2 + B2 * x2 + C2);
        if (sum > ans) ans = sum;       // 比擂主大就換人
    }

    cout << ans << '\n';
    return 0;
}

三個地方值得停下來看一眼:

  • x1 要從 \(0\) 開始,x1 <= n 的等號也不能少。 「其中一個工廠可以分配到 \(0\) 個員工」是題目明講的,\(X_1 = 0\) 和 \(X_1 = n\) 這兩個端點都是合法分法,而且很常就是答案。
  • x2 不用另外讀、也不用另外試,寫成 n - x1 就好——這就是前面說的「只有一個選擇」。
  • if (sum > ans) ans = sum; 就是擂台換人。 這種「條件和一個短語句寫在同一行、又沒有 else」的情況,3.5 說可以不加大括號。

int 夠嗎?這題的收益可以是負的,所以上界和下界都要估一次。 \(X\) 最多 \(100\)、係數的絕對值最多 \(1000\),所以一個工廠的收益頂多是

\[1000 \times 100^2 + 1000 \times 100 + 1000 = 10101000\]

係數全取 \(-1000\) 時就是同一個數字的負版 \(-10101000\)。兩個工廠加起來,答案落在 \(-20202000\) 到 \(20202000\) 之間。int 裝得下 \(-2147483648\) 到 \(2147483647\)(估算方法見 2.9),兩邊都離二十一億還很遠——用 int 很安全。這順便也證明了前面把擂主初值設成 INT_MIN 沒問題:任何一種分法的收益都遠大於它。不放心的話全部改成 long long 也對,答案不會有任何差別。

寫法二:讀完上冊之後

寫法一有兩件事在重複:六個係數的變數名字只差一個數字,兩個工廠的收益公式長得一模一樣。上冊後半的陣列和函式,處理的正是這種重複。

#include <algorithm>
#include <climits>
#include <iostream>
using namespace std;

int A[3], B[3], C[3];                   // 一號、二號工廠的三個係數

int f(int x, int idx) { return x * x * A[idx] + x * B[idx] + C[idx]; }

int main() {
    for (int i = 1; i <= 2; i++) cin >> A[i] >> B[i] >> C[i];

    int n;
    cin >> n;

    int ans = INT_MIN;
    for (int x1 = 0; x1 <= n; x1++) {
        int x2 = n - x1;
        ans = max(ans, f(x1, 1) + f(x2, 2));
    }

    cout << ans << '\n';
    return 0;
}

跟寫法一比,換掉的是這四個地方:

  • A1A2 收成一個陣列 A6.1),工廠編號直接拿來當索引,讀入也就縮成一個迴圈。題目講的是「工廠一、工廠二」,讓 A[1]A[2] 對應一號、二號工廠最好讀,這就是 6.3 的 1-base。要注意陣列得開 \(3\) 格A[3] 的合法索引是 \(0\)、\(1\)、\(2\)),只開 \(2\) 格卻去用 A[2] 會踩到 6.8 的未定義行為。
  • 收益公式收成一個函式 f7.1):傳進人數 x 和工廠編號 idx,回傳那個工廠的收益。本來要寫兩次的式子現在只寫一次,而且 f(x1, 1) + f(x2, 2) 一眼就看得出在算什麼。
  • ABC 宣告在 main 外面,這樣 f 才看得到它們(3.8 的全域變數)。
  • 兩行 if 換成 max7.6,住在 <algorithm>),做的是同一件事。

兩種寫法算出來的答案完全一樣,也都是滿分,不必急著換成寫法二。真正的差別要等題目變了才看得出來:如果哪天工廠變成三間、五間,寫法二只要把讀入的迴圈跑到 \(3\)、\(5\),寫法一就得再多開一組變數、再多抄一次公式。

測過再交:範例只有一個,而且太好過了

這題只給一個範例,\(n = 2\)、答案是正的 \(11\)、最好的分法還剛好在兩個端點上。下面這幾種情況範例完全沒測到,交出去之前自己跑一次:

輸入 為什麼要測 預期輸出
-1 0 0 / -1 0 0 / 2 答案是負數,而且最好的分法在正中間 -2
1 0 0 / 0 0 0 / 3 最好的分法是把人全部給工廠一(\(X_1 = n\) 這個端點) 9
0 0 0 / 1 0 0 / 3 最好的分法是工廠一分到 \(0\) 人(\(X_1 = 0\) 這個端點) 9
1000 1000 1000 / 1000 1000 1000 / 100 係數和 \(n\) 都拉到最大,看 int上界夠不夠 10102000
-1000 -1000 -1000 / -1000 -1000 -1000 / 100 全部拉到最負,看 int下界夠不夠——這一列的答案剛好就是這題可能出現的最小值 -5102000

(每一列的三行分別是 \(A_1\ B_1\ C_1\)、\(A_2\ B_2\ C_2\)、\(n\)。)

構造的思路很單純:想讓某一個端點單獨勝出,就讓一邊的收益隨人數一直長(1 0 0)、另一邊固定不動(0 0 0);想同時逼出負數答案和中間解,就讓兩邊都開口向下(-1 0 0)。最後兩列則是把六個係數同時頂到 \(1000\) 與 \(-1000\),剛好各驗 int 的一邊——上界那一列估算時算過,下界這一列的答案 \(-5102000\) 離 \(-2147483648\) 也還很遠。

這五種都親眼看過正確,這題就穩了。

常犯錯誤
  1. 只算「一半一半」那一種分法:以為平分最好,直接算 \(X_1 = n / 2\)——範例當場印出 5
  2. 兩個工廠各自取最好再相加:忘了兩邊人數必須加起來等於 \(n\),各自挑最賺的人數——範例當場印出 17
  3. ans 初值設成 \(0\):收益可以是負的,所有分法都虧錢時就會印出 0——範例過得了,自測表第一列會印 0(正確答案是 -2)。
  4. 迴圈寫成 x1 < n:漏掉「人全部給工廠一」這種分法——範例過得了,自測表第二列會印 4(正確答案是 9)。
  5. 迴圈從 x1 = 1 開始:漏掉「工廠一分到 \(0\) 人」這種分法——範例過得了,自測表第三列會印 4(正確答案是 9)。
  6. 工廠編號傳錯:寫法二把 f(x2, 2) 打成 f(x2, 1),兩邊都套到工廠一的係數——範例當場印出 12