2023 大社賽
非官方題解 by 晴 | 2023 建北電資
Rank
1
3
2
@fatman (巧虎)
\(400\) Scores
\(17\) Submissions
@yitien (信田)
\(200\) Scores
\(7\) Submissions
@fishball (魚丸)
\(150\) Scores
\(3\) Submissions

pA. 1013 hPa

簡化題序
海平面的大氣壓力為 \(x\) hPa
每上升 \(1\) 千公尺大氣壓力會降低 \(y\) hPa
海拔為 \(z\) 千公尺時,大氣壓力為多少 hPa?
解題思路
\(x - y \times z\)
還能有什麼思路
這就是俗稱的「簽到題」,有來寫的話大部分人都可以 AC 的
AC Code
#include <iostream>
int main() {
// 起始值,削減速度,為期
int initialValue, reducingSpeed, duration;
std::cin >> initialValue >> reducingSpeed >> duration;
std::cout << initialValue - reducingSpeed * duration << "\n";
}
pB. DS

簡化題序
給你 \(n\) 個字串,請依下列規則產生一個神奇字串:
-
假如第 \(k\) 個字串的第一個字元為
'a'
則取此字串的第 \(1\) 個字元 -
假如第 \(k\) 個字串的第一個字元為
'b'
則取此字串的第 \(2\) 個字元 -
'c'則是 \(3\);'d'則是 \(4\),以此類推 -
若字串的長度不足,則取空格
輸入 / 輸出 格式
第一行有一個數字 \(n\)
第二行會有 \(n\) 個字串
輸出依此規則得到的那個字串
解題思路
大家知道字元在電腦中其實是存 ASCII 碼,而且 a ~ z 是依序排列的
所以可以直接用 x - 'a' 來獲取 x 是第幾個英文字母
(Zero-based Numbering 的好處就出現了!)
| 字元 | 'a' | 'b' | 'c | 'd' | 'e' | 'f' | 'g' |
| 號碼 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
與 'a' 的差距 |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
AC Code
#include <iostream>
int main() {
// 會有幾個字串
int numStrings;
std::cin >> numStrings;
// 用來佔存的字串
std::string str;
for (int i = 0; i < numStrings; ++i) {
std::cin >> str;
// str 第 0 個字元與 'a' 的距離 = 要輸出第幾個字
int index = str[0] - 'a';
// 如果在字串長度範圍內
if (index < str.length()) {
// 直接輸出那個字元即可
std::cout << str[index];
}
else {
// 超過長度,輸出空格
std::cout << ' ';
}
}
}
pC. 我的 PC 爆了

簡化題序
給你 \(n\) 個不同面額的金幣,如 \([\ 1,\ 3,\ -2,\ 5,\ -3\ ]\)
你可以自由切割成幾個小區塊,如 \([\ 1,\ 3\ ]\ [\ -2,\ 5 \ ]\ [\ -3\ ]\)
試求
它們乘上他們由左至右順序,如 \(1\) \(\cdot [1 + 3] +\) \(2\) \(\cdot [(-2) + 5] +\) \(3\) \(\cdot [-3] = -1\)
的最大值
※ 例題的結果最大值為切割成 \([\ 1\ ]\ [\ 3,\ -2 \ ]\ [\ 5,\ -3\ ]\),是 \(9\)
輸入 / 輸出 格式
第一行會有兩個數字,\(n\) 和 \(k\)
第二行會有 \(n\) 個數字,分別是每個金幣的面額
請輸出依照題意算出的結果是否 \(> k\)
是 = 輸出 "safewilly",否 = 輸出 "willygonemad"
解題思路
要怎麼做?
解題思路
要怎麼做?
解題思路
要怎麼做?
解題思路
要怎麼做?
解題思路
要怎麼做?
解題思路
要怎麼做?
後綴和!
後綴和的含意
現在我們知道可以用後綴和表示切割後的結果,但有什麼用呢?
→ 如何決定要切在哪
| 值 | 1 | 3 | -2 | 5 | -3 |
| 後綴和 | 4 | 3 | 0 | 2 | -3 |
後綴和的含意
| 值 | 1 | 3 | -2 | 5 | -3 |
| 後綴和 | 4 | 3 | 0 | 2 | -3 |
| -3 |
| 2 |
| 0 |
| 3 |
| 4 |
後綴和的含意
| 值 | 1 | 3 | -2 | 5 | -3 |
| 後綴和 | 4 | 3 | 0 | 2 | -3 |
| -3 |
| 2 |
| 0 |
| 3 |
| 4 |
Greedy,只要 > 0 的就要切
後綴和的含意
| 值 | 1 | 3 | -2 | 5 | -3 |
| 後綴和 | 4 | 3 | 0 | 2 | -3 |
不過還有一個問題
後綴和的含意
不過還有一個問題
| 值 | -8 | 3 | -2 | 5 | -3 |
| 後綴和 | -5 | 3 | 0 | 2 | -3 |
假設它的後綴和第 0 項是負的
我們可以不拿嗎?
後綴和的含意
| 值 | -8 | 3 | -2 | 5 | -3 |
| 後綴和 | -5 | 3 | 0 | 2 | -3 |
我們可以不拿嗎?
| 2 |
| 3 |
後綴和的含意
| 值 | -8 | 3 | -2 | 5 | -3 |
| 後綴和 | -5 | 3 | 0 | 2 | -3 |
我們可以不拿嗎?
| 2 |
| 3 |
一定得拿!
| -5 |
AC Code v.1
#include <iostream>
#include <vector>
int main() {
// 有多少金幣面額要輸入,最後要比較的門檻值
long long numCoins, expectedValue;
std::cin >> numCoins >> expectedValue;
// 輸入進陣列
std::vector<long long> coins(numCoins);
for (long long i = 0; i < numCoins; ++i) {
std::cin >> coins[i];
}
// 用同一個 vector 來存後綴和,只是改名叫 suffixSum 以免混淆
#define suffixSum coins
// 建立後綴和陣列
for (long long i = numCoins - 2; i >= 0; --i) {
suffixSum[i] += suffixSum[i + 1];
}
// 第 0 項一定得拿
long long result = suffixSum[0];
// 其他只拿 > 0 的
for (long long i = 1; i < numCoins; ++i) {
if (suffixSum[i] > 0)
result += suffixSum[i];
}
// 依照題目要求輸出
if (result >= expectedValue) {
std::cout << "safewilly\n";
}
else {
std::cout << "willygonemad\n";
}
}優化解答
真的需要把後綴和存起來嗎?
→ 邊建立邊判斷要不要拿
// 用一個變數 currentSuffixSum 紀錄目前的後綴和值
long long currentSuffixSum = 0, result = 0;
for (long long i = numCoins - 1; i >= 0; --i) {
currentSuffixSum += coins[i];
// 若 > 0 或者為第 0 項,則要拿
if (currentSuffixSum > 0 || i == 0)
result += currentSuffixSum;
}AC Code v.2
#include <iostream>
#include <vector>
int main() {
// 有多少數字要輸入,最後要比較的門檻值
long long numCoins, expectedValue;
std::cin >> numCoins >> expectedValue;
// 輸入進陣列
std::vector<long long> coins(numCoins);
for (long long i = 0; i < numCoins; ++i) {
std::cin >> coins[i];
}
// 用一個變數 currentSuffixSum 紀錄目前的後綴和值
long long currentSuffixSum = 0, result = 0;
for (long long i = numCoins - 1; i >= 0; --i) {
currentSuffixSum += coins[i];
// 若 > 0 或者為第一項,則要拿
if (currentSuffixSum > 0 || i == 0)
result += currentSuffixSum;
}
// 依照題目要求輸出
if (result >= expectedValue) {
std::cout << "safewilly\n";
}
else {
std::cout << "willygonemad\n";
}
}pD. Priority Deque

簡化提序
請維護一個資料結構,支援以下功能:
- 新增一個值到資料結構中
- 輸出最小值並同時刪除最小值
- 輸出最大值並同時刪除最大值
輸入 / 輸出 格式
首先輸入一個數字 \(Q\) 代表接下來有多少次操作
接著 \(Q\) 行可能有這三種指令:
-
1 X代表加入 \(x\) 這個數字 -
2 0代表輸出最小值並刪除它 -
2 1代表輸出最大值並刪除它
解題思路
Priority Deque 這個名字只是在搞你,它事實上就是 Multiset
在 C++ 中,Multiset 是標準範本庫 STL 中的一個容器,它是一個有序的關聯容器,允許儲存相同的元素。與 Set 不同的是,Multiset 允許儲存多個相同的元素,而 Set 只能儲存唯一的元素。
Multiset 以紅黑樹(Red-black Tree)的資料結構實現,這確保了元素的有序性。紅黑樹是一種自平衡的二元搜尋樹,可以保持元素按照某個特定的排序準則有序儲存。根據 ChatGPT:
AC Code
#include <iostream>
#include <set>
// 定義一個自己的資料結構
struct PriorityDeque {
std::multiset<long long> set;
void insert(long long value) {
set.insert(value);
}
long long popMin() {
// begin 是一個 iterator,用 * 取值
int smallest = *set.begin();
// 刪除它
set.erase(set.begin());
// 回傳
return smallest;
};
long long popMax() {
// end 是右界,左閉右開所以要 -- 才會是最後那一項的位置
int largest = *(--set.end());
set.erase(--set.end());
return largest;
}
};
int main() {
// 宣告一個我們自己定義的 struct
PriorityDeque pd;
// 有多少指令
int numActions;
std::cin >> numActions;
for (int i = 0; i < numActions; ++i) {
// 指令的第一部分,1 是插入值,2 是刪除值
char action;
std::cin >> action;
switch (action) {
case '1':
// 輸入要插入的值
long long value;
std::cin >> value;
// 插入
pd.insert(value);
break;
case '2':
// 指令的第二部分,0 是針對最小值,1 則是最大值
char symbol;
std::cin >> symbol;
if (symbol == '0') {
std::cout << pd.popMin() << "\n";
}
else {
std::cout << pd.popMax() << "\n";
}
break;
}
}
}
pE. PE 體育課

簡化題序
你有一些繁忙的時段,時段內不能參與任何活動
接著有一些活動時段(活動時段之間不會重疊)
求你最多可以參與多少活動?
輸入 / 輸出 格式
這題目的輸入格式超噁心
第一行是一個數字 \(n\) 代表有幾個繁忙時段
接下來一行是這 \(n\) 個繁忙時段的開始時刻
第三行是這 \(n\) 個繁忙時段的持續時長
第四行是一個數字 \(m\) 代表有幾個活動時段
接下來 \(m\) 行會有 \(s_i\) \(e_i\) 分別代表一個活動時段的開始與結束時間
解題思路
其實這題很暴力
就是跌代,O(\(nm\)) 解決
Busy
Event
解題思路
Busy
Event
其實這題很暴力
就是跌代,O(\(nm\)) 解決
解題思路
Busy
Event
其實這題很暴力
就是跌代,O(\(nm\)) 解決
解題思路
Busy
Event
其實這題很暴力
就是跌代,O(\(nm\)) 解決
解題思路
Busy
Event
其實這題很暴力
就是跌代,O(\(nm\)) 解決
解題思路
Busy
Event
答:共能出席 1 場活動
其實這題很暴力
就是跌代,O(\(nm\)) 解決
跌代器
我比較喜歡使用 iterator 來跌代 vector
順便解釋一下它的用法好了
vector.begin() 會回傳一個 iterator,指向陣列的開始
vector.end() 則是指向右界(最後一項的下一項,因為左閉右開)
使用 it++ 可以讓它指向下一個
使用 it-- 可以讓它指向上一個
使用 *it 會回傳它指向的那一項的值,但記得要加括號
TLE Code
#include <iostream>
#include <vector>
#include <algorithm>
// 定義一個結構代表一段時間,紀錄開始與結束
struct Period {
long long start;
long long end;
Period(long long start = -1, long long end = -1) : start(start), end(end) {}
};
// 給 std::sort 看的比較運算子
// 輸入兩個 Period,回傳左邊是否應該被整理到右邊的前面
bool operator < (Period left, Period right) {
// 左邊先開始,一定放前面
if (left.start < right.start) {
return true;
}
// 同時開始
else if (left.start == right.start) {
// 看哪邊先結束
return left.end < right.end;
}
// 右邊先開始
else {
return false;
}
}
int main() {
// 這題得加 IO 優化否則會 TLE
std::ios_base::sync_with_stdio(false);
std::cin.tie(0);
// 輸入有幾個繁忙時段
long long numBusies;
std::cin >> numBusies;
// 根據詭異的規則輸入 Busy Periods
std::vector<Period> busyPeriods(numBusies);
for (auto& b : busyPeriods)
std::cin >> b.start;
for (auto& b : busyPeriods) {
long long duration;
std::cin >> duration;
b.end = b.start + duration;
}
// 輸入有幾個活動時段
long long numEvents;
std::cin >> numEvents;
// 輸入 Event Periods
std::vector<Period> eventPeriods(numEvents);
for (auto& e : eventPeriods)
std::cin >> e.start >> e.end;
// 整理兩個陣列
std::sort(busyPeriods.begin(), busyPeriods.end());
std::sort(eventPeriods.begin(), eventPeriods.end());
long long result = 0;
// iterator 名字太長了,我懶得打
using Itr = std::vector<Period>::iterator;
// 把每個 Event Period 都跑過一遍,並確認有沒有重疊到任何 Busy Period
for (Itr ep = eventPeriods.begin(); ep != eventPeriods.end(); ++ep) {
// 從最開頭檢查
Itr bp = busyPeriods.begin();
// 忽略所有在這個 Event Period 開始之前就已經結束的 Busy Period
// 別忘了先判斷是不是整個陣列都走完了,否則
// 有可能會 無窮迴圈 / 跌代超出右界
while (bp != busyPeriods.end() && (*bp).end < (*ep).start)
++bp;
/* 如果已經把整個 Busy Period 陣列跑過一遍,代表
已經沒有其他 Busy Period 了,於是便符合條件
或者
下一個 Busy Period 的開始時間已經
超過現在這個 Event Period 的結束時間,因為
陣列是排好序的,所以後面全部的 Busy Period 都不可能會重疊 */
if (bp == busyPeriods.end() || (*bp).start > (*ep).end)
result++;
}
std::cout << result << "\n";
}
優化解答
首先,我們只是要存開始和結束時間
似乎不需要用自己的 struct,用 pair 應該就可以了
這樣也不用自己定義用來 sort 的比較運算子
然後,重點來了,真的有必要每次都重頭判斷每一個 Busy Period 嗎?
→ 已經在上一個活動時段開始之前就結束的繁忙時段
不可能會重疊到現在這個活動時段!
→ 每次迴圈不用重置 bp
AC Code
#include <iostream>
#include <vector>
#include <algorithm>
#include <utility>
// 改用 pair 取代 Period
// 但是寫 pair、first 和 second 很不直覺,改掉
#define Period std::pair<long long, long long>
#define startTime first
#define endTime second
int main() {
// 這題得加 IO 優化否則會 TLE
std::ios_base::sync_with_stdio(false);
std::cin.tie(0);
// 輸入有幾個繁忙時段
long long numBusies;
std::cin >> numBusies;
// 根據詭異的規則輸入 Busy Periods
std::vector<Period> busyPeriods(numBusies);
for (auto& b : busyPeriods)
std::cin >> b.startTime;
for (auto& b : busyPeriods) {
long long duration;
std::cin >> duration;
b.endTime = b.startTime + duration;
}
// 輸入有幾個活動時段
long long numEvents;
std::cin >> numEvents;
// 輸入 Event Periods
std::vector<Period> eventPeriods(numEvents);
for (auto& e : eventPeriods)
std::cin >> e.startTime >> e.endTime;
// 整理兩個陣列
std::sort(busyPeriods.begin(), busyPeriods.end());
std::sort(eventPeriods.begin(), eventPeriods.end());
long long result = 0;
// iterator 名字太長了,我懶得打
using Itr = std::vector<Period>::iterator;
// 一開始從頭,後來就不用重置了
Itr bp = busyPeriods.begin();
// 把每個 Event Period 都跑過一遍,並確認有沒有重疊到任何 Busy Period
for (Itr ep = eventPeriods.begin(); ep != eventPeriods.end(); ++ep) {
// 忽略所有在這個 Event Period 開始之前就已經結束的 Busy Period
// 別忘了先判斷是不是整個陣列都走完了,否則
// 有可能會 無窮迴圈 / 跌代超出右界
while (bp != busyPeriods.end() && (*bp).endTime < (*ep).startTime)
++bp;
/* 如果已經把整個 Busy Period 陣列跑過一遍,代表
已經沒有其他 Busy Period 了,於是便符合條件
或者
下一個 Busy Period 的開始時間已經
超過現在這個 Event Period 的結束時間,因為
陣列是排好序的,所以後面全部的 Busy Period 都不可能會重疊 */
if (bp == busyPeriods.end() || (*bp).startTime > (*ep).endTime)
result++;
}
std::cout << result << "\n";
}
好欸下課
話說 有些題目是我先填好名字 他們才開始想題目內容的
偷偷放個總結
這次大社賽題目其實不是特別困難
我覺得最有意義的就是 pC 雖然它題序一堆廢話
認真說就是 cjtsai 在搞你們啦
然後其實是因為官方的題解我看不懂…… 所以我才做這份簡報的 :D
2023 大社賽 題解
By 晴☆
2023 大社賽 題解
- 181