# APCS 2020年10月題解
## 第一題 人力分配
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=f312
### 解套
N/A
### 題目分析
直接嘗試每一種組合,找到最大可能收益
時間複雜度$O(n)$
### 解題流程
輸入$→$枚舉$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int a1,b1,c1,a2,b2,c2,n;
```
變數名與題述相同
```cpp=6
int ans=INT_MIN;
```
一開始設答案為極小值
#### 輸入
```cpp=5
cin>>a1>>b1>>c1>>a2>>b2>>c2>>n;
```
#### 暴力枚舉
```cpp=7
for(int x1=0;x1<=n;x1++){
int x2=n-x1;
ans=max(ans,a1*x1*x1+b1*x1+c1+a2*x2*x2+b2*x2+c2);
}
```
枚舉所有x1與x2的可能,並更新答案
#### 輸出
```cpp=11
cout<<ans;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int a1,b1,c1,a2,b2,c2,n;
int main(){
cin>>a1>>b1>>c1>>a2>>b2>>c2>>n;
int ans=INT_MIN;
for(int x1=0;x1<=n;x1++){
int x2=n-x1;
ans=max(ans,a1*x1*x1+b1*x1+c1+a2*x2*x2+b2*x2+c2);
}
cout<<ans;
}
```
## 第二題 人口遷移
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=f313
### 解套
N/A
### 題目分析
直接按照題目說的規則處理即可,不過需要注意的是因為遷移是同步的,但我們的處理只能依序做,
所以遷移到其他地區的量先開另一個陣列math紀錄,所有地區都處理完後再加上去
(如果直接把遷移的量加上去會造成後處理的地區出錯)
另外因為題目保證$k\geq4$,所以不論如何人數都不會是負的,不用特別處理
處理遷移$m$次後再遍歷一次找最小值與最大值
時間複雜度$O(mrc)$
### 解題流程
輸入$→$模擬$→$遍歷$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int r,c,k,m;
int arr[50][50],math[50][50];
```
r,c,k,m與題述相同,arr存總人數,math存每次遷移暫時增加的人數
```cpp=42
int minans=INT_MAX,maxans=0;
```
初始化最小值與最大值
#### 輸入
```cpp=6
cin>>r>>c>>k>>m;
for(int i=0;i<r;i++)for(int j=0;j<c;j++)cin>>arr[i][j];
```
#### 處理遷移m次
```cpp=8
for(int l=0;l<m;l++){
for(int i=0;i<r;i++)for(int j=0;j<c;j++)math[i][j]=0;
for(int i=0;i<r;i++)for(int j=0;j<c;j++){
if(arr[i][j]==-1)continue;
int num=arr[i][j]/k;
int cnt=0;
if(i!=0){
if(arr[i-1][j]!=-1){
math[i-1][j]+=num;
cnt++;
}
}
if(j!=0){
if(arr[i][j-1]!=-1){
math[i][j-1]+=num;
cnt++;
}
}
if(i!=r-1){
if(arr[i+1][j]!=-1){
math[i+1][j]+=num;
cnt++;
}
}
if(j!=c-1){
if(arr[i][j+1]!=-1){
math[i][j+1]+=num;
cnt++;
}
}
arr[i][j]-=num*cnt;
}
for(int i=0;i<r;i++)for(int j=0;j<c;j++)if(arr[i][j]!=-1)arr[i][j]+=math[i][j];
}
```
一開始(第9行)先歸零math
接著遍歷所有位置(第10-39行)
如果該位置不是城市(值是$-1$)就跳過(第11行)
不然開兩個變數num與cnt紀錄要遷移的人數與共要遷移到幾個相鄰區域(第12、13行)
接著看上下左右四個相鄰區域是不是城市,如果是就把人移過去(math加num,cnt加1)(第14-37行)
四個都處理完後一次性減掉人數(第38行)
每次處理完所有位置再用math更新arr(第40行)
#### 找答案
```cpp=43
for(int i=0;i<r;i++)for(int j=0;j<c;j++){
if(arr[i][j]!=-1)minans=min(minans,arr[i][j]);
maxans=max(maxans,arr[i][j]);
}
```
遍歷所有區域更新答案
其中因為代表不是都市的-1會影響取最小值的操作,所以要額外判斷(第44行)
當然也可以一開始遇到-1就直接continue~~只是這樣會多一行~~
#### 輸出
```cpp=47
cout<<minans<<"\n"<<maxans;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int r,c,k,m;
int arr[50][50],math[50][50];
int main(){
cin>>r>>c>>k>>m;
for(int i=0;i<r;i++)for(int j=0;j<c;j++)cin>>arr[i][j];
for(int l=0;l<m;l++){
for(int i=0;i<r;i++)for(int j=0;j<c;j++)math[i][j]=0;
for(int i=0;i<r;i++)for(int j=0;j<c;j++){
if(arr[i][j]==-1)continue;
int num=arr[i][j]/k;
int cnt=0;
if(i!=0){
if(arr[i-1][j]!=-1){
math[i-1][j]+=num;
cnt++;
}
}
if(j!=0){
if(arr[i][j-1]!=-1){
math[i][j-1]+=num;
cnt++;
}
}
if(i!=r-1){
if(arr[i+1][j]!=-1){
math[i+1][j]+=num;
cnt++;
}
}
if(j!=c-1){
if(arr[i][j+1]!=-1){
math[i][j+1]+=num;
cnt++;
}
}
arr[i][j]-=num*cnt;
}
for(int i=0;i<r;i++)for(int j=0;j<c;j++)if(arr[i][j]!=-1)arr[i][j]+=math[i][j];
}
int minans=INT_MAX,maxans=0;
for(int i=0;i<r;i++)for(int j=0;j<c;j++){
if(arr[i][j]!=-1)minans=min(minans,arr[i][j]);
maxans=max(maxans,arr[i][j]);
}
cout<<minans<<"\n"<<maxans;
}
```
## 第三題 勇者修煉
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=f314
### 解套
N/A
### 題目分析
(為方便討論,設$arr[i][j]$為原本的值,$dp[i][j]$為考慮到第$i$行$j$列的情形)
(如果沒有dp基礎可以先去看[這裡](https://oi-wiki.org/dp/))
先把題目~~大幅~~簡化一下
假設從左上角開始,而且只能往右下方走,那要怎麼寫?
那就是常見的dp題目嘛,轉移式$dp[i][j]=max(dp[i-1][j],dp[i][j-1])+arr[i][j]$
那如果加上可從最上層任意位置開始要怎麼做?
只要在處理最上層時增加只考慮自己的可能就好了 $dp[0][j]=max(dp[0][j-1],0)+arr[0][j]$
最後處理完整的題目,即加上左右都能走的條件
因為由右往左跟由左往右可能會有不一樣的結果,取用上也不能互相通用
| 1 | 2 | 3 | 4 |
| - | - | - | - |
| 5 | 6 | 7 | 8 |
(假設要從7走到6,因為不能重複走所以只能考慮3→7→6和8→7→6而不能考慮6→7→6)
所以往右跟往左的dp要分開存
因此我們把dp的定義改成$dp[i][j][k]$,$k=0$代表從左邊走過來,$k=1$代表從右邊走過來
dp轉移式就變成$dp[i][j][0]=max({dp[i][j-1][0],dp[i-1][j][0],dp[i-1][j][1]})+arr[i][j]$
以及$dp[i][j][1]=max({dp[i][j+1][1],dp[i-1][j][0],dp[i-1][j][1]})+arr[i][j]$
初始化的部分則是$dp[0][0][0]=arr[0][0],dp[0][n-1][1]=arr[0][n-1]$,
最上面一列取max時不用考慮上面但要加上直接從自己開始的可能性,
且每一列最左和最右會缺少從右邊來和從左邊來的可能性
dp跑完後再掃最後一列,看最大值是多少並輸出
時間複雜度$O(mn)$
### 解題流程
輸入$→$動態規劃$→$遍歷$→$輸出
### 解題過程
#### 變數宣告
```cpp=3
int m,n,arr[50][10000],dp[50][10000][2]={};
```
變數名與前述、題述相同
```cpp=21
int ans=INT_MIN;
```
一開始設答案為極小值
#### 輸入
```cpp=5
cin>>m>>n;
for(int i=0;i<m;i++)for(int j=0;j<n;j++)cin>>arr[i][j];
```
#### 動態規劃
```cpp=7
dp[0][0][0]=arr[0][0];
dp[0][n-1][1]=arr[0][n-1];
for(int i=0;i<m;i++){
if(i==0){
for(int j=1;j<n;j++)dp[i][j][0]=max(dp[i][j-1][0],0)+arr[i][j];
for(int j=n-2;j>=0;j--)dp[i][j][1]=max(dp[i][j+1][1],0)+arr[i][j];
}
else{
dp[i][0][0]=max(dp[i-1][0][0],dp[i-1][0][1])+arr[i][0];
dp[i][n-1][1]=max(dp[i-1][n-1][1],dp[i-1][n-1][0])+arr[i][n-1];
for(int j=1;j<n;j++)dp[i][j][0]=max({dp[i][j-1][0],dp[i-1][j][0],dp[i-1][j][1]})+arr[i][j];
for(int j=n-2;j>=0;j--)dp[i][j][1]=max({dp[i][j+1][1],dp[i-1][j][0],dp[i-1][j][1]})+arr[i][j];
}
}
```
第7、8行初始化左上角與右上角
第10-13行初始化最上面一列
第15、16行計算每一列最左與最右一格
第17、18行計算剩下的格子
#### 找答案
```cpp=21
int ans=INT_MIN;
for(int i=0;i<n;i++)ans=max({ans,dp[m-1][i][0],dp[m-1][i][1]});
```
遍歷最後一列並更新答案
#### 輸出
```cpp=23
cout<<ans;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
using namespace std;
int m,n,arr[50][10000],dp[50][10000][2]={};
int main(){
cin>>m>>n;
for(int i=0;i<m;i++)for(int j=0;j<n;j++)cin>>arr[i][j];
dp[0][0][0]=arr[0][0];
dp[0][n-1][1]=arr[0][n-1];
for(int i=0;i<m;i++){
if(i==0){
for(int j=1;j<n;j++)dp[i][j][0]=max(dp[i][j-1][0],0)+arr[i][j];
for(int j=n-2;j>=0;j--)dp[i][j][1]=max(dp[i][j+1][1],0)+arr[i][j];
}
else{
dp[i][0][0]=max(dp[i-1][0][0],dp[i-1][0][1])+arr[i][0];
dp[i][n-1][1]=max(dp[i-1][n-1][1],dp[i-1][n-1][0])+arr[i][n-1];
for(int j=1;j<n;j++)dp[i][j][0]=max({dp[i][j-1][0],dp[i-1][j][0],dp[i-1][j][1]})+arr[i][j];
for(int j=n-2;j>=0;j--)dp[i][j][1]=max({dp[i][j+1][1],dp[i-1][j][0],dp[i-1][j][1]})+arr[i][j];
}
}
int ans=INT_MIN;
for(int i=0;i<n;i++)ans=max({ans,dp[m-1][i][0],dp[m-1][i][1]});
cout<<ans;
}
```
## 第四題 低地距離
### 題目敘述
https://zerojudge.tw/ShowProblem?problemid=f315
### 解套
題目寫得好清楚喔怎麼辦
定義$i$的低窪值為一陣列裡兩個數值為$i$的位置中間有幾個小於$i$的是數字,求所有低窪值總和
### 題目分析
首先這題如果暴力做的話時間複雜度會是$O(n^2)$,要執行$10^{10}$次運算,會超時
那究竟要怎麼做呢?
因為題目要的是兩相同數值間比他們小的,而因為從小到大具有規律性,
所以我們可以把題目稍微做一下轉換
如果我們把數字從小到大掃,並開一個新的陣列,每掃到一個數字就在他出現的兩個位置標記,就能動態紀錄比某數字小的數字出現位置
以題目的$[3,1,2,1,3,2]$為例
1. 掃到1
| 位置 | 1 | 2 | 3 | 4 | 5 | 6 |
| -- | - | - | - | - | - | - |
| 標記 | 0 | 1 | 0 | 1 | 0 | 0 |
2. 掃到2
| 位置 | 1 | 2 | 3 | 4 | 5 | 6 |
| -- | - | - | - | - | - | - |
| 標記 | 0 | 1 | 1 | 1 | 0 | 1 |
3. 掃到3
| 位置 | 1 | 2 | 3 | 4 | 5 | 6 |
| -- | - | - | - | - | - | - |
| 標記 | 1 | 1 | 1 | 1 | 1 | 1 |
就會發現一個數字的低窪值就是掃到他之前,他兩個位置之間的區間和!
例如數值3的位置是$1,5$,而掃完2後$[1,5]$的區間和是3,也就是數值3的低窪值
但是要怎麼做到動態更新數值與求區間和呢?答案是用**BIT**(樹狀數組)
他支持在$O(n)$時間初始化,$O(logn)$時間單點加值與求前綴和
BIT的詳細介紹在[這裡](https://oi-wiki.org/ds/fenwick/)(也可以用線段樹,但BIT寫起來比較簡潔)
於是就得到解法:先掃過一遍原始陣列得到各數值的出現位置,接著數字從小到大處理,先把答案加上出現位置之間的區間和再用出現位置更新BIT,最後輸出答案即可
時間複雜度$O(nlogn)$
### 解題流程
輸入$→$用BIT更新答案、操作區間$→$輸出
### 解題過程
#### define
```cpp=2
#define F first
#define S second
```
懶得打字
#### 變數宣告
```cpp=5
int n,a;
long long int ans=0;
pair<int,int>arr[100000+10];
```
a是輸入用的變數,ans是答案(題目說要long long),arr存個數值出現位置
#### BIT
```cpp=8
struct BIT{
int sz;
vector<int>dat;
void init(int n){
sz=n;
dat.assign(n+1,0);
return;
}
void upd(int id,int x){
for(int i=id;i<=sz;i+=i&-i)dat[i]+=x;
return;
}
int sum(int id){
int ans=0;
for(int i=id;i>0;i-=i&-i)ans+=dat[i];
return ans;
}
}bit;
```
#### 輸入、存位置
```cpp=27
cin>>n;
for(int i=1;i<=2*n;i++){
cin>>a;
if(arr[a].F==0)arr[a].F=i;
else arr[a].S=i;
}
```
第30、31行存數值出現的第一和第二個位置
#### 初始化BIT
```cpp=33
bit.init(2*n);
```
要記得大小是$2n$
#### 操作
```cpp=34
for(int i=1;i<=n;i++){
ans+=bit.sum(arr[i].S-1)-bit.sum(arr[i].F);
bit.upd(arr[i].F,1);
bit.upd(arr[i].S,1);
}
```
更新答案、BIT
#### 輸出
```cpp=39
cout<<ans;
```
### 完整程式碼
```cpp=1
#include<bits/stdc++.h>
#define F first
#define S second
using namespace std;
int n,a;
long long int ans=0;
pair<int,int>arr[100000+10];
struct BIT{
int sz;
vector<int>dat;
void init(int n){
sz=n;
dat.assign(n+1,0);
return;
}
void upd(int id,int x){
for(int i=id;i<=sz;i+=i&-i)dat[i]+=x;
return;
}
int sum(int id){
int ans=0;
for(int i=id;i>0;i-=i&-i)ans+=dat[i];
return ans;
}
}bit;
int main(){
cin>>n;
for(int i=1;i<=2*n;i++){
cin>>a;
if(arr[a].F==0)arr[a].F=i;
else arr[a].S=i;
}
bit.init(2*n);
for(int i=1;i<=n;i++){
ans+=bit.sum(arr[i].S-1)-bit.sum(arr[i].F);
bit.upd(arr[i].F,1);
bit.upd(arr[i].S,1);
}
cout<<ans;
}
```