Editorial for 程式交易 (APCS 2022-01 初級)


簡潔題意

一支股票在 \(n\) 個時間點的價格是 \(a[1], a[2], \ldots, a[n]\),交易策略是固定的:時間點 \(1\) 一定用 \(a[1]\) 買進;手上有股票時,遇到「現價 \(-\) 買價 \(\ge D\)」就賣出、賺進這個差額;手上沒股票時,遇到「現價 \(\le\) 上一次賣價 \(-\ D\)」就買進。求總利潤——交易結束時如果還持有股票,那張直接無視、不必扣它的成本(\(1 \le n, D \le 100\)、\(1 \le a[i] \le 100\))

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

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

這題不是要你找「最賺的買賣時機」,而是照題目給的規則走一遍、看最後賺了多少。看到「股票」兩個字先別緊張,規則叫你賣你就賣。

先拿下子題組 1(50 分):只用 if 和 else

子題組 1 保證 \(n = 3\),也就是全部只有 \(a[1]\)、\(a[2]\)、\(a[3]\) 三個價格。時間點 \(1\) 已經用 \(a[1]\) 買進了,剩下兩個時間點一個一個想過去就好:

  • 時間點 \(2\):手上有股票。如果 \(a[2] - a[1] \ge D\) 就賣掉,賺 \(a[2] - a[1]\)。
  • 時間點 \(3\):分兩種情況——
    • 剛剛在時間點 \(2\) 賣掉了 → 現在空手,這一步最多只能「買進」。買進不會賺錢,而且買完立刻結束、那張會被無視,所以對答案完全沒有影響
    • 剛剛沒賣掉 → 手上還是 \(a[1]\) 買的那張。如果 \(a[3] - a[1] \ge D\) 就賣掉,賺 \(a[3] - a[1]\)。

所以 \(n = 3\) 的答案只有三種可能:

情況 答案
\(a[2] - a[1] \ge D\) \(a[2] - a[1]\)
否則,\(a[3] - a[1] \ge D\) \(a[3] - a[1]\)
否則 \(0\)

表格裡的「否則」寫成程式就是 else ifelse3.5)。三種情況一路排下來,一支 if / else if / else 就寫完了,連迴圈都不用。

#include <iostream>
using namespace std;

int main() {
    int n, D;
    cin >> n >> D;

    int a1, a2, a3;
    cin >> a1 >> a2 >> a3;  // 子題組 1 保證 n = 3,就是三個價格

    if (a2 - a1 >= D) {          // 時間點 2 就賣得掉
        cout << a2 - a1 << endl;
    } else if (a3 - a1 >= D) {   // 撐到時間點 3 才賣得掉
        cout << a3 - a1 << endl;
    } else {                     // 從頭到尾都賣不掉
        cout << 0 << endl;
    }

    return 0;
}

n 讀進來以後沒有派上用場(子題組 1 保證它就是 \(3\)),但還是得讀——不把它讀走,後面的價格就會整個錯開一格。

拿範例 1(3 10 / 50 20 45)驗一下:\(20 - 50 = -30\) 沒到 \(10\),\(45 - 50 = -5\) 也沒到,印 \(0\),正確。

範例 2 的 \(n = 6\),這一版只看得到前三個價格,會答錯——這是正常的,逐筆給分,這一版穩穩拿下子題組 1 的 \(50\) 分。想拿另外 \(50\) 分,繼續往下看。

從 50 分到 100 分:一個變數就記得住「上一次成交價」

\(n\) 最大到 \(100\),不可能再一個一個變數寫下去,得改用迴圈(4.3)一路掃過每個時間點。掃的過程中要記住兩件事:

  1. 現在手上有沒有股票——用 bool3.1)記,一開始是 true,因為時間點 \(1\) 已經買進了。
  2. 上一次成交的價格是多少

第 2 點是這題最值得學的地方。乍看要記兩個價格:持有時比的是「買價」,空手時比的是「上一次的賣價」。但看清楚這兩個門檻在跟誰比——持有時的買價,就是最近一次成交的價格;空手時的上次賣價,也是最近一次成交的價格。兩件事其實是同一件事,一個變數 last_price 就記得住,賣出和買進的時候各自把它換成當下的價格即可。

於是每個時間點只要做一次判斷(&&!3.3):

  • 手上有股票,而且 price - last_price >= D賣出:利潤加上 price - last_price,改成空手,last_price 換成現價。
  • 手上沒股票,而且 price <= last_price - D買進:改成持有,last_price 換成現價。
  • 兩個都不成立就什麼事都不做。

還有一個小地方:價格不必存進陣列。每個價格只會被看一次,讀進來就當場處理掉。

#include <iostream>
using namespace std;

int main() {
    int n, D;
    cin >> n >> D;

    int last_price;      // 上一次成交(買進或賣出)的價格
    cin >> last_price;   // 時間點 1 一定買進,成交價就是 a[1]
    bool holding = true; // 現在手上有沒有股票
    int profit = 0;      // 累積利潤

    for (int i = 2; i <= n; i++) {
        int price;
        cin >> price;
        if (holding && price - last_price >= D) {          // 有股票:漲夠了就賣
            profit += price - last_price;
            holding = false;
            last_price = price;
        } else if (!holding && price <= last_price - D) {  // 沒股票:跌夠了就買
            holding = true;
            last_price = price;
        }
    }

    cout << profit << endl;
    return 0;
}

幾個容易被忽略的細節:

  • 迴圈從 \(i = 2\) 開始,因為時間點 \(1\) 的買進已經在迴圈外面做掉了。
  • 利潤用 += 累加(2.7)。單次賣出最多賺 \(99\)(價格介於 \(1\) 到 \(100\)),而買、賣是輪流發生的,\(n \le 100\) 個時間點裡最多賣出 \(50\) 次,所以答案不會超過 \(99 \times 50 = 4950\),int 綽綽有餘。
  • \(n = 1\) 時迴圈一次都不跑,直接印 \(0\):買了就結束,那張被無視。這是題目容許的輸入,別讓程式在這裡出錯。
  • 結束時如果還持有股票,程式什麼都不用做——題目說直接無視那張,profit 當下就是答案。

測過再交:範例沒測到「買回來」的那個門檻

兩個範例合起來測到了「從頭到尾賣不掉」與「賣出後又買回來再賣一次」,但買進條件恰好卡在等號(現價剛好等於上次賣價減 \(D\))從來沒出現過,而那正是最容易寫錯的地方;\(n = 1\) 這種極端輸入也沒有範例。自己補這四筆:

輸入 預期輸出 這一列在測什麼
1 5 / 100 0 \(n = 1\):迴圈一次都不跑,買了就結束
4 10 / 50 60 50 70 30 買進門檻恰好等號(\(50 = 60 - 10\)),而且買回來之後真的又賣了一次
5 1 / 1 2 3 4 5 1 賣掉之後價格一路漲,再也買不回來,全程只賺一次
5 1 / 1 100 1 100 1 198 一路來回買賣的極端情況,最後一筆買進要被無視

第 2 列最關鍵:漏掉買進條件的等號、或是賣出後忘了更新 last_price,這兩種寫法都會在這一列印出 10,而範例通通抓不到它們。

四筆都親眼看過正確,這題就穩了。

常犯錯誤
  1. 忘了時間點 \(1\) 就已經買進:把開頭當成空手,範例 1 會印 25(該印 0)。
  2. 結束時把手上的股票硬賣掉:題目說直接無視,不是拿最後的價格結算。範例 1 會印 -5
  3. 賣出條件漏掉等號(寫成 price - last_price > D):範例 2 會印 15(該印 25)。
  4. 買進條件漏掉等號,或是賣出後忘了更新 last_price:兩種都會害程式「該買回來的時候沒買回來」,範例全部過得了,自測表第 2 列會印 10(該印 30)。
  5. 買進之後忘了把 last_price 換成買價:之後的賣出會拿錯誤的價格當成本,範例 2 會印 15
  6. 買進也去累加利潤:買進只是把錢換成股票,沒有賺。範例 2 會印 60
  7. 以為要挑最賺的那一次賣:規則是遇到第一個滿足條件的價格就得賣掉。3 1 / 1 2 3 的答案是 1(在價格 \(2\) 就賣掉了),不是 2