---
# System prepended metadata

title: APCS 2022年06月題解
tags: [APCS]

---

# 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，可見自己不應該自我設限，即使一開始想不出來也要努力嘗試，說不定就試出答案了。
