跳到內容

20|串流統計

還沒完成C 或 C++ 皆可CPU 時間上限 2 秒記憶體預算 2048 bytes

相關主題: 控制記憶體的必備指令、資料型態與範圍

C 或 C++ 皆可|CPU 時間上限 2 秒|記憶體預算 2048 bytes(2 KB,heap + 全域/靜態資料 + stack 合計)

感測器一口氣送來 n 個整數,請算出這些數字的最小值、最大值、總和,以及平均值。

聽起來很簡單,但這一題的記憶體預算只有 2048 bytes,只夠放 500 個左右的 int,而 n 最多有 200000 個。 所以不能把數字全部存起來再算,要「邊讀邊算」:每讀進一個數字就立刻更新統計結果,然後就可以把這個數字丟掉。

這種處理方式叫做串流處理(streaming)。在記憶體很小的環境(例如微控制器),或資料大到放不進記憶體的時候,都要這樣寫。

  • 第一行是一個整數 n(1 ≤ n ≤ 200000),代表有幾個數字。
  • 第二行有 n 個整數 x,每個都滿足 -10^9 ≤ x ≤ 10^9,數字之間用一個空格隔開。

輸出兩行:

  1. 第一行是最小值、最大值、總和三個整數,依照這個順序,中間用一個空格隔開。
  2. 第二行是平均值(總和 ÷ n),保留到小數點後 2 位,也就是 C 的 printf("%.2f\n", ...) 印出來的樣子。

平均值請用 double 計算:先把總和轉成 double,再除以 n。

輸入:

5
4 -2 7 0 3

輸出:

-2 7 12
2.40

最小值是 -2,最大值是 7,總和是 4 + (-2) + 7 + 0 + 3 = 12,平均值是 12 ÷ 5 = 2.4,印成 2.40。

檢查時會把下面三項加起來,合計不能超過 2048 bytes:

項目包含什麼
heap 最高用量執行期間「同一時間還沒釋放」的 malloc / calloc / realloc / new 的最大總量。std::vector、std::string 等標準函式庫容器在內部配置的記憶體也算在這裡
全域 / 靜態資料你的程式裡的全域變數、static 變數,加上常數和字串常值(例如 printf 的格式字串 "%d\n"、常數表格)
stack 最深用量(約)執行過程中 stack 最深用到多少:區域變數、函式呼叫,還有 scanf / printf 這些函式內部用到的 stack。程式啟動和最基本的輸入輸出本身用掉的部分(約 5 KB)會自動扣掉,所以只用幾個變數的程式會顯示接近 0

不計算的部分:C 執行環境(ucrtbase.dll)內部的輸入輸出緩衝區,以及程式碼本身。所以正常使用 scanf / printf 或 std::cin / std::cout 不必擔心緩衝區。

檢查結果的最後會列出明細,超過預算時從這裡就能看出是哪一項太大。例如參考解答(C)的結果:

記憶體預算(最吃記憶體的是測試 01)
heap 最高用量 0 bytes
全域 / 靜態資料 21 bytes(變數 0,常數與字串 21)
stack 最深用量(約) 720 bytes
✔ 合計 741 bytes / 上限 2048 bytes

(stack 的 720 bytes 主要是 printf 印浮點數時內部用掉的。)

幾個常見的陷阱:

  • 把 200000 個 int 存起來要 800000 bytes,不管用全域陣列、malloc 還是 std::vector 都會超過預算。
  • 用 fgets 或 std::getline 把第二行整行讀進字串,也等於把資料全部存起來(一行最長約 2.4 MB)。
  • C++20 的 std::format 會把約 12.5 KB 的 Unicode 表格放進你的程式(算在「常數與字串」),這一題用不起,輸出請用 std::cout 或 printf。
  • 總和最大可能是 2 × 10^14,int 裝不下(最大約 2.1 × 10^9)。用 int 累加會溢位,第一階段的 Sanitizer 會抓到 signed integer overflow。

C:

  • 在迴圈裡用 scanf 一次讀一個數字,讀完立刻更新統計結果,不需要陣列
  • long long 和它的格式 %lld
  • (double)sum / n:先轉型再除;printf 的 %.2f 印兩位小數
  • 最小值、最大值的初始值:用第一個數字,或用 <limits.h> 的 INT_MAX / INT_MIN

C++:

  • 在迴圈裡用 std::cin >> x 一次讀一個數字,不需要 std::vector
  • long long、static_cast<double>(sum) / n
  • <iomanip> 的 std::fixed 和 std::setprecision(2)
  • std::min / std::max(<algorithm>),或 std::numeric_limits<int>::max()(<limits>)
提示 1:真的需要把數字存起來嗎?

想想看:要算「到目前為止的最小值」,你需要記得之前讀過的每一個數字,還是只需要記得「目前最小的那一個」?

讀進一個新數字 x 時,只要拿它跟目前的最小值比一下,就知道最小值要不要換掉。最大值、總和也一樣,各用一個變數就夠了。讀完 n 個數字時,答案也就算好了。

提示 2:最小值和最大值一開始要設成多少?

不能從 0 開始:如果數字全部是正的,最小值會錯誤地停在 0;全部是負的,最大值會停在 0。

兩種正確的做法:

  • 先讀第一個數字,讓它同時當最小值和最大值的初始值(題目保證 n ≥ 1),再用迴圈讀剩下的 n - 1 個。
  • 最小值從「最大的 int」開始、最大值從「最小的 int」開始:C 用 INT_MAX / INT_MIN,C++ 用 std::numeric_limits<int>::max() / min()。
提示 3:總和與平均值
  • 總和用 long long。C 的 scanf / printf 對應的格式是 %lld。
  • 平均值寫成 (double)sum / n。注意 (double)(sum / n) 是錯的:括號裡已經用整數除法算完、小數被丟掉了,才轉成 double。
  • 印出兩位小數:C 用 printf("%.2f\n", avg);C++ 用 std::cout << std::fixed << std::setprecision(2) << avg。

整個程式的骨架大概是:讀 n →(讀一個數字、更新最小值 / 最大值 / 總和)重複 n 次 → 印出結果。