---
# System prepended metadata

title: 算法模競2--題解 by yungyao

---

# 算法模競2--題解 by yungyao

難度錯估ㄉ部分我很抱歉，我低估題目難度ㄌ

## A Apples
就國小數學啦
主要是浮點數的應用

Subtask 1只要用`long long`儲存$a,b$，然後相除輸出就好

Subtask 2則要用`double`儲存，一樣相除輸出就好
由於我精度只要求到$10^{-4}$，所以不需要特別處理，輸出就好

不過還是小補充一下怎麼輸出高精度的浮點數
以C++來說，你會需要先引入標頭檔`iomanip`，然後利用`setprecision()`和`fixed`
大概像下面這樣
```cpp = 1
using namepace std;
#include <iostream>
#include <iomanip>

...

int main(){
    cout << fixed << setprecision(n);//放入你需要小數點下幾位
    double a;
    
    ...
    
    cout << a;
    
    return 0;
}

```

## B Map
裸的dsu題，就跟你們說會考ㄅ
反正就是好好做就好

### Subtask 1
這個Subtask應該很水
只要對每個query都dfs一次就好了

#### 賽後補充 by yungyao
DFS要好好寫QQ

### Subtask 2
這個Subtask則是可以在邊都增完後，跑一輪DFS
就可以知道每個點是否屬於同一個連通塊，然後就好好做

### Subtask 3
額，就DSU，好好做就好

::: spoiler AC code
```cpp = 1
using namespace std;

//IO
#include <iostream>
#define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0);

//workspace

int dsu[200200];

int query(int k){
    if (k == dsu[k])
        return k;
    return dsu[k] = query(dsu[k]);
}

inline void merge(int i,int j){
    dsu[query(i)] = dsu[query(j)];
}

int main(){
    theyRSOOOOOOOOODIAN
    int n,q;

    cin >> n;
    for (int i=1;i<=n;++i)
        dsu[i] = i;

    for (cin >> q;q--;){
        char c;
        int u,v;

        cin >> c >> u >> v;
        if (c == '1')
            merge(u,v);
        else
            cout << (query(u) == query(v) ? "yes\n" : "no\n");
    }

    return 0;
}
```
:::

#### 賽後補充 by youou
我明明跟鄭永堯講過測資有錯，然後他沒改= =

#### 賽後補充的補充 by yungyao
我有改辣，只是漏改了一個Subtask，我很抱歉

## C Investments
沒那麼裸的背包，這題原本是要出在考幹的，但後來換別題ㄌ，開ㄅ開心

一樣分Subtask講
### Subtask 1
$n \leq 10$，應該滿明顯的是枚舉就能過，然後你就發現常見的枚舉方式好像不太能做
直接標示取或不取好像不太行，因為取的順序有差

然而此時你可以融合兩個常見的枚舉方式：$2^n$表示狀態加上 $n!$ 表示順序(其實這叫做searching tree)
再對所有順序算出答案

不過直接乘起來好像又太大了，感覺不太可做，但其實不會，因為複雜度雖然是 $O(2^nn!)$
但實際上計算運算次數會是

$\sum\limits^{n}_{i=1}C^i_n \times i! \times i = \sum\limits^n_{i=1}\frac{n!}{(n-i)!}i$

在 $n = 10$ 時，這個神奇的算式會得到 $88776910$，大約小於 $10^8$，其實是一個不差的運算次數
不過你在賽中大概也懶得算那麼多啦，反正會過XD(其實我沒測就是ㄌ，但我猜不會有人刻著東西)

打那麼多，重點只是有時候演算法的常數可以小到不可思議，還有其他類似例子(譬如$O(n^3)$區間DP $n = 1000$經常可過)

### Subtask 2
這個Subtask你觀察一下就可以發現，他其實是裸的01背包問題
就...這樣吧，沒寫出來ㄉ再讀一下背包的slides

### Subtask 3
這個就比較有趣ㄌ，你會發現一般的背包問題我們基本上不考慮放置順序，可是這題似乎順序會影響答案
我們還是一樣可以把背包的思路放進這題，只要進行一些小修正就好了

首先我們只要先考慮$r_i - c_i > 0$的情況(Trivial)
接著也知道$dp[i][j] \geq dp[i-1][j]$
也就是說答案只會變大，因此我們只要對$c_i$由小到大排序
而只對 $dp[i-1][j- (r_i-c_i)] \geq c_i$ 的情況做事就好
剩下的部分就跟普通背包一樣ㄌ
100分get



:::spoiler AC code
```cpp = 1
using namespace std;
#include <algorithm>

//IO
#include <iostream>
#define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0);

//workspace

struct node{
    int p;
    int w,rq;
};

int main(){
    theyRSOOOOOOOOODIAN
    int n,k,h;
    int dp[10010]{};
    node stuffs[10010];

    cin >> n >> k >> h;
    for (int i=0;i<=k;++i)
        dp[i] = h;

    for (int i=0;i<n;++i){
        int p,c,r;

        cin >> p >> c >> r;
        stuffs[i] = {p,r-c,c};
    }
    sort (stuffs,stuffs+n,[](node a,node b){return a.rq < b.rq;});

    for (int i=0;i<n;++i){
        for (int j=k;j>=stuffs[i].p && dp[j-stuffs[i].p] >= stuffs[i].rq;--j){
            dp[j] = max(dp[j],dp[j-stuffs[i].p] + stuffs[i].w);
        }
    }

    cout << dp[k];

    return 0;
}
```
:::

比較可惜的部分是開$10000 \times 10000$的二維陣列會爆炸
所以必須要滾動，但我可能沒有講好滾動的部分，所以有些人就這樣MLE的不明不白，非常ㄅ歉

#### ㄛ其實
這題是抄來ㄉ，原題是 [TIOJ 1675](https://tioj.ck.tp.edu.tw/problems/1675)

## D Love
#### Subtask 1
痾，線段樹砸下去。

#### Subtask 2
痾，單點修改砸下去。

#### Subtask 3
痾，暴力的單點修改砸下去。20只是常數。欸不是這樣有90分欸。

#### Subtask 4
可能大家都不想寫這題(?)，我就直接暴雷好了。

首先，gcd有個神奇的性質: $\gcd(a, b) = \gcd(a-b, b)$，

我們可以把它推廣到 $\gcd(a_l, a_{l+1}, ..., a_r) = \gcd(\gcd(a_{l+1} - a_l, a_{l+2} - a_{l+1}, ..., a_r - a_{r-1}), a_r)$

還記得差分嗎? 對於後面那項 $a_r$，我們可以用差分做區間修改、單點查詢的BIT，
前面那項就用單點修改、區間查詢gcd的線段樹就好了。

複雜度 $\mathcal{O}(q \log n)$。

btw 這題是[TIOJ 1282](https://tioj.ck.tp.edu.tw/problems/1282)。

## E Knapsack
當你以為這是背包問題ㄉ時候，你就錯ㄌ，其實是裸的MST
阿跟DSU那題一樣，好好做就好

:::spoiler AC code
```cpp = 1
using namespace std;
#include <algorithm>

//IO
#include <iostream>
#define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0);

//workspace

struct Node{
    int u,v;
    int w;
}edge[200200];

int dsu[100100];

int query(int k){
    return dsu[k] == k ? dsu[k] : dsu[k] = query(dsu[k]);
}

void modify(int i,int j){
    dsu[query(i)] = query(j);
}

int maxEdge[100100];

int main(){
    theyRSOOOOOOOOODIAN
    int n,m;

    cin >> n >> m;
    for (int i=1;i<=n;++i)
        dsu[i] = i;
    for (int i=0;i<m;++i)
        cin >> edge[i].u >> edge[i].v >> edge[i].w;
    
    sort (edge,edge+m,[](Node a,Node b){return a.w < b.w;});
    int e = 0;
    for (int i=0;i<m&&e<n-1;++e){
        while (i < m && query(edge[i].u) == query(edge[i].v))
            ++i;
        if (i < m){
            modify (edge[i].u,edge[i].v);
            maxEdge[edge[i].u] = max(maxEdge[edge[i].u],edge[i].w);
            maxEdge[edge[i].v] = max(maxEdge[edge[i].v],edge[i].w);
        }
    }
    for (int i=1;i<=n;++i)
        cout << maxEdge[i] << ' ';
    
    return 0;
}
```
:::

### Subtask 1
特別講一下這個Subtask，你會發現既然 $m = n - 1$，又保證圖連通
代表這張圖一定是一棵樹
而樹的最小生成樹就是整棵樹
所以輸出每個點最大的邊就好ㄌ

## F Answers

本次預期跟D並列第二大難題
不過理論上這題會再難一點
需要多一點競賽經驗比較做的出來，所以你們現階段想不到我滿可以理解的

### Subtask 1
這個Subtask理論上超級好做的，沒有人去寫實在很可惜
在左界都是1的情況下，你只需要對右界做前綴和就好了

而查詢都在修改後面表示你只需要存每個 $r$ 有多少答案
然後再做前綴和，查詢時只要回答 $r$ 的前綴和是多少就好了

### Subtask 2
跟上一個Subtask類似，但修改跟查詢輪替的情況下
你就得用BIT去做(或線段樹)
也是實作題而已，只有一個人寫也是偏可惜

:::spoiler 80 points get
```cpp = 1
using namespace std;

//IO
#include <iostream>
#define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0);

//workspace

int bit[3030];

void modify(int k,int val){
    for (;k<=3000;k+=k&-k)
        bit[k] += val;
}

int query(int k){
    int r = 0;
    for (;k;k-=k&-k)
        r += bit[k];
    return r;
}

int main(){
    theyRSOOOOOOOOODIAN
    int q;

    for (cin >> q;q--;){
        char o;

        cin >> o;
        if (o == '1'){
            int u,v,w;

            cin >> u >> v >> w;
            modify (v,w);
        }
        else{
            int l,r;

            cin >> l >> r;
            cout << query(r) << '\n';
        }
    }

    return 0;
}
```
:::

### Subtask 3
這個Subtask跟pD的最後一個一樣，寫不出來正常啦
簡單來說我們可以畫一張表格，把左右界分別映射到x/y平面上
下面以縱軸為左界，橫軸為右界

那我們假設在 $[3,5]$ 有了一組解 (v 的位置)
![](https://i.imgur.com/bof0d6S.png)

那會讓用x劃記的都可以用到這組解(也就是所有左界$\leq l$，右界$\geq r$的區間)
那如果換個角度想查詢的狀態
假設要查詢 $[4,6]$ 這個區間 (v 的位置)

![](https://i.imgur.com/NQj6rBH.png)
那所有被x標記的區間都必須被算到 (X標記的區間則是不合法的區間，因為 $l > r$)
也就是X軸前綴、Y軸後綴和，你就可以發現，我們可以用一個二維BIT去維護這個東西ㄌ
但要注意一個維度是前綴，另一個是後綴，所以小心不要混淆

:::spoiler AC code
```cpp = 1
using namespace std;

//IO
#include <iostream>
#define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0);

//workspace

int bit[3001][3001];

inline int query(int x,int y){
    int val = 0;
    for (int i=x;i<=3000;i+=i&-i)
        for (int j=y;j;j-=j&-j)
            val += bit[i][j];
    return val;
}

inline void modify(int x,int y,int val){
    for (int i=x;i;i-=i&-i)
        for (int j=y;j<=3000;j+=j&-j)
            bit[i][j] += val;
}

int main(){
    theyRSOOOOOOOOODIAN
    int q;

    cin >> q;
    while (q--){
        char c;
        int l,r,w;

        cin >> c;
        if (c == '1'){
            cin >> l >> r >> w;
            modify(l,r,w);
        }
        else{
            cin >> l >> r;
            cout << query(l,r) << '\n';
        }
    }

    return 0;
}
```
:::

---

整體來說，大家都很棒辣
接下來上機考加油ㄛ~~~