Editorial for 特技表演 (APCS 2024-06 初級)
簡潔題意
給 \(n\) 棟大樓的樓高 \(h_1, h_2, \ldots, h_n\),找出一段連續的大樓,滿足樓高一路嚴格遞減(\(h_i > h_{i+1} > \cdots > h_j\)),並讓這一段的棟數最多;輸出這個最多的棟數(\(5 \le n \le 100\)、\(1 \le h_i \le 1000\))
依正確通過的測試資料筆數給分,其中:
- 子題組 1(\(60\) 分):\(n = 5\)。
- 子題組 2(\(40\) 分):無限制。
「連續」兩個字是這題的關鍵:路徑上的大樓必須是緊鄰的,不能跳過中間那幾棟。
先拿下子題組 1(60 分):只用 if 和 else
子題組 1 保證 \(n = 5\),也就是只有五個樓高,把它們叫做 \(a\)、\(b\)、\(c\)、\(d\)、\(e\)。
五棟大樓之間只有四個相鄰關係要問:\(a > b\)、\(b > c\)、\(c > d\)、\(d > e\)。而一段長度 \(L\) 的滑翔路徑,正好就是 \(L - 1\) 個連續成立的相鄰關係。所以:
答案 = 最長一串連續成立的相鄰關係個數 \(+\ 1\)。
拿範例 1(5 / 6 2 5 3 1)試一次:\(6 > 2\) 成立、\(2 > 5\) 不成立、\(5 > 3\) 成立、\(3 > 1\) 成立。最長的一串是後面連續兩個,答案 \(2 + 1 = 3\),正確。
接著把所有可能一個一個問過去。從最長的開始問,先成立的先印,就不會弄錯:
| 問題 | 有幾種擺法 | 答案 |
|---|---|---|
| 四個關係全部成立 | \(1\) 種 | \(5\) |
| 連續三個成立 | \(2\) 種 | \(4\) |
| 連續兩個成立 | \(3\) 種 | \(3\) |
| 至少一個成立 | — | \(2\) |
| 一個都不成立 | — | \(1\) |
寫成程式就是一路排下來的 else if(3.5),連迴圈和陣列都不用:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a, b, c, d, e;
cin >> a >> b >> c >> d >> e; // 子題組 1 保證 n = 5
if (a > b && b > c && c > d && d > e) { // 五棟一路遞減
cout << 5 << endl;
} else if ((a > b && b > c && c > d) || (b > c && c > d && d > e)) { // 有連續三個
cout << 4 << endl;
} else if ((a > b && b > c) || (b > c && c > d) || (c > d && d > e)) { // 有連續兩個
cout << 3 << endl;
} else if (a > b || b > c || c > d || d > e) { // 至少有一個
cout << 2 << endl;
} else { // 一次都沒有下降
cout << 1 << endl;
}
return 0;
}
&& 本來就比 || 先算(3.4),所以那些小括號不加也是同樣的意思——但加上去除了好讀,也省得編譯器一直提醒你(實測不加會冒出 suggest parentheses around ‘&&’ within ‘||’ 的警告)。
n 讀進來以後沒有派上用場(子題組 1 保證它就是 \(5\)),但還是得讀——不把它讀走,後面的樓高就會整個錯開一格。
範例 2 的 \(n = 10\),這一版只看得到前五棟,會答錯——這是正常的,逐筆給分,這一版穩穩拿下子題組 1 的 \(60\) 分。想拿另外 \(40\) 分,繼續往下看。
從 60 分到 100 分:一個變數記住「已經連了幾棟」
\(n\) 最大到 \(100\),不可能再一個一個變數寫下去,要改用迴圈(4.3)從左往右走過每一棟。走的時候只要記三件事:
last_h:前一棟的樓高,用來跟現在這一棟比。num:目前這一段連續遞減已經連了幾棟。ans:到目前為止看過最長的一段。
每讀進一棟就問一次「有沒有比前一棟低」:
- 有:這一段可以再延長一棟,
num加 \(1\)。 - 沒有(一樣高或更高都算沒有):這一段到此為止,從這一棟重新開始算,
num設成 \(1\)。
然後拿 num 去挑戰紀錄 ans(擂台法見 3.5)。走完就得到答案。
樓高不必存起來——每一棟只會被看一次,讀進來就當場比掉。
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int num = 0; // 目前這一段已經連了幾棟
int ans = 0; // 到目前為止看過最長的一段
int last_h = 0; // 前一棟的樓高
for (int i = 1; i <= n; i++) {
int h;
cin >> h;
if (h < last_h) {
num++; // 比前一棟低:這一段可以再延長一棟
} else {
num = 1; // 沒有比前一棟低:從這一棟重新開始算
}
if (num > ans) ans = num;
last_h = h;
}
cout << ans << endl;
return 0;
}
兩個容易被忽略的地方:
- 第一棟沒有「前一棟」,卻不用特別處理:
last_h的初值 \(0\) 比任何樓高都小(限制寫明 \(h_i \ge 1\)),所以第一棟一定走else,num從 \(1\) 開始算——這就是把哨兵放在值域外的好處。 - 答案最少是 \(1\):就算五棟一路往上蓋、一次都沒下降,單獨一棟本身也是一條長度 \(1\) 的路徑。上面的寫法因為每一步都會把
num設成至少 \(1\),這件事自動就成立了。
學過陣列的話:每一棟都往右試一次
如果已經學到陣列(6.1),也可以換個角度:先把樓高全部讀進 h[1] 到 h[n](從 \(1\) 開始放,跟題目的編號對齊,見 6.3),再讓每一棟都當一次起點往右試——只要下一棟比前一棟低就把長度記下來,一遇到沒有下降就 break(4.5)跳出內層迴圈,換下一個起點。
#include <algorithm>
#include <iostream>
using namespace std;
int main() {
const int MAX_N = 100;
int h[MAX_N + 1];
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> h[i];
int ans = 1;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (h[j] < h[j - 1]) {
ans = max(ans, j - i + 1); // 從第 i 棟滑到第 j 棟
} else {
break; // 斷掉了,這個起點到此為止
}
}
}
cout << ans << endl;
return 0;
}
- 陣列開
MAX_N + 1格,是因為要用到h[1]到h[100](6.3)。 ans直接從 \(1\) 開始,因為答案至少是 \(1\);max住在<algorithm>(7.6)。- 會不會太慢?外層最多 \(100\) 次、內層最多 \(100\) 次,合起來最多一萬次比較,離會超時還很遠(4.9)。
兩種寫法答案完全一樣,都是滿分,用哪一種都行。
測過再交:兩個範例都沒有「一樣高」的相鄰大樓
兩個範例合起來測到了「中間斷掉再重新開始」,但有幾種局面從來沒出現過:相鄰兩棟一樣高(題目要的是嚴格遞減,一樣高不算下降)、整排一路遞減、最長的一段就在最開頭。自己補這四筆:
| 輸入 | 預期輸出 | 這一列在測什麼 |
|---|---|---|
5 / 5 4 3 2 1 |
5 |
整排一路遞減,答案就是 \(n\) 本身 |
5 / 7 7 7 7 7 |
1 |
一樣高不算下降;一次都沒下降時答案是 \(1\) 不是 \(0\) |
5 / 9 5 1 2 8 |
3 |
最長的一段在最開頭(兩個範例的最長段都不在開頭) |
6 / 4 3 9 8 7 6 |
4 |
前面先出現一段短的,後面才是最長的——不能一遇到中斷就停 |
第 2 列最關鍵:把「一樣高」也當成下降的寫法,兩個範例都過得了,只有這一列會當場印出 5。
前三列的 \(n\) 都是 \(5\),子題組 1 的那一版也應該全部答對。四筆都親眼看過正確,這題就穩了。
常犯錯誤
- 把「一樣高」也當成下降(寫成
h <= last_h):題目要的是嚴格遞減。兩個範例都過得了,自測表第 2 列會印5(該印1)。 - 段斷掉時忘了重新從 \(1\) 開始算(少了
else那一段):num一路累加不歸零,範例 2 會印5(該印4)。 - 忘了更新
last_h:每一棟都跟同一個舊值比,範例 1 會印1。 - 最後印的是
num不是ans(只記得最後一段):範例 1 剛好對,範例 2 印1(該印4)。 - 把答案當成「下降了幾次」:棟數比下降次數多 \(1\)。範例 1 會印
2。 - (子題組 1 的版本)只檢查五棟是不是一路遞減,不是就印 \(1\):中間那些較短的段都沒算到,範例 1 會印
1。 - (陣列版)內層迴圈忘了
break:斷掉之後還繼續往右找,會把不連續的大樓也算進同一段,範例 1 會印5(該印3)。