---
# System prepended metadata

title: SCIST 110 演算法季後賽題解

---

SCIST 110 演算法季後賽題解
===

## A. 背包問題（knapsack）

### 子任務 1

> $k=1$

洽選一個蘋果，則檢查 $s$ 是否在 $1, 2, \dots, n$。

### 子任務 ２

> $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;
}
```

