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