# 模競 I 題解
---
<!-- .slide: data-background="https://i.imgur.com/dhjun4n.jpg" -->
<font color="black">
<font size=7><b>麵團分割 (Dough)</b></font><br>
<font size=6><b>Source: LeetCode 2366</b></font><br>
<font size=6><b>Prepared by Joylintp</b></font><br>
</font>
----
子任務 1
$a_N = 1$
----
滿足麵團長度遞增且最後一塊長度是 $1$
→ 所有的麵團長度都要是 $1$
→ 分割次數為 $(a_1-1) + (a_2-1) + \ldots + (a_N-1)$
----
子任務 2
$N = 2$
----
考慮兩種可能:$a_1 \le a_2$ 和 $a_1 > a_2$
----
$a_1 \le a_2$:已滿足遞增條件,答案為 $0$
----
$a_1 > a_2$:必須第一塊麵團分為每塊長度不超過 $a_2$
→ 必須將 $a_1$ 分成 $\lceil \dfrac{a_2}{a_1} \rceil$ 塊
→ 答案為 $\lceil \dfrac{a_2}{a_1} \rceil - 1$
----
子任務 3
$N \le 100, a_i \le 100$
或許可以作奇怪 DP(但是不重要)
----
子任務 4
無額外限制
----
對於每個 $1 < i \le N$
設分割完第 $i$ 塊麵團後,最小的那段長度為 $x$
則第 $i-1$ 塊麵團的每一塊長度都不能超過 $x$
→ 至少要把第 $i-1$ 塊麵團分成 $\lceil \dfrac{a_{i-1}}{x} \rceil$ 段
----
為了下一塊麵團分盡量少段
你希望讓每一塊麵團最短的那段長度盡量大
→ 盡量平均地將麵團分為 $\lceil \dfrac{a_{i-1}}{x} \rceil$ 段
→ 最小的那段麵團長度會是 $\lfloor a_{i-1} / \lceil \dfrac{a_{i-1}}{x} \rceil \rfloor$
----
注意:
- 若麵團長度小於 $\lceil \dfrac{a_{i-1}}{x} \rceil$ 則不需進行分割,但須更新前一段麵團最小的長度
- $\lfloor \dfrac{a}{b} \rfloor$:`a / b`
- $\lceil \dfrac{a}{b} \rceil$:`(a-1) / b + 1`
---
<!-- .slide: data-background="https://i.imgur.com/2be4qrQ.jpg" -->
<font color="black">
<font size=7><b>蜂巢清理 (Hive)</b></font><br>
<font size=6><b>Source: LeetCode 2290</b></font><br>
<font size=6><b>Prepared by WiwiHo</b></font><br>
</font>
----
子任務 1
$R_u = 0, R_v = 0$
----
所有堆積灰塵的房間都在 $w$ 軸上
----
- 若 $M < 2N+1$ 則中間一定有縫隙可以通過
→ 答案為 $0$
- 若 $M = 2N+1$ 則需判斷 $S$ 跟 $T$ 是否在同一側
→ 同一側答案為 $0$,不同側答案為 $1$
----
判斷在 $w$ 軸的哪側:
$u$ 與 $v$ 方向皆為遠離 $w$ 軸
→ $u+v > 0$ 與 $u+v < 0$ 分別代表 $w$ 軸兩側
----
處理六邊形座標:$w = -u + v$
→ 將座標簡化成只剩下 $u$ 和 $v$ 軸
→ $x = u-w, y = v+w$
----
子任務 2
$M=3N^2+3N-1$
----
所有 $S$ 和 $T$ 以外的房間皆堆積灰塵
→ 答案為 $S$ 和 $T$ 之間的最短路徑長度
----
設原點座標為 $(0,0)$
$x$ 座標上下界為 $[-N, N]$
$y$ 座標上下界:
- 若 $x \ge 0$,$y \in [-N, N-x]$
- 若 $x \le 0$,$y \in [-x-N, N]$
→ 轉換為類似在二維長方形表格上問題
----
子任務 3
$N \le 10, M \le 10$
----
枚舉要打掃哪些房間,
從 $S$ 進行 BFS 檢查是否能走到 $T$
時間複雜度 $O(2^MN^2)$
($|V| = 3N^2+3N+1, |E| < 6|V|$)
----
子任務 4
$N \le 250$
轉換成圖後進行 Dijkstra 尋找最短路
時間複雜度 $O(N^2\log{N})$
----
子任務 5
無額外限制
----
Dijkstra 利用 `priority_queue` 維護距離最小的節點
每次更新時間複雜度 $O(\log{N})$
----
轉換成圖後,邊權只會是 $0$ 或 $1$
→ `priority_queue` 中節點可能的距離只會是 $d$ 或 $d+1$
→ 改用 `deque` 維護:
- 若邊權為 $0$,則將節點加入 `deque` 前端
- 若邊權為 $1$,則將節點加入 `deque` 尾端
每次更新時間複雜度 $O(1)$
總時間複雜度 $O(N^2)$
---
<!-- .slide: data-background="https://i.imgur.com/c4j3Vwl.jpg" -->
<font color="black">
<font size=7><b>維度裂縫 (Rift)</b></font><br>
<font size=6><b>Prepared by WiwiHo</b></font><br>
</font>
----
在一張圖上找一條 $K$ 個點的路徑使邊權總和最大
----
這是一個 NP-Complete 問題
----
不過你亂做總是會有分數的 (?)
----
## 測資 1
$K=3$
枚舉中間那個點,找兩個它最大權重的鄰邊
然後你就有 5 分了
----
## DAG
在一張 DAG 上找 $K$ 點的最長路徑只需要 $O(NK)$ 的時間
----
random 拓樸排序,然後把邊定向
假設最佳解是唯一的
那麼你會有 $2/k!$ 的機率拿到最佳解
----
$2/8! \approx 0.005\%$
$2/10! \approx 0.00005\%$
----

----
```cpp=
#include <bits/stdc++.h>
#define StarBurstStream ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0);
#define iter(a) a.begin(), a.end()
#define lsort(a) sort(iter(a))
#define eb(a) emplace_back(a)
#define mp(a, b) make_pair(a, b)
#define F first
#define S second
#define printv(a, b) {bool pvaspace=false; \
for(auto pva : a){ \
if(pvaspace) b << " "; pvaspace=true;\
b << pva;\
}\
b << "\n";}
using namespace std;
typedef long long ll;
using pii = pair<int, int>;
const ll MAX = 1000000000;
int main(){
StarBurstStream
int n, m, K;
cin >> n >> m >> K;
vector<vector<pii>> g(n + 1);
for(int i = 0; i < m; i++){
int u, v, w;
cin >> u >> v >> w;
g[u].eb(mp(v, w));
g[v].eb(mp(u, w));
}
mt19937 rnd(4235);
int ans = 0;
vector<int> arr;
for(int tt = 0; tt <= 100000; tt++){
vector<int> tmp(n);
iota(iter(tmp), 1);
shuffle(iter(tmp), rnd);
vector<vector<int>> dp(n + 1, vector<int>(K + 1, -MAX));
vector<vector<int>> f(n + 1, vector<int>(K + 1, -1));
vector<bool> vst(n + 1);
int ansp = -1;
for(int i : tmp){
vst[i] = true;
dp[i][1] = 0;
for(int j = 1; j < K; j++){
for(auto [v, w] : g[i]){
if(vst[v]) continue;
if(dp[i][j] + w <= dp[v][j + 1]) continue;
dp[v][j + 1] = dp[i][j] + w;
f[v][j + 1] = i;
}
}
if(dp[i][K] > ans){
ans = dp[i][K];
ansp = i;
}
}
if(ansp == -1) continue;
arr.clear();
int t = K;
while(ansp != -1){
arr.eb(ansp);
ansp = f[ansp][t];
t--;
}
}
printv(arr, cout);
return 0;
}
```
----
## Color Coding
假設每個點都是 $K$ 種顏色的其中一種
答案的路徑必須滿足 $K$ 個點顏色不同
那你可以在 $O(N^22^K)$ 找到最佳解
----
假設最佳解唯一
你會有 $K!/K^K$ 的機率拿到最佳解
----
$8!/8^8 \approx 0.24\%$
$10!/10^{10} \approx 0.04\%$
----
```cpp=
#include <bits/stdc++.h>
#define StarBurstStream ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0);
#define iter(a) a.begin(), a.end()
#define lsort(a) sort(iter(a))
#define eb(a) emplace_back(a)
#define mp(a, b) make_pair(a, b)
#define F first
#define S second
#define printv(a, b) {bool pvaspace=false; \
for(auto pva : a){ \
if(pvaspace) b << " "; pvaspace=true;\
b << pva;\
}\
b << "\n";}
using namespace std;
typedef long long ll;
using pii = pair<int, int>;
const ll MAX = 1000000000;
int main(){
StarBurstStream
int n, m, K;
cin >> n >> m >> K;
vector<vector<pii>> g(n + 1);
for(int i = 0; i < m; i++){
int u, v, w;
cin >> u >> v >> w;
g[u].eb(mp(v, w));
g[v].eb(mp(u, w));
}
mt19937 rnd(123123);
uniform_int_distribution<int> ud(0, K - 1);
int ans = 0;
vector<int> arr;
for(int tt = 0; tt <= 10000; tt++){
vector<int> clr(n + 1);
for(int i = 1; i <= n; i++){
clr[i] = ud(rnd);
}
vector<vector<int>> dp(n + 1, vector<int>(1 << K, -MAX));
vector<vector<int>> f(n + 1, vector<int>(1 << K, -1));
for(int i = 1; i <= n; i++){
dp[i][1 << clr[i]] = 0;
}
for(int i = 0; i < (1 << K); i++){
for(int j = 1; j <= n; j++){
if(dp[j][i] == -MAX) continue;
for(auto [v, w] : g[j]){
if(1 << clr[v] & i) continue;
int nxt = 1 << clr[v] | i;
if(dp[j][i] + w <= dp[v][nxt]) continue;
dp[v][nxt] = dp[j][i] + w;
f[v][nxt] = j;
}
}
}
int ansp = -1;
for(int i = 1; i <= n; i++){
if(dp[i][(1 << K) - 1] > ans){
ans = dp[i][(1 << K) - 1];
ansp = i;
}
}
if(ansp == -1) continue;
int msk = (1 << K) - 1;
arr.clear();
while(ansp != -1){
arr.eb(ansp);
int nmsk = msk ^ (1 << clr[ansp]);
ansp = f[ansp][msk];
msk = nmsk;
}
}
printv(arr, cout);
return 0;
}
```
---
<!-- .slide: data-background="https://i.imgur.com/eUdJwHI.jpg" -->
<font color="black">
<font size=7><b>赤紅薔薇 (Rose)</b></font><br>
<font size=6><b>Source: Codeforces 1535E</b></font><br>
<font size=6><b>Prepared by __shaun_0124</font><br>
</font>
----
## Subtask 1
$Q \leq 5000$
記錄每個節點的父節點
每次發生第二種事件的時候
找出 $v_i$ 到根節點的路徑
然後暴力即可
----
## Subtask 2
$p_i=0$
所有節點到根節點的路徑長度只有 2
----
## Subtask 3
$p_i \neq p_j$
每個人的父節點都相異
就是樹是一條鏈的意思
----
每次第二種事件都是從父節點開始往下拿
令 $l$ 為父節點往下走第一個數量不是 0 的節點
第二種事件其實就是在移動 $l$
$O(Q)$
----
## Subtask 4
所有的第一種事件都發生在所有的第二種事件前
如果我們可以花 $O(P)$ 的時間
找到根節點到 $v_i$ 的路徑上第一個數量不是 0 的節點
那我們就只要花 $O(PQ)$ 的時間
----
蓋倍增表,二分搜
$O(Q \log Q)$
----
## Subtask 5
跟前一個子題不一樣的地方是
這題會不斷加點
----
加點的時候也可以蓋倍增表!
----
```cpp=
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define FOR(i,l,r,d) for(int i=(l);i<=(r);i+=(d))
#define vi vector<int>
#define pii pair<int,int>
#define F first
#define S second
#define mkp make_pair
const int MAX = 300001;
const int lgMAX = 20;
int q;
int pa[MAX];
int anc[MAX][lgMAX];
int cnt[MAX], cost[MAX];
int dep[MAX];
int find_anc(int v){
/// find first anc. of v that still has gold
if(cnt[v] == 0){
return -1;
}
int ret = v;
for(int i=19; i>=0; i--){
if(cnt[ anc[ret][i] ] > 0){
ret = anc[ret][i];
}
}
return ret;
}
pii buy(int v, int buy_cnt){
/// try to buy buy_cnt units of gold from find_anc(v)
/// return : <units bought, money spent>
int Anc = find_anc(v);
if(Anc==-1){
return mkp(0, 0);
}
int bought, spent;
if(cnt[Anc] < buy_cnt){
bought = cnt[Anc];
cnt[Anc] = 0;
}
else{
bought = buy_cnt;
cnt[Anc] -= bought;
}
spent = bought * cost[Anc];
return mkp(bought, spent);
}
signed main(signed argc, char* argv[]){
int pv_ans = 0;
/// init
cin>>q;
pa[0] = 0;
FOR(i, 0, 19, 1){
anc[0][i] = 0;
}
cin>>cnt[0];
cin>>cost[0];
FOR(i, 1, q, 1){
int type;
int v, w;
cin>>type;
if(type==1){
cin>>pa[i]>>cnt[i]>>cost[i];
pa[i] ^= pv_ans;
cnt[i] ^= pv_ans;
cost[i] ^= pv_ans;
anc[i][0] = pa[i];
FOR(j, 1, 19, 1){
anc[i][j] = anc[ anc[i][j-1] ][j-1];
}
}
else if(type==2){
cin>>v>>w;
v ^= pv_ans;
w ^= pv_ans;
int bought = 0;
int spent = 0;
while(w > 0){
pii ret = buy(v, w);
bought += ret.F;
spent += ret.S;
w -= ret.F;
if(ret.F==0){
break;
}
}
cout<<spent<<'\n';
pv_ans = spent % (1<<20);
}
}
return 0;
}
```
---
<!-- .slide: data-background="https://i.imgur.com/dPCaFop.jpg" -->
<font color="black">
<font size=7><b>夢幻之塔 (Tower)</b></font><br>
<font size=6><b>Source: 2022 宜蘭高中校內賽 pF</b></font><br>
<font size=6><b>Prepared by Ccucumber12</b></font><br>
</font>
----
選擇一些高塔使得它們的高度是高低交錯
且權重和最大
----
## Subtask 1
$h_i \leq 2$
高度只有 1 和 2
1 一定是低,2 一定是高
$dp[i]=$ 以第 $i$ 個塔為選到的最右邊的塔的答案
記錄目前 $h_i=1$ 和 $2$ 的最大 $dp[i]$
----
## Subtask 3
枚舉
$O(N2^N)$
----
## Subtask 4
$N \leq 1000$
$dp[0/1][i]=$ 以第 $i$ 個塔為選到的最右邊的塔
且第 $i$ 個塔是低/高點的答案
$dp[0][i]=\max_{j < i \land h_j > h_i}\{ dp[1][j] \} + v_i$
$dp[1][i]=\max_{j < i \land h_j < h_i}\{ dp[0][j] \} + v_i$
$O(N^2)$
----
## Subtask 2
$h \leq 2 \times 10^5$
$mx[0/1][v]=$ 目前為止找到的最大 $dp[0/1][i]$ 滿足 $v_i=v$
用 BIT / 線段樹維護,可以做到 $O(\log N)$ 轉移
$O(N \log N)$
----
## Subtask 5
離散化
AC
----
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std ;
using ll = long long ;
void discrete(vector<int> &v) {
vector<int> tmp = v ;
sort(tmp.begin(), tmp.end()) ;
tmp.resize(unique(tmp.begin(), tmp.end()) - tmp.begin()) ;
for(auto &i:v)
i = (lower_bound(tmp.begin(), tmp.end(), i) - tmp.begin() + 1) ;
}
const int N = 200010 ;
int mxn = 1 ;
ll dp0[4*N], dp1[4*N] ;
void modify(ll arr[], int idx, ll val) {
idx += mxn - 1 ;
arr[idx] = max(arr[idx], val) ;
while(idx > 1) {
idx >>= 1 ;
arr[idx] = max(arr[idx*2], arr[idx*2+1]) ;
}
}
ll query(ll arr[], int l, int r, int lb, int rb, int idx) {
if(l > r) return 0 ;
if(l <= lb && rb <= r)
return arr[idx] ;
int mid = (lb + rb) >> 1 ;
ll ans = 0 ;
if(l <= mid) ans = max(ans, query(arr, l, r, lb, mid, idx*2)) ;
if(mid < r) ans = max(ans, query(arr, l, r, mid+1, rb, idx*2+1)) ;
return ans ;
}
int main() {
ios::sync_with_stdio(false) ;
cin.tie(0) ; cout.tie(0) ;
int n ;
cin >> n ;
vector<int> h(n), v(n) ;
for(int i=0; i<n; ++i)
cin >> h[i] >> v[i] ;
discrete(h) ;
while(mxn < n)
mxn <<= 1 ;
for(int i=0; i<n; ++i) {
modify(dp0, h[i], query(dp1, h[i]+1, mxn, 1, mxn, 1) + v[i]) ;
modify(dp1, h[i], query(dp0, 1, h[i]-1, 1, mxn, 1) + v[i]) ;
}
cout << max(dp0[1], dp1[1]) << '\n' ;
}
```
{"metaMigratedAt":"2023-06-17T07:25:18.751Z","metaMigratedFrom":"YAML","title":"模競 I 題解","breaks":true,"contributors":"[{\"id\":\"02353542-5acb-4a66-abd0-128b1af24abb\",\"add\":10040,\"del\":476},{\"id\":\"6b95215f-91a7-4eaf-bcfe-d43740108f96\",\"add\":3095,\"del\":167}]"}