# APCS 2022年06月題解
## 第一題 數字遊戲
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=i399
### 解套
N/A
### 題目分析
這種題目有很多作法,最基礎的應該是存三個變數然後分各種狀況討論
但那樣實作複雜度感覺有點高,所以我直接用C++內建的資料結構(~~我就懶~~)
要滿足「去除重複項」和「從大到小排列」,很明顯要用set
(其實也能用vector再排序再erase unique,但一樣感覺好麻煩)
另外要用set<int,greater< int>才會讓元素從大到小排列
而眾數數量可以由4-st.size()算出來
如此時間複雜度$O(1)$
(技術上是$O(nlogn)$,但反正n只有三,當常數也行ㄏㄏ)
### 解題流程
讀入測資$→$塞入set$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int a,b,c;
```
#### 把數字放進set
```cpp=6
set<int,greater<int>>st;
st.insert(a);
st.insert(b);
st.insert(c);
```
#### 輸出
眾數數量
```cpp=10
cout<<4-st.size();
```
元素
```cpp=11
auto it=st.begin();
while(it!=st.end()){
cout<<" "<<*it;
it++;
}
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int a,b,c;
int main(){
cin>>a>>b>>c;
set<int,greater<int>>st;
st.insert(a);
st.insert(b);
st.insert(c);
cout<<4-st.size();
auto it=st.begin();
while(it!=st.end()){
cout<<" "<<*it;
it++;
}
}
```
## 第二題 字串解碼
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=i400
### 解套
依指定規則解密
### 題目分析
**重要觀念:解密時要把加密時所有順序都反著做**
除了加密字串要由後往前處理外,
也要先做加密步驟2(按照01判斷接前接後)再做加密步驟1(前後交換)
接下來分析一下加密的兩個步驟要怎麼反操作
1. 很明顯操作不變,因為交換的反操作就是交換
2. 因為加密是「若為0接後面,否則接前面」,因此要還原它就要「若為0接前面,否則接後面」,而且在加密時是「由1到n」,所以解密要「由n到1」
如此就得到解密步驟:
1. 從後往前遍歷字串,並按照01產生新字串,同時記錄有幾個1
2. 如果1的數量是奇數就把字串前後交換
在執行上,步驟1時我用deque執行剪接字元的操作,並於步驟2時再依照需求把它轉成相對應的字串
整個解密過程我寫成一個函數,方便重複調用(加密字串可能不只一個)
如此複雜度$O(mn)$
### 解題流程
讀入測資$→$重複解密$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int m,n;
string s,e;
vector<string>Es;
```
m,n,s,e與題目同義
Es存所有01字串
#### 解密函式
**函式宣告**
```cpp=6
string chg(string s,string e){
```
s是要解密的字串,e是01字串
**變數**
```cpp=7
int cnt=0;
deque<char>dq;
```
cnt紀錄01字串裡有幾個1,dq用於解密操作
```cpp=18
string rt;
```
最後回傳的字串
**步驟一(按照01字串生成新字串)**
```cpp=9
for(int i=e.size()-1;i>=0;i--){
if(e[i]=='1'){
cnt++;
dq.push_back(s[i]);
}
else{
dq.push_front(s[i]);
}
}
```
從後往前遍歷字串,若該位置為1將字元接到後面,否則接到前面
同時記錄1的數量
**步驟二(前後半交換)**
不需交換
```cpp=19
if(cnt%2==0){
for(char i:dq)rt+=i;
return rt;
}
```
直接把dq轉成字串並回傳
---
需要交換
```cpp=23
else{
if(s.size()%2==0){
int mid=s.size()/2;
for(int i=mid;i<s.size();i++)rt+=dq[i];
for(int i=0;i<mid;i++)rt+=dq[i];
}
else{
int mid=s.size()/2;
for(int i=mid+1;i<s.size();i++)rt+=dq[i];
rt+=dq[mid];
for(int i=0;i<mid;i++)rt+=dq[i];
}
return rt;
}
```
先把dq後半變成字串,再把前半段接上去,最後回傳
#### 讀入測資
```cpp=39
cin>>m>>n;
while(m--){
cin>>e;
Es.push_back(e);
}
cin>>s;
```
#### 解密、輸出
```cpp=45
while(Es.size()){
s=chg(s,Es[Es.size()-1]);
Es.pop_back();
}
cout<<s;
```
將01字串按輸入順序倒著處理
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int m,n;
string s,e;
vector<string>Es;
string chg(string s,string e){
int cnt=0;
deque<char>dq;
for(int i=e.size()-1;i>=0;i--){
if(e[i]=='1'){
cnt++;
dq.push_back(s[i]);
}
else{
dq.push_front(s[i]);
}
}
string rt;
if(cnt%2==0){
for(char i:dq)rt+=i;
return rt;
}
else{
if(s.size()%2==0){
int mid=s.size()/2;
for(int i=mid;i<s.size();i++)rt+=dq[i];
for(int i=0;i<mid;i++)rt+=dq[i];
}
else{
int mid=s.size()/2;
for(int i=mid+1;i<s.size();i++)rt+=dq[i];
rt+=dq[mid];
for(int i=0;i<mid;i++)rt+=dq[i];
}
return rt;
}
}
int main(){
cin>>m>>n;
while(m--){
cin>>e;
Es.push_back(e);
}
cin>>s;
while(Es.size()){
s=chg(s,Es[Es.size()-1]);
Es.pop_back();
}
cout<<s;
}
```
## 第三題 雷射測試
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=i401
### 解套
一平面上有許多鏡子,其中有\與/兩種,一雷射光從$(0,0)$向右出發,其會反射幾次?
### 題目分析
很明顯地,這題最大的挑戰在於如何快速地找到下一個遇到的鏡子
如果每次都要從所有鏡子裡$O(n)$地找,複雜度會是$O(n^2)$,顯然是無法接受的,而跟搜尋有關又比線性搜尋快的是...二分搜!
不過這時又會遇到另一個問題,就是鏡子要事先按照甚麼順序排序
這時會發現如果是往左右走會需要遇到相同y座標的鏡子才行,故鏡子應該按照y座標由小到大排序
//畫圖?
同理如果是往上下走會需要遇到相同x座標的鏡子才行,故鏡子應該按照x座標由小到大排序
因此就得到我們的實作方式:
開兩個set儲存鏡子座標,一個叫xst按x座標由小到大排,另一個叫yst按y座標由小到大排
遍歷時紀錄當前的x座標、y座標、鏡子種類與前進方向,並根據前進方向尋找下一面鏡子,
若找不到下一面鏡子則結束遍歷
如此時間複雜度$O(nlogn)$
### 解題流程
讀入測資$→$遍歷、處理$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int n,x,y;
bool t;
set<pair<pair<int,int>,bool>>xst,yst;
```
變數名稱與題目或前面提到的相同
```cpp=14
int ans=0,cx=0,cy=0,ct=0,d=3;// up down left right 0 1 2 3
```
ans存最終答案,cx、cy、ct分別是當前x座標、y座標與鏡子種類,
d存前進方向(上下左右分別是0123)
#### 讀入測資、初始化
```cpp=7
cin>>n;
for(int i=0;i<n;i++){
cin>>x>>y>>t;
xst.insert({{x,y},t});
yst.insert({{y,x},t});
}
yst.insert({{0,0},0});
```
因為一開始是從$(0,0)$向右,故在yst放入初始點座標
#### 遍歷
**方向向上**
```cpp=16
if(d==0){
auto it=xst.find({{cx,cy},ct}),nxt=xst.find({{cx,cy},ct});
nxt++;
if(nxt==xst.end()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.first;
cy=nxt->first.second;
ct=nxt->second;
if(ct==0)d=3;
else d=2;
}
}
```
(因為是向上所以用xst尋找)
在xst中找到當前座標與下一個座標(下一個座標用目前座標往後推來得到)(第17~18行)
此時若沒有下一個位置(到底了)或是下一個位置x座標已經不一樣了,
代表雷射不會再繼續反射,終止遍歷(第19行)
接下來就是更新座標與鏡子種類,並根據鏡子種類決定新的方向
若從下面往上撞上$/$,方向會改為向右(3),若從下面往上撞上$\backslash$,方向會改為向左(2)
---
其他方向依此類推
**方向向下**
```cpp=28
else if(d==1){
auto it=xst.find({{cx,cy},ct}),nxt=xst.find({{cx,cy},ct});
nxt--;
if(it==xst.begin()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.first;
cy=nxt->first.second;
ct=nxt->second;
if(ct==0)d=2;
else d=3;
}
}
```
因為向下y座標會減少,故下一位置要用當前位置往前推一個
**方向向左**
```cpp=40
else if(d==2){
auto it=yst.find({{cy,cx},ct}),nxt=yst.find({{cy,cx},ct});
nxt--;
if(it==yst.begin()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.second;
cy=nxt->first.first;
ct=nxt->second;
if(ct==0)d=1;
else d=0;
}
}
```
**方向向右**
```cpp=52
else{
auto it=yst.find({{cy,cx},ct}),nxt=yst.find({{cy,cx},ct});
nxt++;
if(nxt==yst.end()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.second;
cy=nxt->first.first;
ct=nxt->second;
if(ct==0)d=0;
else d=1;
}
}
```
---
**更新答案**
```cpp=64
ans++;
yst.erase({{0,0},0});
```
另外要把初始座標刪掉,因為那邊事實上沒有鏡子
#### 輸出
```cpp=67
cout<<ans;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int n,x,y;
bool t;
set<pair<pair<int,int>,bool>>xst,yst;
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>x>>y>>t;
xst.insert({{x,y},t});
yst.insert({{y,x},t});
}
yst.insert({{0,0},0});
int ans=0,cx=0,cy=0,ct=0,d=3;// up down left right 0 1 2 3
while(1){
if(d==0){
auto it=xst.find({{cx,cy},ct}),nxt=xst.find({{cx,cy},ct});
nxt++;
if(nxt==xst.end()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.first;
cy=nxt->first.second;
ct=nxt->second;
if(ct==0)d=3;
else d=2;
}
}
else if(d==1){
auto it=xst.find({{cx,cy},ct}),nxt=xst.find({{cx,cy},ct});
nxt--;
if(it==xst.begin()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.first;
cy=nxt->first.second;
ct=nxt->second;
if(ct==0)d=2;
else d=3;
}
}
else if(d==2){
auto it=yst.find({{cy,cx},ct}),nxt=yst.find({{cy,cx},ct});
nxt--;
if(it==yst.begin()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.second;
cy=nxt->first.first;
ct=nxt->second;
if(ct==0)d=1;
else d=0;
}
}
else{
auto it=yst.find({{cy,cx},ct}),nxt=yst.find({{cy,cx},ct});
nxt++;
if(nxt==yst.end()||nxt->first.first!=it->first.first)break;
else{
cx=nxt->first.second;
cy=nxt->first.first;
ct=nxt->second;
if(ct==0)d=0;
else d=1;
}
}
ans++;
yst.erase({{0,0},0});
}
cout<<ans;
}
```
## 第四題 內積
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=i402
### 解套
有兩長度分別為$n,m$的陣列$A_1,A_2,...,A_n$與$B_1,B_2,...,B_n$
對任何正整數$r$,求$\sum_{k=0}^{r} A_{i+k}+B_{j+k}$的最大值
### 題目分析
看到這題我第一個想到的是:「這題的n和m都只有1000而已誒!好像可以$O(nm)$硬爆之類的」
不過用最直觀的方式(所有組合試一遍且沒有優化)複雜度大概會是$O(n^2m)$之類的
所以問題就變成:要如何效率的枚舉?
這時我們發現,如果我們固定其中一個陣列永遠從第一個數字開始試,並枚舉另一陣列的起始位置,
就能把可能的組合數壓到$n$或$m$
(例如第二個陣列固定從第一個開始,另一個陣列依序用第一個、第二個...跟第一個配)
而對每一個組合來說,答案會跟最大子序列和有些相似,只是處理的數字會是兩個陣列相應值的乘積
舉例來說,假設兩陣列分別是$[1,2,3,4]$、$[-1,9,7,-5]$,那枚舉的部分就會讓1配到-1、2配到-1、3配到-1、4配到-1
而就2配到-1的例子來說,就相當於處理$[-2,27,28]$的最大子序列和,也就是55
不過題目說可以翻轉,所以要把某個陣列倒轉後,再把上述過程重複一遍,就能的到答案
如此時間複雜度是枚舉的$O(n)$乘上最大子序列和的$O(n)$,也就是$O(n^2)$
### 解題流程
讀入測資$→$遍歷、處理$→$翻轉$→$遍歷、處理$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int n,m,A[1000+10],B[1000+10];
```
變數名稱與題述相同
```cpp=6
int ans=INT_MIN;
```
最終的答案,先設為極小值
#### 輸入
```cpp=5
cin>>n>>m;
```
```cpp=7
if(n>m){
for(int i=0;i<n;i++)cin>>A[i];
for(int i=0;i<m;i++)cin>>B[i];
}
else{
for(int i=0;i<n;i++)cin>>B[i];
for(int i=0;i<m;i++)cin>>A[i];
swap(n,m);
}
```
為方便起見,強制讓A是長度比較長的陣列,且n比m大
#### 計算答案
```cpp=16
for(int i=-m+1;i<n;i++){
int tmp=0;
for(int j=0;j<m;j++){
if(i+j>=n||i+j<0)continue;
tmp+=A[i+j]*B[j];
ans=max(ans,tmp);
tmp=max(tmp,0);
}
}
```
外層迴圈決定A陣列的起始位置,內層迴圈則是一般的最大子序列和
```cpp=25
for(int i=0;i<m/2;i++)swap(B[i],B[m-i-1]);
```
將B陣列倒轉
```cpp=26
for(int i=-m+1;i<n;i++){
int tmp=0;
for(int j=0;j<m;j++){
if(i+j>=n||i+j<0)continue;
tmp+=A[j+i]*B[j];
ans=max(ans,tmp);
tmp=max(tmp,0);
}
}
```
B陣列倒轉後再更新一次答案
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int n,m,A[1000+10],B[1000+10];
int main(){
cin>>n>>m;
int ans=INT_MIN;
if(n>m){
for(int i=0;i<n;i++)cin>>A[i];
for(int i=0;i<m;i++)cin>>B[i];
}
else{
for(int i=0;i<n;i++)cin>>B[i];
for(int i=0;i<m;i++)cin>>A[i];
swap(n,m);
}
for(int i=-m+1;i<n;i++){
int tmp=0;
for(int j=0;j<m;j++){
if(i+j>=n||i+j<0)continue;
tmp+=A[i+j]*B[j];
ans=max(ans,tmp);
tmp=max(tmp,0);
}
}
for(int i=0;i<m/2;i++)swap(B[i],B[m-i-1]);
for(int i=-m+1;i<n;i++){
int tmp=0;
for(int j=0;j<m;j++){
if(i+j>=n||i+j<0)continue;
tmp+=A[j+i]*B[j];
ans=max(ans,tmp);
tmp=max(tmp,0);
}
}
cout<<ans;
}
```
## 心得
這是第二次參加APCS,比起上次有把握了許多,原本的目標是拿到四級分並看看有沒有辦法五級,後來也確實達成了,原本心情還不錯,只是後來聽說同學拿到五級分,自己事後再想想發現第四題真的沒有那麼難,自己當初應該要寫出來的,所以懊惱了一陣子...
---
當初在寫的時候前三題基本上沒有遇到大障礙,只是因為第二跟第三題實作量都比較大一些所以花了比較多時間。第四題我記得當時想弄一種有點複雜的DP,結果要到太多記憶體導致程式當掉(這是事後想到發現的,那時候只是自己乾著急而已)。不過因為原本只想說把前三題寫完,時間也剩不多,我就放棄思考第四題而去檢查前三題看有沒有bug,當然後來證明這個選擇不是太聰明。
---
我認這次最難的是第三題,因為實作上最複雜,當時只有想到set,不過後來自己重寫的時候發現用vector配lower_bound就不用把原點也放進去事後再拿掉。另外第四題事實證明沒有想像中那麼難,甚至算不太上是DP,可見自己不應該自我設限,即使一開始想不出來也要努力嘗試,說不定就試出答案了。