---
tags: 成大高階競技程式設計 2020
---
# 2020 高階競程 Contest #1 - 題解
## [藍的競程之旅–炎論嬸茶](https://judge.csie.ncku.edu.tw/problems/18)
- Task Provider:ian
- Task Setter:ian
### 首殺 team20_23 (00:04)
### 解法說明
本題的字典其實就是 stack ,每次只能 push or pop 最上面
但在查詢的時候不能遍歷,否則會 TLE。
解決的方式為同時維護 stack and set,查詢時使用 set 進行查詢。
另外要注意有可能在字典為空時進行 pop ,所以要特判
### 標準程式
:::spoiler
```cpp
#include <bits/stdc++.h>
using namespace std;
string s;
set<string> st;
stack<string> dict;
int main() {
int n, m, i, j, k, cmd;
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
while (n--) {
cin >> cmd;
if (cmd == 1) {
cin >> s;
dict.push(s);
st.insert(s);
} else if (cmd == 2) {
if (dict.size()) {
string top = dict.top();
st.erase(top);
dict.pop();
}
} else {
cin >> s;
if (st.find(s) != st.end()) {
cout << "Y";
} else {
cout << "N";
}
}
}
cout << endl;
return 0;
}
```
:::
## [林野調查](https://judge.csie.ncku.edu.tw/problems/19)
- Task Provider:leo900807
- Task Setter:leo900807
### 首殺 team20_23 (00:10)
### 解法說明
#### Subtask 1 $\ \ \ O(n^2)$
只要每一次都 $O(k)$ 掃過去找最大值就可以,共掃 $n-k+1$ 次,以 $n-k$ 次計,根據算幾不等式 $\frac{k + (n-k)}{2}\geq\sqrt{k(n-k)}\Rightarrow k(n-k)\leq\frac{n^2}{4}$ 。
#### Subtask 2 $\ \ \ O(n\log k)$
只要用長度為 $k$ 的滑動窗口,當一個數字被包含進去時就將其計入 map/multiset ,離開時就移出 map/multiset ,每次操作完後取 map/multiset 中最大值。
#### Subtask 3 $\ \ \ O(n)$
其實這是一道經典問題,叫做「滑動窗口最大值」,由於沒教過 deque ,可以以 list 代替,但是還是建議使用 deque ,常數較小。
用一個長度為 $k$ 的滑動窗口,每次有新的值要進來時,就判斷 deque/list 尾端元素,若尾端元素小於當前元素或是 deque/list 為空時就將其 pop ,如此一來可以維護 deque/list 內的元素是遞減的,當要離開時就判斷 deque/list 前端元素的 position 是否小於當前窗口 ,若小於就將其 pop 掉,每次只要取 deque/list 前端元素就是當前窗口最大值。
而這會是正確的原因,是因為後面來的會存在在窗口中較久,代表在它前面且較小的,在之後都不會是最大值,因此可以直接將該值 pop。
每個元素至多進出 deque/list $1$ 次,均攤複雜度 $O(n)$ ,這種作法叫做「單調隊列」,因為在這個隊列中所有元素都維持著遞減的單調性。
~~被各種假解QQ~~
### 標準程式
因為常數問題,標準程式使用 deque 。
:::spoiler
```cpp
#include <iostream>
#include <deque>
#define F first
#define S second
#define MP make_pair
using namespace std;
deque<pair<int, int>> q;
//first is position
//second is element
int main() {
ios::sync_with_stdio(0), cin.tie(0);
int n, k, x;
cin >> n >> k;
for(int i = 0; i < k - 1; i++){
cin >> x;
while(!q.empty() && q.back().S < x)
q.pop_back();
q.push_back(MP(i, x));
}
for(int i = k - 1; i < n; i++){
cin >> x;
while(!q.empty() && q.back().S < x)
q.pop_back();
q.push_back(MP(i, x));
while(!q.empty() && q.front().F <= i - k)
q.pop_front();
cout << q.front().S << " \n"[i == n - 1];
}
}
```
:::
## [丟雞蛋問題](https://judge.csie.ncku.edu.tw/problems/20)
- Task Provider:leo900807
- Task Setter:leo900807
### 首殺 team20_23 (00:15)
### 解法說明
#### Subtask 1
本 Subtask 主要是為了讓參賽者測試程式是否能執行。
#### Subtask 2 $\ \ \ O(M)$
只要從 $1$ 到 $60$ 慢慢往上查詢即可。
#### Subtask 3 $\ \ \ O(\log M)$
假設在第 $i$ 層會破,則大於第 $i$ 層都會破;假設在第 $i$ 層不會破,則小於第 $i$ 層都不會破,可以用此方法來二分搜找出 $k$ 是多少。
### 標準程式
:::spoiler
```cpp
#include "lib0020.h"
long long height_limit(long long M){
long long l = 1, r = M + 1, mid;
while(l < r){
mid = l + r >> 1;
if(is_broken(mid))
r = mid;
else
l = mid + 1;
}
return l - 1;
}
```
:::
## [修補棒棒](https://judge.csie.ncku.edu.tw/problems/21)
- Task Provider:raiso
- Task Setter:raiso
### 首殺 team20_23 (00:31)
### 解法說明
這題重點在於 $k < n$ 時,勢必有兩個洞以上需要使用同一段膠帶修復。
1. 修補完全部的洞,至少需要長度 $n$。
2. 找出需要使用同一段膠帶補的個數 ($n-k$)。
ex: $n=4, k=2$,需要連接兩組洞,省下兩段
3. 找出滿足使用同一段膠帶連接兩個洞的代價,排序,從最小的代價開始選用。
### 標準程式
:::spoiler
```cpp
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll n, m, k;
vector< ll > del;
ll ans = 0;
int main() {
cin >> n >> m >> k;
del.resize(n-1);
ll lst, now;
cin >> lst;
if(n == 1) {
cout << 1 << endl;
return 0;
}
for(int i = 1; i < n; i++) {
cin >> now;
del[i-1] = now - lst;
lst = now;
}
sort(del.begin(), del.end());
ans += n;
for(int i = 0 ; i < n-k; i++) {
ans += (del[i]-1);
}
cout << ans << endl;
return 0;
}
```
:::
## [爬樓梯 k](https://judge.csie.ncku.edu.tw/problems/22)
- Task Provider:ys
- Task Setter:ys
### 首殺 team20_23 (01:00)
### 解法說明
回顧教材的[上樓梯問題](https://hackmd.io/@nckuacm/r1ZEy4ar8#%E7%AF%84%E4%BE%8B-LeetCode-70-Climbing-Stairs%EF%BC%9A)
```cpp
dp[0] = dp[1] = 1;
for(int i = 2; i <= n; i++) dp[i] = dp[i-1] + dp[i-2];
```
造成答案變動的**決策**有每次要走 $1$ 階還是走 $2$ 階的選擇
但是 $dp(i)$ 依舊沒辦法紀錄是否走了 $k$ 次或是正往 $k$ 次邁進
不妨就多為**目前走了幾次**新增一個狀態 $dp(i, j)$
由於走一次會從當前 $j-1$ 次變為 $j$,轉移為
$$
dp(i, j) =
\begin{cases}
dp(i-1, j-1) + dp(i-2, j-1) &\text{if } j \le \min(i, k) \\
0 &\text{otherwise}
\end{cases}
$$
> $j$ 不能超過 $k$ 次,也沒辦法用 $i$ 次以上走到第 $i$ 階樓梯
邊界為 $dp(0, 0) = dp(1, 1) = dp(2, 1) = 1$
> $dp(0, 0) = 1$ 表示站在地板不需要走就是一種方法
最終答案為 $\sum\limits_{j=0}^k dp(N, j)$
特別留意,在求解過程中,變數可能會溢位
但題目只要求答案在除餘 $10^9 + 9$ 範圍內,所以求解中可順便除餘 $10^9 + 9$ 以防溢位
### 標準程式
:::spoiler
```cpp
#include<bits/stdc++.h>
using namespace std;
int const maxn = 1e3 + 10;
int const MOD = 1000000009;
int N, k, dp[maxn][maxn];
int main()
{
scanf("%d%d", &N, &k);
dp[0][0] = dp[1][1] = dp[2][1] = 1;
for(int i = 2; i <= N; i++)
for(int j = 2; j <= min(k, i); j++)
dp[i][j] = (dp[i-1][j-1] + dp[i-2][j-1]) % MOD;
int sum = 0;
for(int j = 0; j <= k; j++) sum = (sum + dp[N][j]) % MOD;
printf("%d\n", sum);
return 0;
}
```
:::