---
# System prepended metadata

title: '2021 高階競程 Contest #1 - 題解'
tags: [成大高階競技程式設計 2021]

---

---
tags: 成大高階競技程式設計 2021
image: https://i.imgur.com/IIj9BcL.png
---

# 2021 高階競程 Contest #1 - 題解

## [鮪魚吃蛋糕](https://judge.csie.ncku.edu.tw/problems/89)
- Task Provider: leo
- Task Setter: leo

### 首殺 team21_24 (00:13)

### 解法說明

你可以把所有蛋糕依照好吃度 $d$ 與價錢 $c$ 的比值，由大到小排序。
接著依序拿蛋糕直到錢不夠為止。
時間複雜度為排序的 $O(N\log N)$。

實作上需要特別注意一些問題：

- 雖然這題因為是浮點數除法，基本上不太會遇到這問題，
    不過在寫根據比值排序的 compare 時，
    儘量不要用除的，因為分母有可能為 $0$，下方提供範例。
    浮點數則因為除以 $0$ 會變成 `inf` 所以不太會出問題，
    不過還是可能被一些浮點數精度的問題影響到。
    :::spoiler 範例
    ```cpp
    struct cake{
        int d, c;
    };

    // Suggest
    bool compare1(const cake &a, const cake &b){
        return (long long)a.d * b.c > (long long)b.d * a.c;
    }

    // Not suggest
    bool compare2(const cake &a, const cake &b){
        return (double)a.d / a.c > (double)b.d / b.c;
    }
    ```
    :::
    <br>
- 因為題目採用相對誤差的比對方式，
    所以其實可以儘量輸出很多位數，以求較精確的輸出，
    請參考下方標準程式
- 如果 $K$ 為 $0$ 時不能直接跳出去，因為蛋糕可能也是 $0$ 元
    
如果題目改成蛋糕不可分割的話，這種依照 CP 值的 greedy 方法是不行的，
詳細原因與解法會在第七週的課程教到，如果有興趣的話可以先自己想想看為什麼。

### 標準程式
:::spoiler
```cpp
#include <iostream>
#include <iomanip>
#include <algorithm>
using namespace std;

struct cake{
    int d, c;
    bool operator<(const cake &a)const{
        return (long long)d * a.c > (long long)a.d * c;
    }
} a[200000];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    long long n, k;
    double ans = 0;
    cin >> n >> k;
    for(int i = 0; i < n; i++)
        cin >> a[i].d >> a[i].c;
    sort(a, a + n);
    for(int i = 0; i < n && (k > 0 || a[i].c == 0); k -= a[i].c, i++)
        if(k >= a[i].c || a[i].c == 0)
            ans += a[i].d;
        else
            ans += a[i].d * (double(k) / a[i].c);
    cout << fixed << setprecision(12) << ans << "\n";
}
```
:::

## [零二](https://judge.csie.ncku.edu.tw/problems/90)
- Task Provider: arashi87
- Task Setter: arashi87

### 首殺 team21_34 (00:38)

### 解法說明
題目要求你將 $N$ 分解為 $K$ 個二的幂次，其實就是單純的二進制分解而已，我們可以很容易的知道下面兩種情況會無法分解。
- 第一種是 $K \gt N$，當發生這種情況即使將 $N$ 通通分解成 $2^0$ 也會不夠
- 第二種是當 $N$ 轉換的二進制裡 `1` 的個數大於 $K$ 時也會無法分解，因為能夠分解的最少數字，其數量已經大於 $K$ 了。

而剩下就是如何分解而已，這邊我們利用 priority queue 可以自動找出最大值的功能，先將 $N$ 做二進制分解並將分解出來的數字都放進 `pq` 裡，如果數量小於 $K$ 的話就從 `pq` 取出最大的數除以 $2$，生成兩個數字，並且放回 `pq`，這樣做一次可以使得 $pq.size$ 增加 $1$，如此只需要一直重複直到 $pq.size = K$。時間複雜度為 priority queue 單次操作的 $O(\log K)$ 乘上總操作次數 $O(K)$，複雜度為 $O(K\log K)$。

總複雜度為分解 $N$ 的 $O(\log N)$ 加上後面 priority queue 的 $O(K\log K)$，因此為 $O(\log N+K\log K)$。

下方的解法 2 是複雜度 $O(K+\log N)$ 的做法，比起上面的解法又更快了一點，有興趣的可以自己研究看看。

### 標準程式
:::spoiler 解法 1
```cpp
#include <queue>
#include <stdio.h>
using namespace std;

int n, k, base = 1;
priority_queue<int> pq;

int main() {
    scanf("%d%d", &n, &k);
    if (k > n) return puts("NO"), 0;
    while (n) {
        if (n & 1) pq.push(base);
        n >>= 1, base <<= 1;
    }

    if (pq.size() > k) return puts("NO"), 0;
    while (pq.size() < k) {
        int tmp = pq.top(); pq.pop();
        pq.push(tmp >> 1), pq.push(tmp >> 1);
    }

    puts("YES");
    while(!pq.empty())
        printf("%d ", pq.top()), pq.pop();
    printf("\n");
}

```
:::
:::spoiler 解法 2
```cpp
#include <iostream>
using namespace std;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    long long n, k, a[64] = {}, cnt = 0;
    bool flag = 0;
    cin >> n >> k;
    for(long long i = 0, tmp = 1; i < 64; tmp <<= 1, i++)
        if(tmp & n)
            a[i] = 1, cnt++;
    if(cnt > k || n < k)
        return cout << "NO\n", 0;
    for(int i = 63; i > 0; i--)
        if(cnt < k){
            int tmp = min(k - cnt, a[i]);
            a[i] -= tmp;
            a[i - 1] += tmp * 2;
            cnt += tmp;
        }
    cout << "YES\n";
    for(long long i = 0, tmp = 1; i < 64; tmp <<= 1, i++)
        for(int j = 0; j < a[i]; j++){
            if(flag)
                cout << " ";
            else
                flag = 1;
            cout << tmp;
        }
    cout << "\n";
}
```
:::

## [BNT](https://judge.csie.ncku.edu.tw/problems/91)
- Task Provider: D4nnyLee
- Task Setter: D4nnyLee

### 首殺 team21_12 (00:02)

### 解法說明

#### Subtask 1 $O(N^3)$

因為這個子任務 $N$ 最大只有到 $100$，因此我們可以直接枚舉所有可能的長度為 $3$ 的子序列，並算出當中有多少是 BNT。

:::spoiler 參考程式
```cpp
#include <iostream>
using namespace std;

int main(void) {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    string s;
    cin >> N >> s;

    int ans{0};
    for (int i{0}; i < N; ++i)
        for (int j{i + 1}; j < N; ++j)
            for (int k{j + 1}; k < N; ++k) 
                if (s[i] == 'B' && s[j] == 'N' && s[k] == 'T')
                    ++ans;

    cout << ans << '\n';

    return 0;
}

```
:::

#### Subtask 2 $O(N)$

#### 解法 1

我們枚舉所有的 N，對於每個找到的 N，若左邊有 $x$ 個 B 以及 $y$ 個 T，從左邊任意取一個 B 再從右邊任意取一個 T 就可以組成一個 BNT 子序列，因此對於這個 N，我們可以得到 $x \cdot y$ 個 BNT 子序列。

#### 解法 2

我們定義一個長度為 $3$ 的陣列 $cnt$，每個元素代表的意義如下：

* $cnt_0$ 代表目前遇到的 B 的個數
* $cnt_1$ 代表目前遇到的 BN 的個數
* $cnt_2$ 代表目前遇到的 BNT 的個數

因此我們只要把 $s$ 從左到右跑一次，並且維護 $cnt$ 的元素，最後的 $cnt_2$ 就是我們要的答案。

> 維護方法：
> * 每個 N 都可以接在目前遇到的 B 的後面，形成 $cnt_0$ 個 BN
> * 每個 T 都可以接在目前遇到的 BN 的後面，形成 $cnt_1$ 個 BNT

要注意答案可能會超出 `int` 的範圍，因此型別需要設為 `long long`。

### 標準程式

:::spoiler 解法 1
```cpp
#include <iostream>
using namespace std;

int main(void) {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    string s;
    cin >> N >> s;

    long long x{0}, y{0}, ans{0};
    for (int i{0}; i < N; ++i)
        if (s[i] == 'T')
            ++y;

    for (int i{0}; i < N; ++i) {
        if (s[i] == 'B') ++x;
        else if (s[i] == 'T') --y;
        else ans += x * y;  // s[i] == 'N'
    }

    cout << ans << '\n';

    return 0;
}

```
:::

:::spoiler 解法 2
```cpp
#include <iostream>
#include <array>
using namespace std;

int main(void) {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    string s;
    cin >> N >> s;

    array<long long, 3> cnt{};
    for (int i{0}; i < N; ++i) {
        if (s[i] == 'B')
            ++cnt[0];
        else if (s[i] == 'N')
            cnt[1] += cnt[0];
        else  // s[i] == 'T'
            cnt[2] += cnt[1];
    }

    cout << cnt[2] << '\n';

    return 0;
}
```
:::

## [遞增數列](https://judge.csie.ncku.edu.tw/problems/92)
- Task Provider: ys
- Task Setter: ys

### 首殺 	team21_18 (00:26)

### 解法說明
#### 解法 1
設 $p(n, m)$ 表示上限為 $n$ 且長度為 $m$ 的遞增數列個數

首先觀察到，長度 $m$ 可由長度 $m-1$ 的遞增數列推得，其關係式為：
$$
p(n, m) = p(n, m-1) + p(n-1, m-1) + \cdots + p(1, m-1)
$$

例如 $n = 2, m = 3$ 的遞增數列 $a$ 可分為
- $a_3 = 2$：
$\textbf{2, 2}, 2$
$\textbf{1, 2}, 2$
$\textbf{1, 1}, 2$
- $a_3 = 1$：
$\textbf{1, 1}, 1$

觀察得知 $p(2, 3)$ 有以下關係式
$$
p(2, 3) = p(2, 2) + p(1, 2)
$$
> 將尾端 $a_3$ 設為 $2$ 時，$a_1, a_2$ 的可能數為 $p(2, 2)$；
將尾端 $a_3$ 設為 $1$ 時，$a_1, a_2$ 的可能數為 $p(1, 2)$

所以欲求 $p(n, m)$，
只需要依序將 $p(1, 1), p(2, 1), \cdots , p(n, 1)$ 求出來，
再將 $p(1, 2), p(2, 2), \cdots , p(n, 2)$ 求出來，
依此類推，就能得到題目所需的目標答案

實作上由於結果相當龐大，做加法時會除餘要求的 $10^9 + 7$ 如 `a = (b + c) % 1000000007`
並且設邊界 $\forall n. p(n, 0) = 1$，因為 $m = 0$ 的時候只有**一種**可能


#### 解法 2
$n-1+m$ 個格子**任選** $m$ 個格子，令**未選到的格子**為隔板
選到的格子填上數字，規則如下：
- 不超過第 $1$ 個隔板之前為 $1$
- 第 $k-1$ 個到第 $k$ 個隔板之間為 $k$
- 超過第 $n-1$ 個隔板之後為 $n$

例如 $n = 2, m = 3$ 的格子為 `_,_,_,_`，有以下可能選法：
- `|,_,_,_`
- `_,|,_,_`
- `_,_,|,_`
- `_,_,_,|`

其中 `|` 表示隔板 (未選到的格子)
根據規則將選到的格子填上數字：
- `|,2,2,2`
- `1,|,2,2`
- `1,1,|,2`
- `1,1,1,|`

格子選法數剛好為遞增數列的可能數，可用排列組合的計算方式算出

### 標準程式
:::spoiler 解法 1
```cpp
#include<bits/stdc++.h>
using namespace std;

int const maxn = 1e3 + 10;
int const mod = 1000000007;

int n, m, p[maxn][25];

int main()
{
  cin >> n >> m;

  for(int i = 1; i <= n; i++) p[i][0] = 1;

  for(int r = 0; r < m; r++) // r := round
    for(int b = n; b >= 1; b--) { // b := bound
      long long sum = 0;
      for(int i = 1; i <= b; i++) sum = (sum + p[i][r]) % mod;

      p[b][r+1] = sum;
    }

  cout << p[n][m] << endl;

  return 0;
}
```
:::

:::spoiler 解法 2
```python
from math import factorial as fact
mod = 10**9 + 7

def C(n, k):
  return fact(n) // (fact(k) * fact(n - k))

n, m = map(int, input().split())
print(C(n + m - 1, m) % mod)
```
:::

## [な NA](https://judge.csie.ncku.edu.tw/problems/93)
- Task Provider: raiso
- Task Setter: raiso

### 首殺 team21_15 (00:13)

### 解法說明

這題有兩個要點需要注意：
- $\text{len}(M)$ 至少為 $1$
- 只需要考慮 $\text{len}(M) = 1$ or $2$ 的情況，因為對於長度更長的 $M$，我們只需要將其同時去頭去尾後，所得到的吸引力只會大於等於還沒去頭去尾的情況。

**超越**
本題可以使用 $O(N\log N)$ 的複雜度實作
- 將比對字串的過程看成多項式相乘，枚舉所有 $M$ 得到的吸引力其實就是多項式相乘後的結果，由於多項式相乘可以使用 FFT 與 IFFT 的方式加速，在 $O(N\log N)$ 的複雜度得到答案，由於整個過程過於神奇，有興趣的人可以自行研究。


### 標準程式

:::spoiler 解法 O(N^2)
```cpp
#include <bits/stdc++.h>
#define pb push_back
#define mp make_pair
#define Int long long
#define x first
#define y second
using namespace std;

void solve(){
  int n;
  cin >> n;
  string str;
  cin >> str;
  vector<int> A(n);
  for(int i = 0; i < n; i++) {
    if(str[i] == 'A') A[i] = 2;
    if(str[i] == 'T') A[i] = 1;
    if(str[i] == 'C') A[i] = -2;
    if(str[i] == 'G') A[i] = -1;
  }
  int ans = 0;
  for(int c = 1; c <= 2; c++) {
    for(int i = 1; i < n-c; i++) {
      int cnt = 0, j = 1;
      while((i+c-1+j) < n && (i-j) >= 0) {
        int tmp = A[i+c-1+j] + A[i-j];
        if(tmp == 3 || tmp == -3) cnt++;
        j++;
      }
      ans = max(ans, cnt);
    }
  }
  cout << ans << endl;
}

int main(){
  ios::sync_with_stdio(0); cin.tie(0);

  int t=1;
  //cin >> t;
  while(t--) solve();
  return 0;
}
```
:::

:::spoiler  解法 O(NlogN)

```cpp=
#include <bits/stdc++.h>
#define pb push_back
#define mp make_pair
#define add insert
#define Int long long
#define pdd pair<double,double>
#define pii pair<int,int>
#define pII pair<Int,Int>
#define sqr(x) ((x)*(x))
#define PI acosl(-1)
#define MEM(x) memset(x,0,sizeof(x))
#define x first
#define y second
using namespace std;

typedef complex<double> CD;

void FFT(vector<CD> &a, bool inv) {
	//bit reversal
	int n = a.size();
	for(int i=0, j=0; i < n; i++) {
		if(j > i) swap(a[i], a[j]);
		int k = n;
		while(j & (k >>=1)) j &=~k;
		j |= k;
	}
	double pi = inv? -PI:PI;
	for(int step=1; step < n; step <<= 1) {
		double alpha = pi/step;
		for(int k = 0; k < step; k++) {
			CD omegak = exp(CD(0, alpha*k));
			for(int Ek = k; Ek < n; Ek += step << 1) {
				int Ok = Ek + step;
				CD t = omegak * a[Ok];
				a[Ok] = a[Ek] - t;
				a[Ek] += t;
			}
		}
	}
	if(inv) for(int i = 0; i < n; i++) a[i] /= n;
}

inline vector<double> operator * (const vector<double>& v1, const vector<double>& v2) {
	int s1 = v1.size(), s2 = v2.size(), S = 2;
	while(S < s1+s2) S <<= 1;
	vector<CD> a(S,0), b(S,0);
	for(int i = 0; i < s1; i++) a[i] = v1[i];
	for(int i = 0; i < s2; i++) b[i] = v2[i];
	FFT(a, false);
	FFT(b, false);
	for(int i = 0; i < S; i++) a[i] *= b[i];
	FFT(a, true);
	vector<double> res(s1+s2-1);
	for(int i = 0; i < s1+s2-1; i++) res[i] = a[i].real();
	return res;
}

bool match(char a, char b) {
	if(a > b) swap(a, b);
	if(a == 'A' && b == 'T') return 1;
	else if(a == 'C' && b == 'G') return 1;
	return 0;
}

void solve(){
	int n; cin >> n;
	string str; cin >> str;
	vector<double> A(n,0), B(n,0), res1, res2;
	vector<int> res;
	for(int i = 0; i < n; i++) if(str[i] == 'A') A[i] = 1.0;
	for(int i = 0; i < n; i++) if(str[i] == 'T') B[i] = 1.0;
	res1 = A*B;
	A.clear();
	B.clear();
	A.assign(n, 0);
	B.assign(n, 0);
	for(int i = 0; i < n; i++) if(str[i] == 'C') A[i] = 1.0;
	for(int i = 0; i < n; i++) if(str[i] == 'G') B[i] = 1.0;
	res2 = A*B;
	res.resize(res1.size());
	int nn = res.size();
	for(int i = 0; i < nn; i++) res[i] = int(res1[i] + res2[i]);
	for(int i = 0; i < n; i++) if(match(str[i], str[i+1])) res[i*2+1]--;
	int maxi = 0;
	for(int i = 0; i < nn; i++) maxi = max(maxi, res[i]);
	cout << maxi << endl;
}
int main(){
	ios::sync_with_stdio(0); cin.tie(0);
	int t=1;
	//cin >> t;
	while(t--) solve();
	return 0;
}
```
:::