20|串流統計
相關主題: 控制記憶體的必備指令、資料型態與範圍
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,數字之間用一個空格隔開。
輸出兩行:
- 第一行是最小值、最大值、總和三個整數,依照這個順序,中間用一個空格隔開。
- 第二行是平均值(總和 ÷ n),保留到小數點後 2 位,也就是 C 的
printf("%.2f\n", ...)印出來的樣子。
平均值請用 double 計算:先把總和轉成 double,再除以 n。
輸入:
54 -2 7 0 3輸出:
-2 7 122.40最小值是 -2,最大值是 7,總和是 4 + (-2) + 7 + 0 + 3 = 12,平均值是 12 ÷ 5 = 2.4,印成 2.40。
記憶體預算怎麼算
Section titled “記憶體預算怎麼算”檢查時會把下面三項加起來,合計不能超過 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 次 → 印出結果。