---
# System prepended metadata

title: APCS 2023年01月題解
tags: [APCS]

---

# APCS 2023年01月題解
## 第一題 程式考試
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=j605
### 解套
N/A(抱歉我真的不知道要寫甚麼)
### 題目分析
維護三個變數$mx$、$mxtime$、$errors$存最高分、最高分發生時間與嚴重錯誤次數
接下來就一一讀入測資並依題目敘述處理，時間複雜度$O(K)$
### 解題流程
讀入測資$→$更新變數$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int k,s,t,mx=-1,mxtime,errors=0;
```
#### 輸入與處理
```cpp=5
cin>>k;
for(int i=0;i<k;i++){
    cin>>t>>s;
    if(s==-1)errors++;
    else if(s>mx){
        mx=s;
        mxtime=t;
    }
}
```
得到時間與分數後，若為嚴重錯誤則將$errors$加一，
不然判斷該分數是否為目前最高的，若是則更新$mx$與$mxtime$
#### 輸出
```cpp=14
cout<<(mx-k-errors*2>0?mx-k-errors*2:0)<<" "<<mxtime;
```
分數部分按照題意計算後，若為負數輸出$-1$
※三元運算子：
```cpp
if(mx-k-errors*2>0)cout<<mx-k-errors*2;
else cout<<0;
//等價於
cout<<(mx-k-errors*2>0?mx-k-errors*2:0);
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int k,s,t,mx=-1,mxtime,errors=0;
int main(){
    cin>>k;
    for(int i=0;i<k;i++){
        cin>>t>>s;
        if(s==-1)errors++;
        else if(s>mx){
            mx=s;
            mxtime=t;
        }
    }
    cout<<(mx-k-errors*2>0?mx-k-errors*2:0)<<" "<<mxtime;
}
```
## 第二題 造字程式
### 題目敘述 
https://zerojudge.tw/ShowProblem?problemid=j606
### 解套
裸的字串處理，唯一需要注意的是輸出需求
### 題目分析
按照題目需求照做就好，並在每個操作後把會用到的字元存起來，最後再輸出。
實作上採用比較直觀但稍微沒有效率的做法(反正這是APCS第二題複雜度不是重點)，就是每次操作直接原地複製一個新字串，並以其為操作基準
每次操作完再將新字串第$0～r-1$個字元存起來，時間複雜度$O(Q(R+K))$
### 解題流程
輸入$→$$($輸入操作$→$處理、儲存$)_{循環}$$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
string s;
int k,q,r,p[20];
```
變數名稱與題目描述相同
```cpp=7
vector<vector<char>>ans(r,vector<char>(q,' '));
```
儲存答案的陣列，大小為$r*q$
#### 操作處理
```cpp=8
for(int i=0;i<q;i++){
    for(int j=0;j<k;j++)cin>>p[j];
    string tmp=s;
    for(int j=0;j<k;j++)s[p[j]-1]=tmp[j];
    for(int j=0;j<r;j++)ans[j][i]=s[j];
}
```
先讀入操作陣列 $p$，再複製一個跟$s$一樣的$tmp$當做操作時的參考
接下來(第11行)就是按照$tmp$ 與 $p$把新字串複製到$s$ 裡
最後(第12行)將操作後的字串存到答案陣列裡
#### 輸出
```cpp=14
for(int i=0;i<r;i++){
    for(int j=0;j<q;j++)cout<<ans[i][j];
    cout<<"\n";
}
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
string s;
int k,q,r,p[20];
int main(){
    cin>>k>>q>>r>>s;
    vector<vector<char>>ans(r,vector<char>(q,' '));
    for(int i=0;i<q;i++){
        for(int j=0;j<k;j++)cin>>p[j];
        string tmp=s;
        for(int j=0;j<k;j++)s[p[j]-1]=tmp[j];
        for(int j=0;j<r;j++)ans[j][i]=s[j];
    }
    for(int i=0;i<r;i++){
        for(int j=0;j<q;j++)cout<<ans[i][j];
        cout<<"\n";
    }
}
```
## 第三題 先加後乘與函數
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=j607
### 解套
計算一式子，但是先加減後乘，並且有一神秘函數$f$，會回傳參數內的最大值減最小值
### 題目分析
基本上跟經典的「計算式子」題目相似，只是少了除法，以及多了$f$函數
就我所知一般會用遞迴或是stack寫，我個人是偏好遞迴的寫法
實作上我寫了三個函式，分別是獲取數字的getnum、一般計算的cal以及算$f$函式的fcal
在一開始先讀入整個字串，並用全域變數id紀錄目前處理到哪一個位置
時間複雜度$O(N)$，其中$N$為字串長度
### 解題流程
呼叫cal函式$→$輸出答案
### 解題過程
#### 開 long long
```cpp=2
#define int long long
```
因為到最後才發現，而且懶得把每個int都改成long long
不過int main()要改成signed main()，因為main函式不能是 long long
#### 變數宣告
```cpp=4
string s;
int id=0;
```
id紀錄處理到字串的第幾個位置

#### 函式宣告
**1.getnum 函式**
```cpp=6
int getnum(){
    int ans=0;
    while(id<s.size()&&'0'<=s[id]&&s[id]<='9'){
        ans=ans*10+s[id]-'0';
        id++;
    }
    return ans;
}
```
跟一般字串轉數字的方法差不多，好像可以用stoi，但我不太會用

**2.cal、fcal函式**
```cpp=14
int cal(bool cmp);
int fcal();
```
因為他們會互相調用，所以先宣告，到main函式後再定義

**3.cal函式**
```cpp=33
int cal(bool cmp){
    int n;
    if('0'<=s[id]&&s[id]<='9')n=getnum();
    else n=fcal();
    if(cmp||s[id]==','||s[id]==')')return n;
    while(id<s.size()){
        if(s[id]=='+'){
            id++;
            int b=cal(1);
            n+=b;
        }
        else if(s[id]=='-'){
            id++;
            int b=cal(1);
            n-=b;
        }
        else if(s[id]=='*'){
            id++;
            int b=cal(0);
            n*=b;
        }
        if(s[id]==','||s[id]==')')return n;
    }
    return n;
}
```
cal的參數cmp是決定「要不要馬上回傳」
因為本題先加減後乘，如果遇到加或減需要先運算，乘則不用，在後面會有詳細解釋

一開始先宣告一個變數n來運算與當作回傳的答案
因為呼叫cal時第一個不是數字就是f，故依此決定n的數值(第34~36行)

此時若cmp為1(須馬上回傳)、碰到「,」或「)」(代表在f函式內)，則直接回傳n(第37行)

接下來就是不斷向後運算(第38~55行)，此時分做4種狀況
* 遇到加號
    1.將id加一，移到下一個數字
    2.呼叫cal(1)並將其值存在變數b裡
    3.把n加上b
* 遇到減號
    1.將id加一，移到下一個數字
    2.呼叫cal(1)並將其值存在變數b裡
    3.把n減掉b
* 遇到乘號
    1.將id加一，移到下一個數字
    2.呼叫cal(0)並將其值存在變數b裡
    3.把n除上b
* 遇到「,」或「)」
    直接回傳數字

在這邊解釋一下cmp的運作：
假設有一式子$5+2*3+1$，依照這題的規則會得到28(7*4)
當我算到5後面的加號時，因為先加減後乘除，我希望這個加號會**馬上算到**，因此cmp設為1，
這樣算到2時2就會直接回傳，而非繼續往後算$*$的部分
同理算到$*$時，會希望後面的3+1先算完再一起乘，所以cmp設為0，
這樣算到3時就會繼續往後算，最後再回傳

函式最後回傳n(第57行)

**4.fcal函式**
```cpp=20
int fcal(){
    id+=2;
    int mx=cal(0),mn=mx;
    while(s[id]!=')'){
        id++;
        int tmp=cal(0);
        mx=max(mx,tmp);
        mn=min(mn,tmp);

    }
    id++;
    return mx-mn;
}
```
一開始先把id加二以跳過$"f("$(第21行)
因為f函式內至少有一個數，故將此數以cal(0)取得後同時設為最大與最小值
接下來就是不斷地將id加一、用cal(0)取得f裡的數字、與更新最大最小值
這個步驟重複到遇到「)」為止(f函式的結束)(第23~29行)
最後將id加一以跳過「)」，並回傳最大值減最小值
#### main函式
```cpp=16
signed main(){
    cin>>s;
    cout<<cal(0);
}
```
輸入字串後輸出cal(0)的值
其中signed跟int同義，用signed是避免被define轉成long long
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
#define int long long
using namespace std;
string s;
int id=0;
int getnum(){
    int ans=0;
    while(id<s.size()&&'0'<=s[id]&&s[id]<='9'){
        ans=ans*10+s[id]-'0';
        id++;
    }
    return ans;
}
int cal(bool cmp);
int fcal();
signed main(){
    cin>>s;
    cout<<cal(0);
}
int fcal(){
    id+=2;
    int mx=cal(0),mn=mx;
    while(s[id]!=')'){
        id++;
        int tmp=cal(0);
        mx=max(mx,tmp);
        mn=min(mn,tmp);

    }
    id++;
    return mx-mn;
}
int cal(bool cmp){
    int n;
    if('0'<=s[id]&&s[id]<='9')n=getnum();
    else n=fcal();
    if(cmp||s[id]==','||s[id]==')')return n;
    while(id<s.size()){
        if(s[id]=='+'){
            id++;
            int b=cal(1);
            n+=b;
        }
        else if(s[id]=='-'){
            id++;
            int b=cal(1);
            n-=b;
        }
        else if(s[id]=='*'){
            id++;
            int b=cal(0);
            n*=b;
        }
        if(s[id]==','||s[id]==')')return n;
    }
    return n;
}
```
## 第四題 機器出租
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=j608
### 解套
很像是經典的排程問題，只是有不只一個機器
### 題目分析
這題如果只看前兩組測資，會發現機器的數量$K$都是一，也就是一般常見的排程問題
這時常用的貪心解是將所有時間區段按照結束時間由小到大排序，若相同再按開始時間由大到小排序，最後再從頭遍歷，遇到能執行的活動就執行
~~唬爛的~~理由是因為工作越早結束代表越能趕快做下一個所以排前面，
而同樣結束時間下時間越短的(開頭越大)越可能讓前一項工作完成，故排前面

這時我們再考慮滿分解，也就是機器數$K$大於一的情況
原本的排序方式可以照用，但需要多考慮的是要怎麼安排不同機器的使用時間
這時我們發現，若考慮我們的排序方式，在所有能用的機器當中，用「上一次結束時間最接近的」不會比較差，畢竟更早結束的搞不好能處理後面更早開始的
實作上我們用C++內建的multiset，但考慮到要找「小於」的元素只能用「大於等於」的lower_bound找等於再往前推，感覺會出事，所以把它做一下轉換
因為題目說時間最大只有$10^8$，所以用$10^8$減每個數字就變成找「大於」的，就能用upper_bound了！
這樣時間複雜度$O(NlogN+NlogK)$
(後來看別人的題解才發現可以用lower_bound再把指標往前移，多判指標是不是開頭就好，但感覺有點麻煩，我也沒想到)
### 解題流程
讀入測資$→$排序$→$一次遍歷、處理$→$輸出
### 解題過程
#### define
```cpp=2
#define F first
#define S second
```
為了少打一些字
#### 變數宣告
```cpp=4
int n,k;
vector<pair<int,int>>v;
```
n,k與題目定義一樣，v儲存各活動的開始與結束時間
#### 自定義排序
```cpp=7
bool cmp(pair<int,int>a,pair<int,int>b){
    if(a.S!=b.S)return a.S<b.S;
    else return a.F>b.F;
}
```
按照前述條件進行排序
#### 輸入
```cpp=12
cin>>n>>k;
v.assign(n,{0,0});
for(int i=0;i<n;i++)cin>>v[i].F;
for(int i=0;i<n;i++)cin>>v[i].S;
```
其中第13行讓v大小變成n，且每個元素都是{0,0}
#### 排序
```cpp=16
sort(v.begin(),v.end(),cmp);
```
#### 遍歷、處理
```cpp=17
int ans=0;
multiset<int>st;
for(int i=0;i<k;i++)st.insert(1e8+1);
for(auto i:v){
    auto it=st.upper_bound(1e8-i.F);
    if(it!=st.end()){
        ans++;
        st.erase(it);
        st.insert(1e8-i.S);
    }
}
```
一開始先在multiset放k個$10^8-(-1)$，代表尚未執行任務的機器(上一次結束時間-1)
接下來遍歷(第20～27行)，每次用$10^8$減開始時間找upper_bound，若upper_bound存在代表任務可以執行，於是把答案加一，並用新的結束時間取代舊的結束時間(第24、25行)
#### 輸出
```cpp=28
cout<<ans;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
#define F first
#define S second
using namespace std;
int n,k;
vector<pair<int,int>>v;
bool cmp(pair<int,int>a,pair<int,int>b){
    if(a.S!=b.S)return a.S<b.S;
    else return a.F>b.F;
}
int main(){
    cin>>n>>k;
    v.assign(n,{0,0});
    for(int i=0;i<n;i++)cin>>v[i].F;
    for(int i=0;i<n;i++)cin>>v[i].S;
    sort(v.begin(),v.end(),cmp);
    int ans=0;
    multiset<int>st;
    for(int i=0;i<k;i++)st.insert(1e8+1);
    for(auto i:v){
        auto it=st.upper_bound(1e8-i.F);
        if(it!=st.end()){
            ans++;
            st.erase(it);
            st.insert(1e8-i.S);
        }
    }
    cout<<ans;
}
```
## 心得
這次APCS因為有事所以也沒有考，原本有點懊悔，但因為那陣子剛好在段考前，過得十分忙碌，所以後來想想沒去考或許就結果來說是比較好的
後來聽有去考的同學的說法，有的說這次「很簡單，跟送分一樣」，也有的說自己「死定了」，所以結論就是沒有結論...(可能就只是跟題目種類擅不擅長有關吧)

---
個人認為第一題的難度跟之前差不多，但第二題明顯感覺比之前簡單(除了輸出稍微複雜一點點以外沒有甚麼複雜的操作)，或許也能因此增加考試中想第三、四題的時間(我猜的)，這兩題基本上都蠻快寫出來的，也不需要甚麼特殊技巧

第三題時間主要是花在想$f$函數的處理方式，試了幾次才想到用逗點和右括弧當額外的中止條件，id的控制也花了一點時間思考。
個人還是習慣用遞迴，但聽說用stack好像可能比較有效率？(但我這題排行榜第一所以...我也不知道)
(更新：被擠下去了，但還在排行榜裡)

第四題一開始根本沒想法。
後來想了一下之後想起來$K=1$的Greedy解法，但機器多於一台還是不知道怎麼處理
後來只好去看別人的題解，知道了要找能用的機器中上一次結束時間最接近的
後來自己想到用$10^8$減活動時間來換成用upper_bound，也成功AC，但後來看別人的code才發現直接把指標往前移就好了，不過個人認為兩種做法都行

---
這次APCS讓我發現我Greedy還需要加強，畢竟只想得到以前寫過的題目的寫法。
雖然DP和Greedy某種程度上或許比較靠天分一點才能推(通)論(靈)出解法，但多練習應該能培養出解題的感覺，讓自己更快想出關鍵的步驟
而且它們都是APCS第四題很容易出的範圍，如果要拿五級分不弄懂不行
