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 if3.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\)),所以第一棟一定走 elsenum 從 \(1\) 開始算——這就是把哨兵放在值域外的好處。
  • 答案最少是 \(1\):就算五棟一路往上蓋、一次都沒下降,單獨一棟本身也是一條長度 \(1\) 的路徑。上面的寫法因為每一步都會把 num 設成至少 \(1\),這件事自動就成立了。

學過陣列的話:每一棟都往右試一次

如果已經學到陣列(6.1),也可以換個角度:先把樓高全部讀進 h[1]h[n](從 \(1\) 開始放,跟題目的編號對齊,見 6.3),再讓每一棟都當一次起點往右試——只要下一棟比前一棟低就把長度記下來,一遇到沒有下降就 break4.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 的那一版也應該全部答對。四筆都親眼看過正確,這題就穩了。

常犯錯誤
  1. 把「一樣高」也當成下降(寫成 h <= last_h):題目要的是嚴格遞減。兩個範例都過得了,自測表第 2 列會印 5(該印 1)。
  2. 段斷掉時忘了重新從 \(1\) 開始算(少了 else 那一段):num 一路累加不歸零,範例 2 會印 5(該印 4)。
  3. 忘了更新 last_h:每一棟都跟同一個舊值比,範例 1 會印 1
  4. 最後印的是 num 不是 ans(只記得最後一段):範例 1 剛好對,範例 2 印 1(該印 4)。
  5. 把答案當成「下降了幾次」:棟數比下降次數多 \(1\)。範例 1 會印 2
  6. (子題組 1 的版本)只檢查五棟是不是一路遞減,不是就印 \(1\):中間那些較短的段都沒算到,範例 1 會印 1
  7. (陣列版)內層迴圈忘了 break:斷掉之後還繼續往右找,會把不連續的大樓也算進同一段,範例 1 會印 5(該印 3)。