SCIST 110 演算法季後賽題解
===
## A. 背包問題(knapsack)
### 子任務 1
> $k=1$
洽選一個蘋果,則檢查 $s$ 是否在 $1, 2, \dots, n$。
### 子任務 2
> $n \le 15$
$O(2^n)$ 枚舉每個蘋果選或不選,再檢查是否符合題目要求。
### 子任務 3
> $n \le 100$
直接做背包。以 $dp[i][j][x]$ 代表在蘋果 $1 \dots i$ 中選 $j$ 個,是否有可能花 $x$ 元。
最後一維可以用 bitset 壓掉,$O(\frac{n^4}{32})$
### 子任務 4
> 題目範圍限制
在 $1 \dots n$ 中選 $k$ 個,最小的可能是選 $1, 2, \dots, k$,總價為 $m = (1+k)\frac{k}{2}$;最大的可能是選 $n-k+1, \dots, n-1, n$,總價為 $M = (2n-k+1)\frac{k}{2}$。
可以證明,$m, m+1, \dots, M$ 的每個價格,都可以組合出來。
:::info
不嚴謹的證明:
先採用最小選法,也就是 $1, 2, \dots, k$ ,當 $s$ 比現在的選法大,我們可以將最大、右邊一格沒被選中的數字,改選擇右邊一格。
例如 $n=8, k=4$,現在的選法為:$[1, 2, 5, 8]$。我們可以將 $5$ 改成 $6$,也就是 $[1, 2, 6, 8]$,總和就會加一。
我們可以一直做這件事情直到我們選擇了 $n-k+1, \dots, n-1, n$。
因此若 $m \le s \le M$,我們總是有存在至少一種方法使得總和為 $s$。
:::
#### 參考程式碼
```cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, s, k;
signed main()
{
cin >> n >> s >> k;
cout << (((1+k)*k/2)<=s && s<=((n+(n-k+1))*k/2)? "Frank":"Jonason") << '\n';
}
```
## B. 選手指派 (Player Assignment)
### 子任務 1
> $N \le 20$
以 $O(2^n)$ 枚舉該場比賽是由 Colten 或是 Gino 出賽。
### 子任務 2
> 題目範圍限制
先假設全部由 Colten 出賽,此時表現度為 $a_1, a_2, \dots, a_n$。
若第 $i$ 場比賽改由 Gino 出賽,則表現度會增加 $b_i - a_i$。
求出 $b_1-a_1, b_2-a_2, \dots, b_n-a_n$,從中取出前 $N-K$ 個改由 Gino 出賽即可。
複雜度為排序的 $O(nlogn)$
## C. 王警官已介入調查(cming)
### 子任務 1
> 不存在迴路
無向簡單圖不存在迴路則 $n$ 個節點是一個連通塊當且僅當 $m = n-1$。
### 子任務 2
> $n \le 100$
將邊依照時間做排序,依序加進圖並 dfs 檢查是否為只有一個連通塊。每次檢查 $O(n+m)$,時間 $O(m(n+m))$。
### 子任務 3
> $w = 1$
所有邊都是在同時間建成的,則直接 $O(n+m)$ 檢查是否只有一個連通塊即可。
### 子任務 4
子任務 2 的作法每次花 $O(n+m)$ 檢查是否為一個連通塊。我們改用「並查集」維護目前所有的連通塊,每次使用接近 $O(1)$ 的時間就可以插入邊。
最一開始共有 $n$ 個連通塊。若該次插入邊 $u, v$ 本屬不同連通塊,則連通塊數量少一,因此我們可以知道每次插入後還有多少連通塊。
#### 參考程式碼
```cpp
#include <bits/stdc++.h>
#define rep(i,j,k) for (int i=j; i<=k; i++)
#define ff first
#define ss second
#define mothersrosario ios::sync_with_stdio(0), cin.tie(0);
using namespace std;
typedef pair<int,int> pii;
const int N = 2000, M = N*N;
int n, m;
pair<int,pii> edgs[M];
int p[N];
int find(int x) { return p[x]==x? x:p[x]=find(p[x]); }
signed main()
{
cin >> n >> m;
rep(i,1,m) cin >> edgs[i].ss.ff >> edgs[i].ss.ss >> edgs[i].ff;
sort(edgs+1, edgs+1+m);
rep(i,1,n) p[i] = i;
int cnt = n;
rep(i,1,m)
{
if (find(edgs[i].ss.ff) == find(edgs[i].ss.ss))
continue;
else
p[find(edgs[i].ss.ff)] = find(edgs[i].ss.ss), cnt--;
if (cnt == 1)
{
cout << edgs[i].ff << '\n';
return 0;
}
}
cout << -1 << '\n';
}
```
## D. Gino 種竹子 (Planting Bamboo)
`tags: DFS, Brute Force`
### 子任務 1
> $1 \le n \le 2$, 不會有格子已經種植作物了
可以直接手算所有的可能性,會發現答案其實可以直接使用 if-else 簡單的計算完畢
#### Case 1: $n=1$
只有在 $k=1$ 時有答案,答案為 $1$,否則皆為 $0$
#### Case 2: $n=2, k \le 3$
答案皆為 $4$
#### Case 3: $n=2, k=4$
答案為 $1$
### 子任務 2
> 不會有格子已經種植作物了
本子題是留給寫不出能在時限內跑完的滿分解,或者不會障礙物的 Case 時可以撈的子任務
#### 預設解法:
暴力生成所有 $n \times n$ 且放置了 $k$ 個竹子的盤面,接著檢查是否滿足竹子皆連通
#### 總共有幾種不同狀態?
在 $n = 8$ 且 $k = 8$,如果直接暴力 DFS 找所有放置了 $8$ 個竹子的盤面,再去檢查答案
一共會有 $C^{64}_8 = 4426165368 \approx 4 \times 10^9$ 種答案
無法在時限內跑完!
#### 本機建表
既然狀態數量這個多,但實際上會詢問的可能性只有 $8 \times 8 = 64$ 種
那我們直接在本機使用這個做法,跑過所有 $n \le 8, \ k \le 8$ 的答案並記錄下來
接著我們遇到詢問,就可以直接回答了!
### 滿分解
#### 目前的困難點
所有 $n \times n$ 且放置 $k$ 個竹子的盤面太多了?
#### 解決方式
事實上,如果在 DFS 的過程,我們就只維護所有竹子都相連的情況
則總方案數並不會太大,是能夠在 $1$ 秒內跑完的
#### 實作細節
可以另外使用一個 set 等等的 STL,幫忙維護已經走過的盤面
在 DFS 時,很多人的方式是從 $(0,0) \sim (0,n-1) \sim (1,0) \sim (7,7)$ 這樣枚舉
而這樣的枚舉方式,不見得能使放下去的竹子相連 (可能要額外進行判斷
#### 參考程式碼
```cpp=
#include <bits/stdc++.h>
#define int long long
#define fastio ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
using namespace std;
const int N = 10;
set<vector<string>> used;
vector<string> grid;
int n, k, ans;
void dfs(int num){
if(used.find(grid)!=used.end()){
return;
}
used.insert(grid);
if(num==0){
ans++;
return;
}
vector<pair<int,int>> nxt;
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
if(grid[i][j]=='.'){
bool ok = false;
for(auto dx : {-1,0,1}){
for(auto dy : {-1,0,1}){
if(abs(dx)==abs(dy)) continue;
if(i+dx < 0 || i+dx >= n || j+dy < 0 || j+dy >= n) continue;
if(grid[i+dx][j+dy]=='*'){
ok = true;
}
}
}
if(ok){
nxt.push_back({i,j});
}
}
}
}
for(auto [nx,ny] : nxt){
grid[nx][ny] = '*';
dfs(num-1);
grid[nx][ny] = '.';
}
}
signed main(){
fastio
cin >> n >> k;
for(int i = 0; i < n; i++) {
string str;
cin >> str;
grid.push_back(str);
}
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
if(grid[i][j]=='.'){
grid[i][j] = '*';
dfs(k-1);
grid[i][j] = '.';
}
}
}
cout << ans << "\n";
}
```
## E. 企鵝回家記 (Go home)
### 子任務 1:$n \le 10, m = 1$
直接搜哪一格不會被打就好
### 子任務 2:$n \le 800, m \le 500$
我們可以設 $dp[i][j]$ 為第 $j$ 個時間點時更改 $i$ 次的最少被打次數,look(t) 代表第 $t$ 個時間點 Zhu 會不會看訊息
那麼就可以推出轉移式:$dp[i][j] = \min(\min_{l < j}(dp[i][l]), \min_{l \le j-k}(dp[i - 1][k]) + look(j))$
共有 $nm$ 個狀態,每次轉移 $\mathcal{O}(n)$,時間複雜度 $\mathcal{O}(n^2m)$
### 子任務 3:$n \le 10^4, m \le 1000$
將狀態改為 $dp[i][j]$ 為時間點 $1 \sim j$ 更改 $i$ 次的最少被打次數
轉移式變為:$dp[i][j] = \min(dp[i - 1][j - k] + look(j), dp[i][j - 1])$
時間複雜度:$\mathcal{O}(nm)$
#### 參考程式:
```cpp=
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
#define MAXN 50000
#define MAXM 5000
int n, m, k, q;
int a[MAXN], pre[MAXN], b;
int dp[MAXM + 1][MAXN + 1];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> k >> q;
for (int i = 0; i < q; i++)
cin >> a[i];
cin >> b;
for (int i = 0; i < q; i++) {
pre[a[i]]++;
pre[a[i] + b]--;
}
for (int i = 1; i <= n; i++)
pre[i] = pre[i - 1] + pre[i];
memset(dp, 0x3f, sizeof(dp));
dp[0][0] = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j <= n - k; j++) {
dp[(i + 1)][j + k] = dp[i][j] + pre[j + k];
dp[(i + 1)][j + k] = min(dp[(i + 1)][j + k], dp[(i + 1)][j + k - 1]);
dp[i][j + 1] = min(dp[i][j + 1], dp[i][j]);
}
}
cout << dp[m][n] << endl;
return 0;
}
```
### 子任務 4:$n \le 5 \times 10^4, m \le 5000$
用子任務 3 的方法會 MLE,將 dp 陣列換成滾動,開 dp[2][n],對於每個 i 用 dp[i % 2] 做
空間複雜度:$\mathcal{O}(n)$
### 參考程式:
```cpp=
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
#define MAXN 50000
#define MAXM 5000
int n, m, k, q;
int a[MAXN], pre[MAXN], b;
int dp[2][MAXN + 1];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> k >> q;
for (int i = 0; i < q; i++)
cin >> a[i];
cin >> b;
for (int i = 0; i < q; i++) {
pre[a[i]]++;
pre[a[i] + b]--;
}
for (int i = 1; i <= n; i++)
pre[i] = pre[i - 1] + pre[i];
memset(dp, 0x3f, sizeof(dp));
dp[0][0] = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j <= n - k; j++) {
dp[(i + 1) % 2][j + k] = dp[i % 2][j] + pre[j + k];
dp[(i + 1) % 2][j + k] = min(dp[(i + 1) % 2][j + k], dp[(i + 1) % 2][j + k - 1]);
dp[i % 2][j + 1] = min(dp[i % 2][j + 1], dp[i % 2][j]);
}
for (int j = 0; j <= n; j++)
dp[i % 2][j] = 1e9;
}
cout << dp[m % 2][n] << endl;
return 0;
}
```