商品比價 題解|APCS 2026 年 11 月中級 第 1 題
簡潔題意
\(m\) 家店、\(n\) 項商品。第 \(i\) 家店第 \(j\) 項商品的標價是 \(p_{i,j}\)(\(-1\) 表示沒賣),第 \(i\) 家店全店打 \(d_i\)% off,售價=標價 \(\times (100 - d_i) / 100\)。每項商品都到售價最便宜的店買,同價選編號最小的店;輸出每項商品到哪家店買,以及買齊的最低總價(\(1 \le m, n \le 50\);\(p_{i,j} = -1\),或是 \(0\)~\(50000\) 之間 \(100\) 的倍數;每項商品至少一家店有賣;\(0 \le d_i \le 100\))。
依正確通過的測試資料筆數給分,其中:
- 子題組 1(\(60\) 分):每家店都有賣所有商品(沒有 \(-1\))。
- 子題組 2(\(40\) 分):無額外限制。
先拿下子題組 1(60 分):售價用整數算,一欄一欄打擂台
先把整張表讀進來。折扣在最後一行,讀完所有標價才知道每家店打幾折,所以先把標價存進二維陣列 p(語法書 6.9):p[i][j] 是第 \(i\) 家店第 \(j\) 項商品的標價,店號和商品編號都從 \(1\) 開始用,陣列各多開一格(語法書 6.3)。輸入的一行是一家店(表格的一列),所以一項商品是表格的一欄。
售價用整數算。標價是 \(100\) 的倍數,p / 100 一定整除,售價寫成 p / 100 * (100 - d) 就是精確的整數。例如範例 1 店 1 的商品 2:\(200 / 100 \times 80 = 160\)。
不要用小數算,例如 p * (1 - d / 100.0)。\(0.45\)、\(0.67\) 這種小數,電腦只存得下一個很接近的值(\(0.45\) 存的是 \(0.45000000000000001\ldots\)),乘出來就會差一點點。下面兩家店的售價其實都是 \(990\),用這個式子算出來卻不一樣:
- 店 1:標價 \(1800\)、折扣 \(45\),算出
990.0000000000001 - 店 2:標價 \(3000\)、折扣 \(67\),算出
989.9999999999999
這是本站用 setprecision(16) 多印幾位看到的值;直接 cout 的話兩個都印 990,看不出差別。可是比大小時店 2 比店 1 小,< 就換成店 2——兩家其實同價,應該選編號小的店 1。題目給的數字都是整數,就全程用整數算,這種誤差根本不會出現。
一欄一欄打擂台。每一欄先讓第 \(1\) 家店上台(語法書 4.8 的第一種初值:拿第一個當初值),第 \(2\)~\(m\) 家店輪流挑戰,售價比較低才換:用 < 不用 <=,同價時留在台上的就是先上台、編號比較小的店。每一欄的最低售價加起來就是總價,最多 \(50 \times 50000 = 2.5 \times 10^6\),int 裝得下。
#include <bits/stdc++.h>
using namespace std;
const int MAX_M = 50;
const int MAX_N = 50;
int p[MAX_M + 1][MAX_N + 1]; // p[i][j]:第 i 家店第 j 項商品的標價(都從 1 用起)
int d[MAX_M + 1]; // d[i]:第 i 家店的折扣
int store[MAX_N + 1]; // store[j]:第 j 項商品到哪家店買
int main() {
int m, n;
cin >> m >> n;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
cin >> p[i][j];
for (int i = 1; i <= m; i++)
cin >> d[i];
int total = 0;
for (int j = 1; j <= n; j++) { // 一項商品=表格的一欄
int best = p[1][j] / 100 * (100 - d[1]); // 第 1 家店先上台
int who = 1;
for (int i = 2; i <= m; i++) { // 第 2~m 家店輪流挑戰
int sale = p[i][j] / 100 * (100 - d[i]); // 標價是 100 的倍數:售價一定是整數
if (sale < best) { // 比較便宜才換:同價時留下編號小的
best = sale;
who = i;
}
}
store[j] = who;
total += best;
}
for (int j = 1; j <= n; j++) {
if (j > 1) cout << ' '; // 數字之間一個空白
cout << store[j];
}
cout << '\n' << total << '\n';
return 0;
}
兩個範例都有 \(-1\),這份程式交上去兩個範例都會 WA——這是正常的,逐筆給分之下子題組 1 的 \(60\) 分穩穩到手;想自己驗,用自測表的第 1、2、5 列(都沒有 \(-1\))。
從 60 分到 100 分:跳過沒賣的店、換掉擂台的初值
有店沒賣時,子題版有兩個地方會出事:
- 沒賣的店變成免費:\(-1\) 照樣代進去,
-1 / 100是 \(0\)(整數除法向零取整,語法書 2.6),售價就是 \(0\) 元,比誰都便宜。所以先看標價,是 \(-1\) 就continue,這家店不參加。 - 第 1 家店不一定能先上台:它可能沒賣這項商品(範例 1、範例 2 的商品 3 都是)。改用語法書 4.8 的第二種初值:先放一個比所有售價都大的數。售價最多是 \(50000\)(標價 \(50000\)、折扣 \(0\)),初值設 \(50001\),第一家有賣的店一定換得上去;
who先設 \(0\),表示還沒有店上台。
初值不能剛好設 \(50000\):某項商品最便宜如果就是 \(50000\),50000 < 50000 不成立,沒有店上得了台,who 停在 \(0\)。
拿範例 1 的商品 3 走一次:店 1 沒賣,跳過;店 2 售價 \(140 < 50001\),上台;店 3 售價 \(150\) 沒有比 \(140\) 小,不換。答案是店 2、\(140\) 元。
#include <bits/stdc++.h>
using namespace std;
const int MAX_M = 50;
const int MAX_N = 50;
int p[MAX_M + 1][MAX_N + 1]; // p[i][j]:第 i 家店第 j 項商品的標價(都從 1 用起)
int d[MAX_M + 1]; // d[i]:第 i 家店的折扣
int store[MAX_N + 1]; // store[j]:第 j 項商品到哪家店買
int main() {
int m, n;
cin >> m >> n;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
cin >> p[i][j];
for (int i = 1; i <= m; i++)
cin >> d[i];
int total = 0;
for (int j = 1; j <= n; j++) { // 一項商品=表格的一欄
int best = 50001; // 比任何售價都大:售價最多 50000
int who = 0; // 0:還沒有店上台
for (int i = 1; i <= m; i++) { // 第 1 家店也要挑戰
if (p[i][j] == -1) continue; // 這家店沒賣
int sale = p[i][j] / 100 * (100 - d[i]); // 標價是 100 的倍數:售價一定是整數
if (sale < best) { // 比較便宜才換:同價時留下編號小的
best = sale;
who = i;
}
}
store[j] = who;
total += best;
}
for (int j = 1; j <= n; j++) {
if (j > 1) cout << ' '; // 數字之間一個空白
cout << store[j];
}
cout << '\n' << total << '\n';
return 0;
}
表格最多 \(50 \times 50 = 2500\) 格,每一格只看一次,時間完全不是問題。
測過再交:範例的同價剛好算得準,也沒有 0 元和 50000 元
兩個範例已經考到沒賣的店(第 1 家店也有沒賣的商品)、同價選編號小的店;但範例 2 的同價用小數算也剛好相等,而且沒有 \(0\) 元、\(50000\) 元、折扣 \(100\) 的店。用小數算、初值設 \(50000\)、把 \(0\) 當記號、把沒賣當成很貴,這幾種寫法兩個範例都會過,下面幾列專門抓它們:
| 輸入 | 正確輸出 | 這一筆在測什麼 |
|---|---|---|
2 1 / 300 / 800 / 44 79 |
1 / 168 |
兩家店都是 \(168\) 元:用上面的小數算法,店 2 會比店 1 小一點點 |
3 1 / 0 / 40000 / 500 / 0 100 0 |
1 / 0 |
\(0\) 元:店 1 標價 \(0\)、店 2 折扣 \(100\),兩家都免費,選店 1 |
2 1 / -1 / 50000 / 0 0 |
2 / 50000 |
最便宜剛好 \(50000\)(售價的最大值),而且只有店 2 有賣 |
2 1 / -1 / 500 / 100 0 |
2 / 500 |
沒賣的店 1 折扣是 \(100\):它不能被當成 \(0\) 元 |
| 用程式造:\(m = n = 50\),標價全是 \(50000\)、折扣全是 \(0\) | \(50\) 個 1 / 2500000 |
總價最大;每項商品 \(50\) 家同價,都選店 1 |
五筆都親眼看過正確,這題就穩了。
常犯錯誤
- 照題目的式子寫成
p * ((100 - d) / 100):括號裡先做整數除法,有打折的店售價都變成 \(0\)——範例 1 印1 1 2 1/0。 - 用小數算售價(
p * (1 - d / 100.0)):兩個範例都過,自測表第 1 列印2;總價也用double加起來直接cout,第 5 列還會印成2.5e+06。 - 同價用
<=:同價就換成後面的店——範例 2 印2 2 2。 - 只補了跳過 \(-1\),初值還是拿第 1 家店:範例 1 印
2 1 1 3/430,商品 3 停在沒賣的店 1。 - 沒賣當成「很貴」(標價改成 \(10^9\) 再打折):兩個範例都過;折扣 \(100\) 的店不管標價多少都是 \(0\) 元,自測表第 4 列印
1/0。 - 初值設成 \(50000\):兩個範例都過,自測表第 3 列印
0。 - 把 \(0\) 當成記號:用
best == 0代表「還沒找到」,\(0\) 元的店會被下一家換掉,自測表第 2 列印3/500;用p <= 0判斷沒賣,標價 \(0\) 的店被跳過,同一列印2/0。