# USACO 2025 Open Contest AA 競程 dreamoon 老師的參考做法
[TOC]
## Bronze
### [Problem 1. Hoof Paper Scissors Minus One](https://usaco.org/index.php?page=viewproblem&cpid=1503)
#### 題目敘述
在遊戲 Hoof Paper Scissors 中,Bessie 與 Elsie 各自從 $N$ 個不同的手勢(蹄式)中選一個(編號為 $1$ 到 $N$),並根據一張規則表,不同手勢間比賽時會有兩種結果:
* 一方勝出、另一方失敗
* 平手
而在 Hoof Paper Scissors Minus One 中,兩隻牛各自同時出示兩個手勢(左蹄一個、右蹄一個)。各自手勢都被對方看見後,他們再從自己的兩個手勢中選一個作為正式比賽所出的手勢,最後依照規則表決定勝負。
給定 Elsie 在每場比賽中預計要出的手勢組合(共 $M$ 場比賽),Bessie 想知道自己有多少種手勢組合可以保證一定能贏過 Elsie。這裡「手勢組合」指的是一個有序對 $(L, R)$,其中 $L$ 為左蹄的手勢,$R$ 為右蹄的手勢。
#### 輸入格式
第一行包含兩個空格分隔的整數:
* $N$:象徵總數 ($1 \le N \le 3000$)
* $M$:比賽場數 ($1 \le M \le 3000$)
接下來 $N$ 行:
* 第 $i$ 行包含 $i$ 個字元:$a_{i,1}a_{i,2}\dots a_{i,i}$,每個字元皆為以下三者之一:
* D:象徵 $i$ 與象徵 $j$ 平手
* W:象徵 $i$ 勝過象徵 $j$
* L:象徵 $i$ 輸給象徵 $j$
* 保證 $a_{i,i} = \texttt{D}$。
接下來 $M$ 行:
* 每行包含兩個空格分隔的整數 $s_1$ 與 $s_2$,表示 Elsie 本場比賽出的象徵組合。
#### 輸出格式
輸出 $M$ 行,每行包含一個整數代表在第 $i$ 場比賽 Bessie 有幾種手勢組合可以保證一定贏過 Elsie。
#### 測資額外限制
測資 2~6:$N, M \le 100$
測資 7~12:無額外限制
#### 參考做法
> 此題用到了 AA 競程 Level 1 的「競程數學」中的知識點。
假設 Bessie 出的左蹄和右蹄手勢編號分別為 $a$ 和 $b$,那麼 Bessie 一定能贏的情況就是 $a$ 同時贏過 $s_1$ 和 $s_2$ 或 $b$ 同時贏過 $s_1$ 和 $s_2$,用 C++ 的程式語法來寫就是:
```cpp
(s[a][s1] == 'W' && s[a][s2] == 'W') || (s[b][s1] == 'W' && s[b][s2] == 'W')
```
暴力枚舉 $a$ 和 $b$ 即可通過前 6 筆測資。
若要通過所有測資就要用點數學方法來計算 Bessie 一定能贏的組合數量。
首先,我們還是要先暴力計算有多少手勢是能同時贏過 $s_1$ 和 $s_2$ 的,令此數量為 $cnt$。
接著 Bessie 能贏的數量其實就是「全部可能的手勢組合數量」減去「不能贏的組合數量」
「全部的組合數量」可用乘法原理算出是 $N \times N$,而不能贏的組合數量就是 $a$ 和 $b$ 都不是那 $cnt$ 個能贏的手勢的組合,也能利用乘法原理算出此數樣為 $(N - cnt) \times (N - cnt)$,所以答案就是 $N \times N - (N - cnt) \times (N - cnt)$
#### 參考程式碼
```cpp=
#include <iostream>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
const int SIZE = 5000;
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
int N, M;
cin >> N >> M;
vector<vector<char>> c(N + 1, vector<char>(N + 1));
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= i; j++) {
cin >> c[i][j];
if (c[i][j] == 'D') {
c[j][i] = 'D';
} else if (c[i][j] == 'W') {
c[j][i] = 'L';
} else {
c[j][i] = 'W';
}
}
}
while (M--) {
int x, y;
cin >> x >> y;
int cnt = 0;
for (int i = 0; i < N; i++) {
if (c[i][x] == 'W' && c[i][y] == 'W') cnt++;
}
cout << N * N - (N - cnt) * (N - cnt) << '\n';
}
}
```
### [Problem 2. More Cow Photos](https://usaco.org/index.php?page=viewproblem&cpid=1504)
#### 題目敘述
今天牛群特別頑皮! Farmer John(FJ)只想拍下排成一行的牛,但牠們總在快要拍照時移動。
具體來說,FJ 有 $N$ 隻牛 ($1 \le N \le 10^5$),每隻牛都有一個介於 $1$ 到 $N$ 的整數高度。FJ 希望拍出一張牛的排列照片,要求牛的高度從左到右必須滿足以下三個條件:
1. **山形排列**:牛的高度必須先不降後不升。也就是說,存在一個位置 $i$ 使得 $h_1 \le h_2 \le \dots \le h_i \ge h_{i+1} \ge \dots \ge h_K.$
2. **相鄰不等**:相鄰的兩頭牛高度必須不同,即對所有 $1 \le i < K$ 有 $h_i \ne h_{i+1}$。
3. **對稱**:照片必須左右對稱。也就是說,若 $i+j=K+1$,則必須有 $h_i = h_j$。
FJ 可以移除部分牛,並重新排列剩下的牛。請求出 FJ 能夠在照片中包含的牛的最大數量,使得上述條件均被滿足。
#### 輸入格式
輸入包含多個測試案例。
- 第一行包含一個整數 $T$ ($1 \le T \le 10^5$),表示測試案例數量。
- 接下來每個測試案例格式如下:
- 第一行:一個整數 $N$,表示牛的數量。
- 第二行:$N$ 個整數,分別表示每頭牛的高度,均介於 $1$ 到 $N$ 之間。
保證所有測試案例中 $N$ 的總和不超過 $10^6$。
#### 輸出格式
輸出 $T$ 行,每一行輸出一個整數,表示對應測試案例中 FJ 能在照片中包含的牛的最大數量。
#### 測試資料額外限制
- 測資 2-3:$T \le 100$, $N \le 7$
- 測資 4-5:$T \le 10^4$,所有牛的高度均不超過 $10$
- 測資 6-11:無額外限制
#### 參考做法
> 此題的參考做法用到了 AA 競程 Level 0 競程入門班的「用陣列紀錄資訊」單元以及 Level 1 的「簡易貪心」單元中的知識點。
先最高的牛的高度,以及用陣列把每個高度的牛各有幾隻算出來。
由於牛的排列必須是「山形排列」,且相鄰的兩隻牛又不能一樣,那把牛的身高列出來可能會是如下:
> 1 2 4 7 9 7 4 2 1
可發現除了最大的數字外,其他數字都是恰出現兩次。
既然我們要找的是最長的排列,那麼我們讓身高最大的牛站在正中央一定能找到解,令身高最大的牛高度為 $max\_h$,那麼對於所有 $i = 1 \sim max_h - 1$,只要身高 $i$ 的牛出現兩隻以上,我們就一定可以選其中兩隻出現在照片中,所以答案就是:
$$
\sum\limits_{i=1}^{max\_h-1} 2 \times [cnt[i] \ge 2]
$$
其中 $cnt[i]$ 代表身高為 $i$ 的牛有幾隻,$[\ ]$ 為 [艾佛森括號](https://zh.wikipedia.org/zh-tw/%E8%89%BE%E4%BD%9B%E6%A3%AE%E6%8B%AC%E8%99%9F)
#### 參考程式碼
```cpp=
#include <iostream>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
void solve() {
int N;
cin >> N;
vector<int> cnt(N + 1);
int max_h = 0;
for (int i = 1; i <= N; i++) {
int x;
cin >> x;
if (max_h < x) max_h = x;
cnt[x]++;
}
int ans = 1;
for (int i = 1; i < max_h; i++) {
if (cnt[i] >= 2) ans += 2;
}
cout << ans << '\n';
}
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
int t;
cin >> t;
while (t--) solve();
}
```
### [Problem 3: It's Mooin' Time III](https://usaco.org/index.php?page=viewproblem&cpid=1505)
#### 題目敘述
Elsie 正試圖向 Bessie 描述她最喜愛的 USACO 競賽,但 Bessie 卻無法理解 Elsie 為何如此喜歡這個比賽。Elsie 說:「而且到了 mooin' time!誰想要來個 mooin'? 拜託,我只想參加 USACO。」
Bessie 仍然不懂,所以她將 Elsie 的描述記錄成一個長度為 $N$ ($3 \leq N \leq 10^5$) 的字串,字串由小寫英文字母組成,記作 $s_1 s_2 \ldots s_N$。Elsie 定義一個由三個字元組成的字串 $t$ 為 moo,當且僅當 $t_2 = t_3$ 且 $t_2 \neq t_1$。
一個三元組 $(i,j,k)$ 若滿足 $i < j < k$ 且字串 $s_i s_j s_k$ 構成一個 moo,則稱該三元組有效。對於一個有效三元組,FJ 會進行以下操作以計算其值:
- FJ 將字串 $s$ 在索引 $j$ 處旋轉 $90$ 度。
- 該三元組的值為三角形 $\Delta ijk$ 面積的兩倍。
換句話說,三元組的值為 $(j-i)(k-j)$。
Bessie 向你提出 $Q$ ($1 \leq Q \leq 3 \cdot 10^4$) 個查詢。在每個查詢中,她給出兩個整數 $l$ 與 $r$ ($1 \leq l \leq r \leq N$, 且 $r-l+1 \geq 3$),要求你求出所有滿足 $l \leq i$ 且 $k \leq r$ 的有效三元組 $(i,j,k)$ 中,其值的最大值。如果不存在任何有效三元組,請輸出 $-1$。
<strong>注意:</strong> 此題中涉及到的整數數值可能非常大,因此在程式中可能需要使用 64 位整數型態(例如 C/C++ 中的 "long long")。
#### 輸入格式
- 第一行包含兩個整數 $N$ 與 $Q$。
- 接下來一行包含字串 $s_1 s_2 \ldots s_N$。
- 接下來 $Q$ 行,每行包含兩個整數 $l$ 與 $r$,表示一次查詢。
#### 輸出格式
對每個查詢,輸出一行答案,即該查詢中最大有效三元組的值;若不存在有效三元組,輸出 $-1$。
#### 參考做法
> 此題的參考做法用到了 AA 競程 Level 1「簡易貪心」、「數列相關函式」單元以及 「Level 1 公開課」中的知識點。
首先對於每個詢問我們要枚舉三元組的第二第三個元素是哪個字元,只有 $26$ 種可能,每種可能各自算出最佳解後,再取最大的那個就是答案了。
接下來我們假設第二第三個元素對應的字母是 $c$。
首先我們可以用貪心的概念來縮小我們要考慮的三元組的數量,最後可發現對於每個 $c$,其實我們至多只要考慮 $2$ 個三元組,解説如下:
* 在知道在確定第二第三個字母是 $c$ 的情況,第一個字母一定是選區間 $[l, r]$ 中最左邊的不是 $c$ 的字母最好(假設此位置為 $first\_pos$),最後一個字母則是選區間中最右邊的 $c$(假設次位置為 $last\_pos$)。而第二個字母一定是選越靠近 $mid = (first\_pos + last\_pos)/2$ 的位置越好,最靠近 $mid$ 的候補為只有兩個: $\ge mid$ 的最左邊位置以及 $< mid$ 的最右邊位置。
而 $first\_pos$ 的位置可讀入字串後,就先預處理 $nextDiff$ 陣列,$nextDiff[pos]$ 代表位置 $pos$ 右邊第一個和 $s[pos]$ 不一樣的字母位置,$nextDiff$ 陣列求法請參考範例程式碼。
如此一來,對於如果 $s[l]$ 不是 $c$,那麼 $first\_pos$ 就是 $l$,否則 $first\_pos$ 就是 $nextDiff[l]$。
而 $last\_pos$ 的求法可是先把所有字元 $c$ 的位置放入一個 vector 裡,然後使用 $upper\_bound$ 來求。
最後,靠近 $mid$ 的位置也可使用 $lower\_bound$ 函式來求出,於是此題的時間複雜度分析如下:
* 預處理 $nextDiff$ 陣列的時間為 $O(N)$
* 回答每個詢問的時間為 $O(字母集大小 \times \log{N})$
* 總時間複雜度為 $O(N + 字母集大小 \times \log{N})$
#### 參考程式碼
```cpp=
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
#define SZ(X) (X).size()
typedef long long ll;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
cin >> N >> Q;
string s;
cin >> s; // 原始字串讀入時為 0-indexed,但我們後續用 1-indexed 處理
s = " " + s + " ";
// 預處理:計算 nextDiff 陣列 (1-indexed)
vector<int> nextDiff(N + 2, N + 1); // 設定 INF 為 N+1
nextDiff[N] = N + 1; // 最後一個位置無後續不同字元
for (int i = N; i >= 1; i--) {
if (s[i] != s[i + 1]) nextDiff[i] = i + 1;
else nextDiff[i] = nextDiff[i + 1];
}
// 對每個字母建立出現位置陣列(1-indexed)
vector<vector<int>> pos(26);
for (int i = 1; i <= N; i++) {
char c = s[i];
pos[c - 'a'].push_back(i);
}
// 處理每個查詢
while (Q--) {
int l, r;
cin >> l >> r;
ll ans = -1;
// 枚舉每個字母作為三元組第二和第三個位置的值的情況,分別求最佳解
for (char c = 0; c < 26; c++) {
// 先求出當第二第三個字元是 c 時, [l, r] 間可作為第一個字元的位置的最左邊的位置
int first_pos = l;
if (s[l] == c + 'a') {
first_pos = nextDiff[l];
}
// 如果第一個字母可能的最左邊位置都大於 r - 2,一定無解
if (first_pos > r - 2) continue;
auto &vec = pos[c];
// 利用 upper_bound 求出該三元組第三個元素在 [l, r] 可出現的最右邊的位置,若有解選最右邊的位置一定是最佳解
auto itR = upper_bound(vec.begin(), vec.end(), r);
if (itR == vec.begin()) continue;
itR--;
if (*itR <= first_pos) continue;
int last_pos = *itR; // last_pos 為最 [l, r] 內最右邊可能是 c 的位置
int mid = (first_pos + last_pos) / 2; // 最佳解只出現在三元組第二個元素 >= mid 的最小位置或 < mid 的最大位置
int mid_index = lower_bound(vec.begin(), vec.end(), mid) - vec.begin();
for (int j = max(0, mid_index - 1); j <= mid_index && j < SZ(vec); j++) {
if (vec[j] > first_pos && vec[j] < last_pos) {
ans = max(ans, (vec[j] - first_pos) * 1LL * (last_pos - vec[j]));
}
}
}
cout << ans << "\n";
}
return 0;
}
```
## Silver
### [Problem 1: Sequence Construction](https://usaco.org/index.php?page=viewproblem&cpid=1506)
#### 題目敘述
給你兩個正整數 $M, K$ ($1 \le M \le 10^9, 1 \le K \le 31$),請構造一個非負整數數列 $a_1, a_2, \ldots, a_N$ 滿足以下條件:
1. $1 \le N \le 100$
2. $\sum\limits_{i=1}^N a_i = M$
3. $popcount(a_1)\ xor\ popcount(a_2)\ xor\ \ldots\ xor\ popcount(a_N) = K$ ($popcount(x)$ 指的是 $x$ 寫成二進制後,有幾個位數是 $1$, $xor$ 則是按位元互斥或)
#### 參考做法
> 此題的參考做法免強算是用到了 AA 競程 Level 1「簡易貪心」吧...
這種莫名其妙的構造題,感覺就是用來防 chatGPT 用的 XD
題目只有兩個參數 $M$ 和 $K$,不妨先固定其中一個參數,想想看另一個參數在什麼時候會有解,尤其時去思考看看有解時該參數的極小極大值會是什麼
例如我們可以固定 $K$,思考看看對於同一個 $K$ 值,$M$ 最小能多小。
由於 $K$ 是所有 $a_i$ 位元 $1$ 的數量 xor 起來的結果,那也就是說,如果 $K$ 二進制最高位是 $h$,那麼無論如何都至少要有一個 $a_i$ 含有的 位元 $1$ 的數量是 $2^h$ 以上了。
這樣想下去大概能發現 $M$ 最小的情況會發生在 所有的 $popcount(a_i)$ 都是 $2$ 的冪次方,且 $popcount(a_1)\ +\ popcount(a_2)\ +\ \ldots\ +\ popcount(a_N) = K$ 的時候,於是我們就能夠貪心地算出給定 $K$ 時,$M$ 的可能最小值,令此值為 $f(K)$。
接著分 $4$ 種情況討論。
1. 當 $M \le f(K)$,無解。
2. 當 $M \ge f(K)$,且 $M - f(K)$ 是偶數時,我們就只要先把輸入為 ``f(K) K`` 的解構造出來,最後數列 $a$ 再加上兩個 $(M - f(k))/2$ 就是答案了。
3. 當 $M \ge f(K)$,且 $M - f(K) = 1$ 時,又有兩種情況:
1. 若輸入為 ``f(K) K`` 的解有某個 $a_i = 1$,那把此 $a_i$ 改成 $2$,此 $a_i$ 的位元 $1$ 數量不會改變,但 $a_i$ 的總和增加 $1$ 了,所以也會是現在的輸入的解。
2. 若輸入為 ``f(K) K`` 的解不存在任何一個 $a_i = 1$,那麼就無解,因為要讓題目要求的第三個條件不變的情況,並要把 $a_i$ 的總和變大,增加的量不可能只有 $1$。
4. 當 $M \ge f(K)$,且 $M - f(K)$ 是 $\ge 3$ 的奇數時,首先先把輸入為 ``f(K) K`` 的解構造出來,然後發現 $a$ 再增加 $1, 2$ 這兩個數字後,題目要求的第三個條件的 xor 結果也不會改變,但 $a_i$ 的數字和增加了 $3$,接著,若要再讓 $a_i$ 的總和增加某個 $d$($d$ 是偶數),就只要把數列 $a$ 的再加入 $2$ 個 $d/2$ 即可。
#### 參考程式碼
```cpp=
// 16 min
#include <iostream>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
void solve() {
int M, K;
cin >> M >> K;
vector<int> a;
int sum = 0;
for (int i = 0; i < 5; i++) {
if ((K >> i) & 1) {
a.push_back((1 << (1 << i)) - 1);
sum += a.back();
}
}
if (sum > M) {
cout << "-1\n";
return;
}
if ((M - sum) % 2 == 0) {
a.push_back((M - sum) / 2);
a.push_back((M - sum) / 2);
} else {
if (M - sum == 1) {
if (a[0] == 1) a[0]++;
else {
cout << "-1\n";
return;
}
} else {
int d = M - sum - 3;
a.push_back(2);
a.push_back(1);
a.push_back(d / 2);
a.push_back(d / 2);
}
}
cout << SZ(a) << '\n';
for (int i = 0; i < SZ(a); i++) {
cout << a[i] << (i + 1 < SZ(a) ? ' ' : '\n');
}
}
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
int t;
cin >> t;
while (t--) solve();
}
```
### [Problem 2: Compatible Pair](https://usaco.org/index.php?page=viewproblem&cpid=1507)
#### 題目敘述
有非常多隻牛,每個牛都有一個數字代表他的 ID,但有可能有一些牛共用同個 ID。給你正整數 $N$,代表共有 $N$ 種不同的 ID,第 $i$ 種 ID 對應的數字是 $d_i$,並且有 $n_i$ 隻牛的 ID 是 $d_i$ ($1 \le n_i \le 10^9, 0 \le d_i \le 10^9$)。
給你兩個數字 $A, B$ ($0 \le A \le B \le 2 \times 10^9$),兩隻牛若他們的 ID 加起來為 $A$ 或 $B$ 的話,這兩隻牛就可以湊成一對,每隻牛至多只能和其他一隻牛湊成一對,請問所有牛至多可湊成幾對?
#### 參考做法
> 此題的參考做法用到了 AA 競程 Level 1「簡易貪心」和「關聯容器」單元裡的知識點
可把每個 ID 都視為圖論上的點,若兩個相異的 ID 可以配對就連一條邊,若一個 ID 自己和自己能配對,就把該點視為「特殊的點」。可發現此圖的每個點度數至多只有 $2$,且不會有環,也就是說此圖是由很多條鏈(整個連通塊本身就是一條路徑(path) 的子圖)構成。
每條鏈之間是互相不影響的,所以可分開考慮,算出各條鏈的答案再加總起來即可。
對於一條鏈,至少會有一個度數為 $1$ 的端點不是「特殊的點」,可發現優先把此點對應的 ID 的牛和他們唯一能配對 ID 的牛配對一定不會比較差,配完後,就可把此點移除,接下來一直做一樣的事,直到最後只剩下特殊的點,再把特殊的點對答案的貢獻算上即可。
存在各式各樣的實作方式,範例程式碼用 dfs 的概念實作。
#### 參考程式碼
```cpp=
#include <algorithm>
#include <iostream>
#include <map>
#include <set>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
map<int, int> cnt; // cnt[x] 紀錄 ID 為 x 的牛還有幾隻尚未被配對到
map<int, int> vis; // 紀錄一個點是否已被拜訪過
set<int> special_p; // speicial_p 紀錄那些可以和相同 ID 的牛配對的 ID
map<int, vector<int>> e; // 若 ID 為 x 的牛可以和 ID 為 y 的牛配對,那麼 e[x] 裡會含有 y
long long ans;
void dfs(int x) {
for (int y : e[x]) {
if (vis[y]) continue;
vis[y] = 1;
int mi = min(cnt[x], cnt[y]);
cnt[x] -= mi;
cnt[y] -= mi;
ans += mi;
dfs(y);
}
}
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
int N, A, B;
cin >> N >> A >> B;
for (int i = 0; i < N; i++) {
int n, d;
cin >> n >> d;
cnt[d] = n;
}
for (auto [d, n] : cnt) {
for (int x : {A - d, B - d}) {
if (d == x) special_p.insert(d);
else {
auto it = cnt.find(x);
if (it != cnt.end()) {
e[d].push_back(x);
}
}
}
}
for (auto &[x, vi] : e) {
// 若發現一個點度數是 1 且尚未被拜訪過,且不是「特殊的點」,就從它開始 dfs
// 每次枚舉到一條邊,就把該邊對應到的兩個 ID 的牛能配多少就配多少
if (SZ(vi) == 1 && !vis[x] && special_p.find(x) == special_p.end()) {
dfs(x);
}
}
// 最後還要考慮相同 ID 能配的牛
for (int d : special_p) {
ans += cnt[d] / 2;
}
cout << ans << '\n';
}
```
### [Problem 3: Ski Slope](https://usaco.org/index.php?page=viewproblem&cpid=1508)
#### 題目敘述
Bessie 與她的朋友們準備去滑雪旅行。這座山上有 $N$ 個「中繼點」(waypoints),分別標記為 $1, 2, \ldots, N$,海拔依照編號遞增(其中中繼點 $1$ 在山底)。
對於每個中繼點 $i$,都有一條滑雪道從 $i$ 處開始,一直延伸到 $p_i$ ($1 \le p_i < i$) 處。每條滑雪道各自有一個「難度」(difficulty) $d_i$ 與「樂趣」(enjoyment) $e_i$,其中
- $0 \le d_i \le 10^9$
- $0 \le e_i \le 10^9$
Bessie 有 $M$ 位朋友($1 \le M \le 10^5$),他們將依照以下方式滑雪:
- 每位朋友選擇一個起始中繼點 $i$;
- 從該中繼點開始,沿著滑雪道依序往下滑($i \to p_i \to p_{p_i} \to \dots$),最終抵達中繼點 $1$。
對於滑過的每一條滑雪道,朋友所獲得的「樂趣」會累加;整段旅程的總樂趣即為沿途所有滑雪道的樂趣總和。
不過,每位朋友也有自己不同的「技能值」(skill) $s_j$ 與「勇氣值」(courage) $c_j$,範圍為:
- $0 \le s_j \le 10^9$
- $0 \le c_j \le 10$
第 $j$ 位朋友只能選擇路徑上難度超過 $s_j$ 的滑雪道至多只有 $c_j$ 條的路徑。
你的任務是:對於每位朋友,計算他能獲得的最大樂趣總和。
---
#### 輸入格式
1. 第一行輸入單一整數 $N$($1 \le N \le 10^5$),表示中繼點的數量。
2. 接下來從第 2 行到第 $N$ 行(共 $N-1$ 行),每行包含三個以空格分隔的整數:
- $p_i$:第 $i$ 個中繼點有滑雪道通往中繼點 $p_i$
- $d_i$:第 $i$ 個中繼點的滑雪道的「樂趣」
- $e_i$:第 $i$ 個中繼點的滑雪道的「難度」
3. 接著讀入一行,包含單一整數 $M$($1 \le M \le 10^5$),表示 Bessie 的朋友人數。
4. 之後的 $M$ 行中,每行包含兩個以空格分隔的整數:
- $s_i$:第 $i$ 位朋友的技能值
- $c_i$:第 $i$ 位朋友的勇氣值
#### 其他說明
- 由於本題中可能出現非常大的整數運算,建議在程式實作時使用 64 位整數型態(例如 C/C++ 中的 `long long`)以避免溢位。
#### 參考做法
> 此題的參考做法用到了 AA 競程 Level 2「深度優先搜索(DFS)」和 Level 1「關聯容器」單元裡的知識點,並假設大家已有 DFS 的 backtracking(回溯法) 的概念
首先注意到一個重點:
若第 $j$ 位朋友可以從中繼點 $i$ 出發,代表著:「該條路徑難度第 $c_j+1$ 大(從 $1$ 開始編號) 的滑雪道值小於等於 $s_i$。」
於是若不管時間複雜度,我們可以有個 $O(N^2M)$ 的做法如下:
> 每個朋友的答案各自計算,再枚舉所有中繼點 i 作為起點的情況,搜集該路徑所有滑雪道難度,求出第 c_i + 1 大的難度值,若該值 <= s_i,那麼此路徑的「樂趣」總和就能更新此位朋友的答案
接著我們來一步一步優化此做法。
**1. 利用DFS+回溯的概念優化**
上述比較暴力的做法是每個朋友都要獨立計算一次,但每次計算都要重新算一次每條路徑第 $c_j+1$ 大的困難度,況且 $c_j$ 的值其實只有 $11$ 種,若可以一剛開始就算出來,那對時間複雜度優化肯定是有幫助的。
這部分我們可使用 DFS+回溯的概念,改成從 $1$ 號中繼點當起點 DFS,並在全域維護一個 multiset,儲存 DFS 時起點到當前點路徑上所有滑雪道的難度,這樣 DFS 時每走到一個中繼點 $i$,就能求出 $1$ 好中繼點到 $i$ 號中繼點的路徑中前 $11$ 大困難度的值了。這部分總時間複雜度只要 $O(N log N)$ (此時間複雜度的計算是把 $11$ 當常數)
**2. 把資料整理成單調序列,以便利用 upper_bound 優化
接著,對於相同 $c_j$ 值,我們回溯後可以得到許多個 pair $(x, y)$,代表若第 $j$ 位朋友的技能值($s_j$) 大於等於 $x$,就可以有總和為 $y$ 的「樂趣」,若存在兩個 pair $(x1,y1)$ 和 $(x2, y2)$,若 $x2 \ge x1$ 但 $y2 < y1$ ,那麼 $(x2, y2)$ 至個 pair 就不需要考慮了,因為他不會比 $(x1, y1)$ 還好。於是我們可先把所有 pair 按照 $x$ 值排序,排完後,再把剛才說不需要考慮的那些 pair 移除,剩下的 pair 就會隨著 $x$ 的遞增,$y$ 也是遞增的。於是對於每個詢問,我們就可以使用 upper_bound 找到 $\le s_i$ 的最大的 $x$ 的 pair,此 pair 的 $y$ 值就是該詢問的答案。
#### 參考程式碼
```cpp=
#include <algorithm>
#include <array>
#include <iostream>
#include <set>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
using ll = long long;
const int SIZE = 1 << 17;
struct Edge {
int y, difficulty, enjoyment;
};
vector<Edge> e[SIZE];
multiset<int> difs;
vector<array<ll, 2>> pp[11];
void dfs(int x, ll len) {
auto it = difs.rbegin();
for (int i = 0; i <= 10; i++) {
if (it != difs.rend()) {
pp[i].push_back({*it, len});
it++;
} else {
pp[i].push_back({0, len});
}
}
for (int i = 0; i < SZ(e[x]); i++) {
difs.insert(e[x][i].difficulty);
dfs(e[x][i].y, len + e[x][i].enjoyment);
difs.erase(difs.find(e[x][i].difficulty));
}
}
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
int N;
cin >> N;
for (int i = 2; i <= N; i++) {
int p, d, en;
cin >> p >> d >> en;
e[p].push_back({i, d, en});
}
dfs(1, 0);
for (int i = 0; i <= 10; i++) {
sort(pp[i].begin(), pp[i].end());
ll last_v = -1;
int it = 0;
vector<array<ll, 2>> tmp;
for (int j = 0; j < SZ(pp[i]); j++) {
if (pp[i][j][1] <= last_v) continue;
last_v = pp[i][j][1];
tmp.push_back(pp[i][j]);
}
pp[i].swap(tmp);
}
int M;
cin >> M;
while (M--) {
int s, c;
cin >> s >> c;
auto pos = upper_bound(pp[c].begin(), pp[c].end(), array<ll, 2>{s + 1LL, -1LL}) - pp[c].begin();
pos--;
if (pos < 0) cout << "0\n";
else {
cout << pp[c][pos][1] << '\n';
}
}
}
```
## Gold
### [Problem 1: Moo Decomposition](https://usaco.org/index.php?page=viewproblem&cpid=1509)
#### 簡要題意
輸入給你三個正整數 $K, N, L$ ($1 \le N, K \le 10^6, 1\le L \le 10^{18}$),以及給你一個長度為 $N$ 且由英文大寫字母 `'M'` 和 `'O'` 組成字串 $s$,而你要處理的真正字串是由 $s$ 複製 $L$ 次接起來的字串。例如說,當 $s = $ `"MOMOOO"`,$L = 3$,那麼你要處理的真正字串是:`"MOMOOOMOMOOOMOMOOO"`。
定義 $MOO$ 字串是由一個 `'M'` 後面接上 $K$ 個 `'O'` 形成的字串。
現在請你回答以下問題:
要把 $s$ 複製 $L$ 次接起來的字串拆成很多個子序列,使得每個子序列都是長度為 $MOO$ 字串,請問有幾種拆法?(保證至少有 $1$ 種拆法) 請輸出答案除以 $10^9+7$ 的餘數。
例如,當 $K=2,N=4,L=1$ 且 $s=$ `"MOMOOO"`,就有 $3$ 種拆法:
1. 第一個子序列位置是 $1,2,4$,第二個子序列位置是 $3,5,6$
2. 第一個子序列位置是 $1,2,5$,第二個子序列位置是 $3,4,6$
3. 第一個子序列位置是 $1,2,6$,第二個子序列位置是 $3,4,5$
#### 參考做法
> 此題的參考做法用到了 AA 競程 Level 4「特殊 DP 題型」裡所介紹的組合數相關知識。
看完這道題我第一個想法是:「這道題放在金組也太簡單了吧...」
首先,由於題目保證至少有一種拆法,所以字串 $s$ 裡 `'O'` 的數量一定是 `'M'` 的 $K$ 倍,接著 $L$ 這個參數對此題難度的影響非常少,因為可發線每個子序列一定是在同一份複製出來的 $s$ 裡,所以 $s$ 複製 $L$ 次的答案其實就是只有一個 $s$ 的答案的 $L$ 次方。
最後來解說要怎麼計算當 $L = 1$ 時的答案。
我們只要從右往左枚舉字串裡的每個字元,並用一個變數 $cnt$ 紀錄目前有多少個被枚舉到的字母 `'O'` 還沒被確定是放在哪個子序列裡,而當我們枚舉到 `'O'` 時 $cnt$ 就增加 $1$;而當枚舉到 `'M'` 時,我們就決定這個 `'M'` 後面要接哪些字母 `'O'`,因為還有 $cnt$ 個字母 `'O'` 還沒決定要放在哪個子序列,所以我們就有 $cnt \choose K$ 種選擇方式,可直接把此值乘上答案後,在把 $cnt$ 的值減少 $K$ 即可。時間複雜度為 $O(N)$。
#### 參考程式碼
```cpp=
#include <algorithm>
#include <iostream>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
const int MOD = (int)1e9 + 7;
const int SIZE = 1'000'001;
long long mypow(long long x, long long y, int mod = MOD) {
x %= mod;
long long res = 1 % mod;
while (y) {
if (y & 1) res = res * x % mod;
y >>= 1;
x = x * x % mod;
}
return res;
}
long long fac[SIZE];
void pre() {
fac[0] = 1;
for (int i = 1; i < SIZE; i++) fac[i] = fac[i - 1] * i % MOD;
}
long long C(long long x, long long y) {
if (y < 0 || y > x) return 0;
return fac[x] * mypow(fac[y] * fac[x - y], MOD - 2) % MOD;
}
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
pre();
int K, N;
long long L;
cin >> K >> N >> L;
string s;
cin >> s;
long long ans = 1;
int cnt = 0;
for (int i = SZ(s) - 1; i >= 0; i--) {
if (s[i] == 'O') cnt++;
else {
ans = ans * C(cnt, K) % MOD;
cnt -= K;
}
}
cout << mypow(ans, L) << '\n';
}
```
### [Problem 2: Election Queries](https://usaco.org/index.php?page=viewproblem&cpid=1510)
#### 簡要題意
給你一個長度為 $N$ 的數列,$a_1, a_2, \ldots, a_N$,接下來有 $Q$ 次詢問,每次詢問都會給你兩個正整數 $i, x$ 代表你要把 $a_i$ 的值改為 $x$,請你回答以下問題:
請把數列 $a$ 拆成兩個非空的子序列,使得此兩子序列的眾數的差的絕對值盡可能大(如果眾數有很多個,可以任取一個)。舉例來說,數列 $1, 4, 1, 5, 4, 4, 4$
#### 參考作法
> 此題的參考做法用到了 AA 競程 Level 5「根號資料結構與演算法」單元的知識點
先提一下,我覺得出題者使用的方法極可能和我的方法不一樣,這種複雜的資料結構題通常都有好多種做法。
---
定義 $cnt[x]$ 代表數字 $x$ 在 $a$ 裡出現幾次。
先介紹 4 個觀察:
1. 若數字 $x$ 和數字 $y$ 可以成為兩個子序列的眾數,那麼
$cnt[x] + cnt[y] \ge \max\limits_{i=1\sim N}{cnt[i]}$
因為你只要把全部的 $x$ 放在第一個子序列,全部的 $y$ 放在第二個子序列,然後剩下的每個數字就好好的分配到兩個子序列,使得在第一個子序列的數量不要超過 $cnt[x]$,在第二個子序列的數量不要 $cnt[y]$ 即可。
2. $cnt[x]$ 的不同的值的數量為 $O(\sqrt{n})$,這是老梗就不解釋了。
3. 定義集合 $S_i = \{x | cnt[x]=i\}$,對於每個 $S_i$,利用貪心的概念,在計算答案時,都只要考慮 $S_i$ 中最大和最小的數字即可,綜合第 2 個觀察,每個詢問我們都只要考慮 $O(\sqrt{n})$ 個數字。
4. 如果兩個子序列裡的眾數只需要從其中 $O(\sqrt{n})$ 個數去考慮,且這些數已經根據 $cnt[x]$ 的值排序好的話,那我們很容易用雙指標一類的方法 $O(\sqrt{n})$ 算出答案。
有了第 $4$ 個觀察後,如果我們有辦法每個詢問都只花 $O(\sqrt{n})$ 的時間把這 $O(\sqrt{n})$ 個要考慮的數字找出來,那麼此題就可以用 $O(Q \sqrt{N})$ 的時間解決啦!
而要找到這些要考慮的數字,其實只要找到所有非空的 $S_i$ 的索引值即可。
至於這件事我們要用根號分塊來解決。
在程式碼裡,我們會真的使用 STL 的 set 表示觀察 3 裡的 $S_i$(程式碼裡叫做 $ids[i]$),可以發現,每個詢問裡只會改變兩個數字的數量,也就是至多會變動到 $4$ 個 $S_i$。
有了這樣的觀察後其實就有很多種解決方式了,這裡只介紹我程式碼使用的方法:
把集合 $S_i$ 每 $GROUP\_SIZE$ 個一組,並用集合 $exist\_number[j]$ 儲存第 $j$ 組中非公的 $S_i$ 的索引值。
每個詢問都變動到的 $S_i$ 至多只會出現在 $4$ 組裡,再用 $O(GROUP\_SIZE)$ 的時間暴力更新這幾組的 $exist\_number[i]$,更新完後,在用 $O(N/GROUP\_SIZE+\sqrt{N})$ 的時間把所有集合 $exist\_number[i]$ 裡的索引值按照小到大搜集起來即可。
細節請直接參考程式碼。
#### 參考程式碼
```cpp=
#include <algorithm>
#include <iostream>
#include <set>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
const int SIZE = 200'001;
const int GROUP_SIZE = 512;
int N;
int cnt[SIZE];
set<int> ids[SIZE]; // ids[i] 紀錄所有出現在 a 裡數量為 i 的數字
// exist_number[j][*] 紀錄 S 集合序列中第 $j$ 組裡非空集合的編號們
// exist_num[j] 是第 $j$ 組裡非空的集合數量
int exist_number[GROUP_SIZE][GROUP_SIZE];
int exist_num[GROUP_SIZE];
void add(int x) {
int id = x / GROUP_SIZE;
exist_number[id][exist_num[id]++] = x;
}
// 重新計算 $S$ 集合序列中第 seg_id 組有哪些非空集合
void recalc(int seg_id) {
exist_num[seg_id] = 0;
for (int i = seg_id * GROUP_SIZE; i < seg_id * GROUP_SIZE + GROUP_SIZE && i <= N; i++) {
if (SZ(ids[i])) add(i);
}
}
int hid[SIZE], a[SIZE];
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
int Q;
cin >> N >> Q;
for (int i = 1; i <= N; i++) {
cin >> a[i];
cnt[a[i]]++;
}
for (int i = 1; i <= N; i++) {
if (cnt[i]) ids[cnt[i]].insert(i);
}
for (int i = 1; i <= N; i++) {
if (SZ(ids[i])) {
add(i);
}
}
while (Q--) {
int i, x;
cin >> i >> x;
if (a[i] != x) {
set<int> tmp;
ids[cnt[a[i]]].erase(a[i]);
if (ids[cnt[a[i]]].empty()) tmp.insert(cnt[a[i]] / GROUP_SIZE);
cnt[a[i]]--;
if (cnt[a[i]]) {
if (ids[cnt[a[i]]].empty()) tmp.insert(cnt[a[i]] / GROUP_SIZE);
ids[cnt[a[i]]].insert(a[i]);
}
if (cnt[x]) {
ids[cnt[x]].erase(x);
if (ids[cnt[x]].empty()) tmp.insert(cnt[x] / GROUP_SIZE);
}
cnt[x]++;
if (ids[cnt[x]].empty()) {
tmp.insert(cnt[x] / GROUP_SIZE);
}
ids[cnt[x]].insert(x);
a[i] = x;
for (int seg_id : tmp) {
recalc(seg_id);
}
}
// 以下這段程式碼會把所有非空的 S_i 的 i 值搜集起來放在 hid 陣列裡
int hn = 0;
for (int i = 0; i * GROUP_SIZE <= N; i++) {
for (int j = 0; j < exist_num[i]; j++) {
hid[hn++] = exist_number[i][j];
}
}
int bb = hid[hn - 1];
int j = hn - 1;
int mi = 1e9;
int ma = -1e9;
int ans = 0;
// 用雙指標計算答案
for (int i = 0; i < hn; i++) {
while (j >= 0 && hid[j] + hid[i] >= bb) {
ma = max(ma, *ids[hid[j]].rbegin());
mi = min(mi, *ids[hid[j]].begin());
j--;
}
ans = max(ans, ma - *ids[hid[i]].begin());
ans = max(ans, *ids[hid[i]].rbegin() - mi);
}
cout << ans << '\n';
}
}
```
### [Problem 3: OohMoo Milk](https://usaco.org/index.php?page=viewproblem&cpid=1511)
#### 簡要題意
有一個長度為 $N$ 的數列 $m_1, m_2, \ldots, m_N$ ($1 \le m_i \le 10^9$),Bessie 和 Elsie 根據此數列完以下遊戲:
1. 遊戲共有三個參數 $D, A, B$ ($1 \le A < B \le N, 1 \le D \le 10^9$)。
1. 遊戲共會進行 $D$ 輪。
2. 每一輪開始時,首先 Bessie 會把數列 $m$ 的其中 $A$ 個數增加 $1$,接著 Elsie 會把其中 $B$ 個大於 $0$ 的數減 $1$。
3. 遊戲最終分數的計算方式為 $\sum\limits_{i=1}^N m_i$,Bessie 的目標是讓分數盡可能高,而 Elsie 的目標是讓分數盡可能小
假設 Bessie 和 Elsie 都採取最佳策略,請問遊戲最終分數會是多少?
#### 參考作法
> 此題的參考做法用到了 AA 競程 Level 2「二分搜常見題型」單元的知識點
首先,可用貪心的思維證明在最佳策略中, Bessie 一定是選 $m$ 中最大的 $A$ 個數增加 $1$,而 Elsie 也會是選 $m$ 中最大的 $B$ 個數減 $1$,就結論而言,每一輪操作會是數列 $m$ 中第 $B+1$ 大的數至第 $A$ 大的數都增加 $1$。
那麼除了最大的 $A$ 個數以外的數肯定是維持不變的,就先把他們忽略,最終答案再加上這些數的平方和,所以我們就直接當作數列 $m$ 長度為 $A$,每次操作則是會把數列裡最小的 $A-B$ 個數增加 $1$。
但這題困難的點在於回合數實在太多了,那該怎麼辦呢?
**(以下已經把數列 m 的長度視為 A 了)**
模擬一些 case 後可發現,由於我們一定是盡可能讓比較小的數變大,最終數列 $m$ 的長相一定是存在某個數字 $x$,原本數列 $m$ 裡小於等於 $x$ 的 $m[i]$ 都會變為 $\min(m[i] + D, x)$,或是變為 $x+1$ (一定變為這樣的證明請參考 [CF1551 B2. Wonderful Coloring - 2](https://codeforces.com/problemset/problem/1551/B2) 的題解,其實和這題 Codeforces 題是樣的東西),那我們就去二分搜這個 $x$ 值即可,也就是說,要找到最大的 $x$,使得 $\sum_{i=1}^A max(0, min(x-m[i], D)) \le D \cdot (A-B)$,二分搜求出 $x$ 值之後,就不難得到數列 $m$ 的最終長相,然後就可以算出最終得分。
#### 參考程式碼
```cpp=
#include <algorithm>
#include <iostream>
#include <vector>
#define SZ(X) (int)(X).size()
using namespace std;
const int MOD = (int)1e9 + 7;
template <typename T>
void add(T &x, long long v) {
x = (x + v) % MOD;
if (x < 0) x += MOD;
}
void solve() {
int N, D;
cin >> N >> D;
int A, B;
cin >> A >> B;
vector<int> m(N);
for(int &x: m) cin >> x;
sort(m.begin(), m.end(), greater<>());
long long low = m[A - 1], hi = m[0] + D;
while (low <= hi) {
long long mid = (low + hi) / 2;
long long need = 0;
for (int i = 0; i < A; i++) {
need += max(0LL, min(1LL * D, mid - m[i]));
}
if (need < 1LL * D * (A - B)) low = mid + 1;
else hi = mid - 1;
}
int h = low - 1;
long long use = 0;
long long ans = 0;
for (int i = 0; i < A; i++) {
if (m[i] < h) {
int inc = min(h - m[i], D);
use += inc;
m[i] += inc;
}
}
for (int i = 0; i < N; i++) {
if (m[i] == h && use < 1LL * (A - B) * D) {
use++;
m[i]++;
}
add(ans, 1LL * m[i] * m[i]);
}
cout << ans << '\n';
}
int main() {
cin.tie(0);
ios_base::sync_with_stdio(0);
solve();
}
```