---
# System prepended metadata

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

---

# APCS 2020年01月題解
## 第一題 猜拳
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=h026
### 解套
N/A
### 題目分析
直接按照題述進行相關操作
先輸入所有妹妹準備出的拳，接著從頭遍歷
每一輪先輸出哥哥要出的拳，接著判斷勝負，如果分出勝負就輸出並直接return 0結束程式
否則就照題述決定下一輪哥哥要出的拳，不過要注意如果是第一輪的話因為妹妹不可能"連續兩輪出一樣的拳"，所以不能判斷這一條(陣列會吃到邊界外)
最後如果能把所有迴圈跑完(沒有被結束)代表平手，輸出相關內容

時間複雜度$O(N)$
### 解題流程
輸入$→$遍歷、輸出
### 解題過程
#### 變數宣告
```cpp=3
int f,n,y[10];
```
意義與題述同
#### 輸入
```cpp=5
cin>>f>>n;
for(int i=0;i<n;i++)cin>>y[i];
```
#### 迴圈遍歷
```cpp=7
for(int i=0;i<n;i++){
    cout<<f<<" ";
    if(f==0&&y[i]==2||f==2&&y[i]==5||f==5&&y[i]==0){
        cout<<": Won at round "<<i+1;
        return 0;
    }
    else if(y[i]==0&&f==2||y[i]==2&&f==5||y[i]==5&&f==0){
        cout<<": Lost at round "<<i+1;
        return 0;
    }
    else{
        if(i>=1&&y[i-1]==y[i]){
            if(y[i]==0)f=5;
            else if(y[i]==2)f=0;
            else f=2;
        }
        else f=y[i];
    }
}
```
先輸出要出的拳(第8行)
接著判斷有沒有贏(第9-12行)以及有沒有輸(第13-16行)
其中第12、15行直接return 0結束main函式避免跑到後面的平手部分

最後按照規則決定下一輪要出的拳(第17-24行)
#### 平手的輸出
```cpp=26
cout<<": Drew at round "<<n;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int f,n,y[10];
int main(){
    cin>>f>>n;
    for(int i=0;i<n;i++)cin>>y[i];
    for(int i=0;i<n;i++){
        cout<<f<<" ";
        if(f==0&&y[i]==2||f==2&&y[i]==5||f==5&&y[i]==0){
            cout<<": Won at round "<<i+1;
            return 0;
        }
        else if(y[i]==0&&f==2||y[i]==2&&f==5||y[i]==5&&f==0){
            cout<<": Lost at round "<<i+1;
            return 0;
        }
        else{
            if(i>=1&&y[i-1]==y[i]){
                if(y[i]==0)f=5;
                else if(y[i]==2)f=0;
                else f=2;
            }
            else f=y[i];
        }
    }
    cout<<": Drew at round "<<n;
}
```
## 第二題 矩陣總和
### 題目敘述 
https://zerojudge.tw/ShowProblem?problemid=h027
### 解套
定義兩相同小矩陣的「距離」為相同位置但數字不同的位置數量
求一大矩陣所有子矩陣中與小矩陣距離不超過$r$的子矩陣個數，以及其中總和與小矩陣相差最小的值
(若無輸出-1)
### 題目分析
因為矩陣大小最多只到$10×100$，所以可以考慮暴力一點的作法
直接遍歷大矩陣中所有與小矩陣大小相同的子矩陣，計算距離與總和即可
實作上我是枚舉子矩陣的左上角(這樣就能透過小矩陣長寬推得邊界)
並額外寫一個計算距離與總和的函式calc，並用calc回傳的值更新答案

時間複雜度$O(nmst)$
### 解題流程
輸入$→$枚舉子矩陣、計算答案$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int s,t,n,m,r;
int small[10][100],big[10][100],ssum=0,cnt=0,ans=INT_MAX;
```
small存小矩陣，big存大矩陣，ssum存小矩陣元素和，cnt存要多少子矩陣距離不超過$r$，ans存最小距離差，其餘與題述同
#### calc函式
```cpp=5
pair<int,int> calc(int x1,int y1){
    int cnt=0,ans=0;
    for(int i=x1;i<=x1+s-1;i++)for(int j=y1;j<=y1+t-1;j++){
        ans+=big[i][j];
        if(big[i][j]!=small[i-x1][j-y1])cnt++;
    }
    return {cnt,ans};
}
```
參數x1,x2是子矩陣的左上角座標(第5行)
一開始宣告cnt,ans存距離與元素和(第6行)
接者遍歷子矩陣，計算cnt和ans(第7-10行)
最後以pair形式回傳cnt與ans(第11行)
#### 輸入
```cpp=14
cin>>s>>t>>n>>m>>r;
for(int i=0;i<s;i++)for(int j=0;j<t;j++){
    cin>>small[i][j];
    ssum+=small[i][j];
}
for(int i=0;i<n;i++)for(int j=0;j<m;j++)cin>>big[i][j];
```
第17行算小矩陣元素和
#### 枚舉子矩陣
```cpp=20
for(int i=0;i<=n-s;i++)for(int j=0;j<=m-t;j++){
    auto num=calc(i,j);
    if(num.first<=r){
        cnt++;
        ans=min(abs(num.second-ssum),ans);
    }
}
```
先用num存calc函式計算的值(第21行)
接著判斷距離是否不超過$r$，若是就更新cnt與ans(第22-25行)
#### 輸出
```cpp=27
cout<<cnt<<"\n";
if(cnt!=0)cout<<ans;
else cout<<-1;
```
輸出cnt與ans，其中若沒有符合條件的子矩陣就不輸出ans改輸出-1
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int s,t,n,m,r;
int small[10][100],big[10][100],ssum=0,cnt=0,ans=INT_MAX;
pair<int,int> calc(int x1,int y1){
    int cnt=0,ans=0;
    for(int i=x1;i<=x1+s-1;i++)for(int j=y1;j<=y1+t-1;j++){
        ans+=big[i][j];
        if(big[i][j]!=small[i-x1][j-y1])cnt++;
    }
    return {cnt,ans};
}
int main(){
    cin>>s>>t>>n>>m>>r;
    for(int i=0;i<s;i++)for(int j=0;j<t;j++){
        cin>>small[i][j];
        ssum+=small[i][j];
    }
    for(int i=0;i<n;i++)for(int j=0;j<m;j++)cin>>big[i][j];
    for(int i=0;i<=n-s;i++)for(int j=0;j<=m-t;j++){
        auto num=calc(i,j);
        if(num.first<=r){
            cnt++;
            ans=min(abs(num.second-ssum),ans);
        }
    }
    cout<<cnt<<"\n";
    if(cnt!=0)cout<<ans;
    else cout<<-1;
}
```
## 第三題 砍樹
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=h028
### 解套
N/A
### 題目分析
首先要先了解到一個重要的性質
> 無論砍樹的順序為何，最後能砍除的樹木是相同的

這個性質讓我們不必花時間思考「能砍掉最多樹木的方法」之類的，
只要最後有把能砍的樹木都砍完就行了
至於為什麼會這樣呢？因為題目有寫

知道順序不影響後，接下來就只要把該砍的樹都砍完就行了
這題的操作主要可以分成兩種：
判斷一棵樹可不可以砍，以及找到某棵樹的前一棵以及後一棵樹
判斷一棵樹能不能砍的部分比較簡單，只要一開始存好每棵樹的位置與高度，找到前後的樹再算一下就好了，所以本題的重點還是在於找到前後的樹

很明顯地一棵樹的前後會因為有樹被砍掉而有所改變，所以不能直接用原始輸入的順序判斷
我原本的想法是用set維護現存的樹已達到$O(logn)$查詢、刪除，在一年前也過了，但最近重寫發現時間限制被壓緊，所以只好想新的解法

後來發現可以直接用陣列紀錄一棵樹前後樹的編號，這樣就能$O(1)$查詢，
而且刪除的操作也算簡單，只要把「前一棵樹的下一棵樹」改成「自己的下一棵樹」、「下一棵樹的前一棵樹」改成「自己的前一棵樹」就能解決，如此就能避免真的把樹刪掉，節省時間

**前：**

| 樹編號 | 前一棵樹 | 後一棵樹 |
| -------- | -------- | -------- |
| 2 | 1 | 3 |
| 3 | 2 | 4 |
| 4 | 3 | 5 |

**後：** 
 
| 樹編號 | 前一棵樹 | 後一棵樹 |
| -------- | -------- | -------- |
| 2 | 1 | 4 |
| 3 | 2 | 4 |
| 4 | 2 | 5 |

實作上開一個二維陣列$loc[N][2]$，其中$loc[i][0]$、$loc[i][1]$分別代表編號為$i$的樹前一棵與後一棵的編號

於是就得到我們的解法：從左往右掃依次判斷每棵樹能不能砍(用$loc$加速)，如果砍成功就更新答案、$loc$，並**跳回到前一棵樹**，因為如果這棵樹砍了，空間就變大了，可能讓前一棵樹變得可以砍

最終複雜度應該是$O(n)$，因為每顆樹最多砍掉一次，且就總數來看頂多處理$2n$次
### 解題流程
輸入$→$初始化$→$砍樹(遍歷+回頭檢查)$→$輸出
### 解題過程
#### define
```cpp=2
#define pos first
#define h second
```
依照存的東西更改命名
#### 變數宣告
```cpp=5
int n,l,cnt=0,mx=0,loc[100000+10][2];
pair<int,int>arr[100000+10];
```
cnt砍了幾棵樹，mx存最高的能砍的樹，arr存每棵樹的位置與高度，loc同前述，n,l同題述
#### 輸入優化
```cpp=8
ios::sync_with_stdio(0);
cin.tie(0);
```
不加也會過
#### 輸入
```cpp=10
cin>>n>>l;
for(int i=1;i<=n;i++)cin>>arr[i].pos;
for(int i=1;i<=n;i++)cin>>arr[i].h;
```
#### 初始化
```cpp=13
arr[0]={1,0};
arr[n+1]={l,0};
for(int i=0;i<=n+1;i++){
    loc[i][0]=i-1;
    loc[i][1]=i+1;
}
```
為了方便操作在arr前後加上邊界(這題設$0～l$或$1～l$都會過)
#### 砍樹
```cpp=19
int it=1;
while(1){
    if(arr[it].pos-arr[it].h>=arr[loc[it][0]].pos||arr[it].pos+arr[it].h<=arr[loc[it][1]].pos){
        cnt++;
        mx=max(mx,arr[it].h);
        loc[loc[it][0]][1]=loc[it][1];
        loc[loc[it][1]][0]=loc[it][0];
        if(loc[it][0]!=0)it=loc[it][0];
        else it=loc[it][1];
    }
    else it=loc[it][1];
    if(it>n)break;
}
```
一開始初始化位置為1(第19行)
接著不斷重複以下操作：
先判斷一棵樹能不能砍(第21行)

如果可以的話就更新答案(第22.23行)，然後更新前後樹木的位置關係，最後更新it
需要注意的是如果it已經是第一棵樹了就不用往前，不然都要往前推一棵

如果不行就直接跳到下一棵樹

最後判斷如果已經到底了就終止迴圈
#### 輸出
```cpp=32
cout<<cnt<<"\n"<<mx;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
#define pos first
#define h second
using namespace std;
int n,l,cnt=0,mx=0,loc[100000+10][2];
pair<int,int>arr[100000+10];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>l;
    for(int i=1;i<=n;i++)cin>>arr[i].pos;
    for(int i=1;i<=n;i++)cin>>arr[i].h;
    arr[0]={1,0};
    arr[n+1]={l,0};
    for(int i=0;i<=n+1;i++){
        loc[i][0]=i-1;
        loc[i][1]=i+1;
    }
    int it=1;
    while(1){
        if(arr[it].pos-arr[it].h>=arr[loc[it][0]].pos||arr[it].pos+arr[it].h<=arr[loc[it][1]].pos){
            cnt++;
            mx=max(mx,arr[it].h);
            loc[loc[it][0]][1]=loc[it][1];
            loc[loc[it][1]][0]=loc[it][0];
            if(loc[it][0]!=0)it=loc[it][0];
            else it=loc[it][1];
        }
        else it=loc[it][1];
        if(it>n)break;
    }
    cout<<cnt<<"\n"<<mx;
}
```
## 第四題 貨物分配
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=h029
https://zerojudge.tw/ShowProblem?problemid=f163
(f163的測資範圍較大，要開多一點記憶體，接下來開的大小是根據h029，要丟f163麻煩自行調整)
### 解套
構造一個二元樹狀結構，從根往下加值，每次把數值加到值比較小的子樹上，
並輸出最後到達的葉節點編號
### 題目分析
說實話我不太確定這題究竟想要考什麼？
感覺應該是想考圖論中的二元樹，不過好像又只有用到存圖跟存點權的技巧，所以...

我的做法是用叫arr的pair陣列存每個節點的左右節點，並用叫w的int陣列存每個節點的重量
初始化重量時由下往上，若是貨艙就直接回傳原本的重量，否則把自身重量設為左右兩節點的值加起來再回傳
每次放貨物就從根節點往下遍歷，把重量加到比較輕的子樹上並更新自身重量，重覆遞迴到遇到葉節點為止(貨箱)，最後輸出到達的貨箱
為此需要兩個函式，分別是初始化重量的init函式以及放置貨物的put函式
另外題目有保證入口編號一定是1，所以不須判斷
時間複雜度$O(mn)$(如果是一條鍊的話)
### 解題流程
輸入、存圖$→$初始化二元樹$→$放貨物、輸出
### 解題過程
#### define
```cpp=2
#define L first
#define R second
```
讓自己寫起來比較順
#### 變數宣告
```cpp=5
int n,m,w[200000],goods[100],p,s,t;
pair<int,int>arr[100000];
```
n,m,p,s,t同題述，w,arr如前述，goods存每個貨物重量
#### init函式
```cpp=7
int init(int id){
   if(id>=n)return w[id];
   w[id]=init(arr[id].L)+init(arr[id].R);
   return w[id];
}
```
若編號大於n代表是貨艙，直接回傳重量(第8行)
不然把重量設為左右節點的重量和(要先各自初始化完)再回傳自身重量(第9-10行)
#### put函式
```cpp=12
void put(int good,int id){
    w[id]+=good;
    if(id>=n){
        cout<<id<<" ";
        return;
    }
    if(w[arr[id].L]<=w[arr[id].R])put(good,arr[id].L);
    else put(good,arr[id].R);
    return;
}
```
先更新自身重量(第13行)
若已抵達貨倉就直接輸出並結束(第14-17行)
不然就依照左右重量決定貨物放到哪裡，並回傳(第18-20行)
#### 輸入優化
```cpp=23
ios::sync_with_stdio(0);
cin.tie(0);
```
不加也會過
#### 輸入、存圖
```cpp=25
cin>>n>>m;
for(int i=n;i<2*n;i++)cin>>w[i];
for(int i=0;i<m;i++)cin>>goods[i];
for(int i=1;i<n;i++){
    cin>>p>>s>>t;
    arr[p]={s,t};
}
```
#### 初始化
```cpp=32
int a=init(1);
```
因為是int函式所以還是要把值存到某個地方去
#### 放貨物
```cpp=33
for(int i=0;i<m;i++)put(goods[i],1);
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
#define L first
#define R second
using namespace std;
int n,m,w[200000],goods[100],p,s,t;
pair<int,int>arr[100000];
int init(int id){
   if(id>=n)return w[id];
   w[id]=init(arr[id].L)+init(arr[id].R);
   return w[id];
}
void put(int good,int id){
    w[id]+=good;
    if(id>=n){
        cout<<id<<" ";
        return;
    }
    if(w[arr[id].L]<=w[arr[id].R])put(good,arr[id].L);
    else put(good,arr[id].R);
    return;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for(int i=n;i<2*n;i++)cin>>w[i];
    for(int i=0;i<m;i++)cin>>goods[i];
    for(int i=1;i<n;i++){
        cin>>p>>s>>t;
        arr[p]={s,t};
    }
    int a=init(1);
    for(int i=0;i<m;i++)put(goods[i],1);
}
```