###### tags: `OI` # 111-2 延平中學校內賽題解 以下題解如果有問題,歡迎找出題者討論: * E-mail: dave910602@gmail.com * Facebook: 郭軒語(Dave Kuo) * Discord: HNO2#4672 特別感謝 SorahISA 幫忙驗題。 ## pA. 取餘數 ### Subtask 1 直接讀入三個數字後,判斷 $a_0 \bmod a_1 = a_2$ 是否成立即可。需要特別判掉 $a_1 = 0$。 ### Subtask 2 用三層迴圈枚舉 $i,j,k$,分別檢查是否有 $a_i \bmod a_j = a_k$ 即可。 :::spoiler AC code ```cpp= #include <iostream> #include <vector> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; vector<int> a(n); for (int &i : a) cin >> i; 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 (a[j] != 0 && a[i] % a[j] == a[k]) ans++; cout << ans << '\n'; } ``` ::: ## pB. 上課座位 ### Subtask 1 使用二維陣列讀進整個座位表 (可以用 `char[*][*]` 或 `vector<string>` 等等),假設讀進去的陣列是 `seats`。 之後對於每個 `seats[i][j] == '0'` 的位置去看 `seats[i-1][j] == '0'`, `seats[i+1][j] == '0'`, `seats[i][j-1] == '0'`, `seats[i][j+1] == '0'` 是否都成立。 這裡提供一個實作時的技巧:可以把要判斷的方向都存成一個向量 array (也就是類似 `dx[4]={0,0,1,-1}; dy[4]={1,-1,0,0}` 這樣),之後用一個 for loop 判斷所有方向。 ### Subtask 2 和上一個 Subtask 類似,只是需要加上判斷每個方向是否有可能超出二微陣列範圍。 :::spoiler AC code ```cpp= #include <iostream> #include <vector> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, m; cin >> n >> m; vector<string> seats(n); for (auto &i : seats) cin >> i; vector<int> dx = {0, 0, 1, -1}; vector<int> dy = {1, -1, 0, 0}; auto valid = [&](int x, int y) { return x >= 0 && x < n && y >= 0 && y < m; }; int ans = 0; for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) { if (seats[i][j] == '1') continue; bool ok = 1; for (int k = 0; k < 4; k++) { if (valid(i + dx[k], j + dy[k]) && seats[i + dx[k]][j + dy[k]] == '1') ok = 0; } if (ok) ans++; } cout << ans << '\n'; } ``` ::: ## pC. 序列價值 ### Subtask 1 枚舉所有可能的區間 $[l,r]$,之後檢查是否區間內的數字都一樣,然後找出裡面最大的。檢查區間內是否數字都一樣,可以看是否存在相鄰數字不一樣。 ### Subtask 2 和上一個 subtask 類似,只是在枚舉 $r$ 的時候可以順便看相鄰數字是否都一樣。類似這樣: ```cpp= for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (j > i + 1 && a[j] != a[j - 1]) break; // do something } } ``` ### Subtask 3 可以注意到一件事:當所有數字都非負時,固定右界時左界一定是取一直往左延伸的最大區間一定最好。所以我們可以由左到右枚舉每個數字,並且同時計算 $cur,cnt$ 兩個變數,代表目前右界的值以及連續出現了幾次。當要從 $i$ 變到 $i+1$ 時,如果 $cur$ 和 $a_{i+1}$ 相同就把 $cnt$ 加 $1$,否則就把 $cnt$ reset 為 $1$。之後再更新 $cur$。 ### Subtask 4 當至少有一個非負整數時和 Subtask 3 一樣,但是當全部數字都是非負時要輸出最大的負數。 :::spoiler AC code ```cpp= #include <iostream> #include <vector> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; vector<int> a(n); for (int &i : a) cin >> i; long long ans = a[0], last = 0, cnt = 0; for (int i : a) { if (i == last) cnt++; else last = i, cnt = 1; ans = max(ans, last * cnt); } cout << ans << '\n'; } ``` ::: ## pD. 切棍子問題 ### Subtask 1 直接照題意模擬即可。具體實作方法是用一個 array 紀錄每個切點有沒有被切過,詢問時從那個詢問點開始往左往右找。 ### Subtask 2 所有詢問都在所有裁切的操作之後,所以我們可以先求出每一段棍子長度是多少。要求出每一段是多少的話,可以由左而右掃過每個切點,前後兩個切點之間長度就是兩者之差,把這個差 assign 給兩切點之間中間的所有詢問點。(雖然每一段長度都不一樣,但是加起來是 $n$ 所以可以放心開一個迴圈直接改。) 之後每筆詢問就可以 $O(1)$ 回答。 ### Subtask 3 我們用一個 `std::set` 紀錄所有的切點,一開始這個 set 只有 $\{0,n\}$。之後當遇到一個 query = 1 時,把這個切點放入 set; 當遇到一個 query = 2 時,找出這個 set 裡第一個比 $x$ 大的點,他和前一個的差就是答案。 實作上需要注意「找出這個 set 裡第一個比 $x$ 大的點」這個操作,對應到 STL set 操作就是 `s.lower_bound(x)`, 他個前一個 iterator 可以用 `prev(it)` 找到。注意不能寫 `lower_bound(s.begin(), s.end(), x)`,因為 set 沒有 random access 的功能,所以這個操作實際上是 $O(n \log n)$。 這題也有別種做法,例如說使用 DSU + 時光倒流可以做到 $\mathcal{O}(q \alpha(n))$,或者是使用區間資料結構直接找到每個詢問點左邊和右邊的切點,都是很經典的作法。 :::spoiler AC code ```cpp= #include <iostream> #include <set> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, q; cin >> n >> q; set<int> s = {0, n}; while (q--) { int type, x; cin >> type >> x; if (type == 1) { s.insert(x); } else { auto nearest = s.lower_bound(x); auto prev_nearest = prev(nearest); cout << (*nearest) - (*prev_nearest) << '\n'; } } } ``` ::: ## pE. 裝飾組裝 ### Subtask 1 因為 $n$ 很小,所以可以嘗試所有可能的組裝方式:$n=1,2$ 有一種,$n=3$ 有三種。找出所有裡面最小的即可。 ### Subtask 2 有一個直觀的想法是,把所有東西切成均勻大小的兩半,之後兩半都合併完成以後再將兩邊合併。 先講結論:這個作法是對的,正確性證明可以在 subtask 4 得到。所以我們可以用一個遞迴函式去 `f(x)` 去計算當有 $x$ 個裝飾品的最小費用。時間複雜度是 $O(n \log n)$。 ### Subtask 3 和 Subtask 4 作法一樣,只是 Subtask 4 需要用到 Heap,這個 subtask 就是給不會用或不知道 Heap 的。 ### Subtask 4 和經典問題 [Add all](https://zerojudge.tw/ShowProblem?problemid=d221) 的做法很像,維護一個 min heap (也就是 `std::priority_queue`)。把所有重量放進 heap,每次拿重量最小的兩個合併起來,只是合併的代價變成 $2(x+y)$。 這個做法為什麼是對的呢?Add all 的原型是 [Huffman code](https://en.wikipedia.org/wiki/Huffman_coding),證明也和他很像。簡單來說就是合併的過程可以看做一棵二元樹,然後要證明以下兩件事: 1. 在固定 tree structure 的情形下,最佳解一定會有兩個重量最小的 code 是最深的相鄰的節點。 2. 當把這兩個點合併起來以後,剩下 $n-1$ 個點繼續遞迴下去做得到的結果一定最優。 1 可以使用排序不等式等方式證明,2 可以使用反證法證明,如果有興趣可以自行查閱詳細證明 ([資訊之芽 Greedy 那一篇](https://www.csie.ntu.edu.tw/~sprout/algo2023/ppt_pdf/week05/greedy.pdf)就講得不錯,可以從第 23 頁開始看。) :::spoiler AC code ```cpp= #include <iostream> #include <queue> using namespace std; using ll = long long; int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; priority_queue<ll, vector<ll>, greater<>> pq; for (int i = 1; i <= n; i++) { ll x; cin >> x; pq.push(x); } while (pq.size() > 1) { ll x = pq.top(); pq.pop(); ll y = pq.top(); pq.pop(); pq.push((x + y) * 2); } cout << pq.top() << '\n'; } ``` ::: ## pF. 費氏數列問題 ### Subtask 1 因為範圍很小,$1^i, 2^i, 3^i, F_i$ 都可以直接寫在 code 裡面,就可以直接計算。 ### Subtask 2 這個 subtask 主要的任務在計算 $a^i, b^{n-i}$ 和 $F_i$。$a^i$ 可以直接用一個陣列計算,大概類似這樣: ```cpp= A[0] = 1; for (int i = 1; i <= n; i++) A[i] = A[i - 1] * a % MOD; ``` $F_i$ 可以使用簡單的 DP 算出來。小心 mod 運算不要 overflow! ### Subtask 3 從這個 subtask 開始需要考慮矩陣。考慮以下矩陣: $M = \begin{bmatrix} 1 & 1 & 0 \\[0.3em] 0 & 1 & 1 \\[0.3em] 0 & 1 & 0 \end{bmatrix}$ $b_i = \begin{bmatrix} sum_i \\[0.3em] F_{i+1} \\[0.3em] F_{i} \end{bmatrix}$ 其中 $sum_i = F_0 + \cdots + F_i$。則可以發現 $b_i = Mb_{i-1}$,因此 $b_n$ = $M^n b_0$。由於矩陣乘法有結合律,因此 $M^n$ 可以先算,而這一項可以使用快速冪的方法算出來。[這裡](https://hackmd.io/@fdhscpp110/matix_fast_pow)有這個技巧的詳細說明。 ### Subtask 4 和上一個 Subtask 很像,只是矩陣變成 $M = \begin{bmatrix} 1 & 1 & 0 \\[0.3em] 0 & a & a^2 \\[0.3em] 0 & 1 & 0 \end{bmatrix}$ $b_i = \begin{bmatrix} sum_i \\[0.3em] a^{i+1}F_{i+1} \\[0.3em] a^iF_{i} \end{bmatrix}$ $sum_i = a^0F_0 + \cdots + a^iF_i$ ### Subtask 5 和 Subtask 3,4 很像,可以注意到一件事:把整條式子整理一下以後就能得到 $b^n(\sum_{i=1}^n (ab^{-1})^i F_i)$。裡面那一項和 Subtask 4 一樣的做法,之後再乘上 $b^n$ 就是答案。 有一個小問題是 $b^{-1}$ 在 mod 底下代表的意義是什麼。事實上有個東西稱為模反元素 (或模逆元),就是在 mod 底下滿足 $x^{-1} \cdot x \bmod 10^9+7 = 1$ 的 $x^{-1}$。而因為 $10^9+7$ 是質數,根據費馬小定理這個數字就是 $b^{(10^9+7)-2} \bmod 10^9+7$。 :::spoiler 有關這題的 fun fact 這題在比賽前 8 個小時被檢查到官解是錯的。幸好我有自己再檢查... ::: :::spoiler AC code ```cpp= #include <cstring> #include <iostream> using namespace std; using ll = long long; constexpr ll MOD = 1'000'000'007; struct matrix { ll A[3][3]; void set_empty() { memset(A, 0, sizeof A); } void set_identity() { set_empty(); A[0][0] = A[1][1] = A[2][2] = 1; } matrix operator*(const matrix &rhs) const { matrix ret; ret.set_empty(); for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) for (int k = 0; k < 3; k++) { ret.A[i][j] = (ret.A[i][j] + A[i][k] * rhs.A[k][j]) % MOD; } return ret; } } m; ll pow_num(ll x, ll y) { ll ret = 1; while (y) { if (y & 1) ret = ret * x % MOD; y >>= 1; x = x * x % MOD; } return ret; } matrix pow_matrix(matrix x, ll y) { matrix ret; ret.set_identity(); while (y) { if (y & 1) ret = ret * x; x = x * x; y >>= 1; } return ret; } int main() { ios::sync_with_stdio(0); cin.tie(0); ll a, b, n; cin >> a >> b >> n; ll f = a * pow_num(b, MOD - 2) % MOD; ll f2 = f * f % MOD; matrix m; m.A[0][0] = 1, m.A[0][1] = 1, m.A[0][2] = 0; m.A[1][0] = 0, m.A[1][1] = f, m.A[1][2] = f2; m.A[2][0] = 0, m.A[2][1] = 1, m.A[2][2] = 0; matrix m_pow = pow_matrix(m, n); ll ans = m_pow.A[0][1] * f % MOD; cout << ans * pow_num(b, n) % MOD << '\n'; } ``` ::: ## pG. 寶石城 在這題中,我們用圖論的術語:每個小鎮是一個節點,而每座橋是一條邊。 ### Subtask 1 在 Subtask 1~3 中,因為樹的結構是一條長鍊,所以可以直接把他當作序列處理。在這個 Subtask 只需要依題意列舉所有 $[l,r]$ 之間的所有長度是 $1$ 或 $2$ 的子序列即可。注意 $l>r$ 的情形! ### Subtask 2 當所有詢問長度是 $1$ 時,相當於詢問一個序列中每個區間裡面有多少同樣的字元。這個可以使用前綴和的技巧做到。 ### Subtask 3 我們需要支援一個字串的一個區間有多少個子序列可以是 $qs_i$。最簡單的方法一樣是建立前綴和:對於每個長度是 $1$ 和 $2$ 的字串計算每個前綴有多少個這個字串。具體算長度是 $2$ 的前綴的方法:假設要算 $[1,r]$ 出現了幾次 `ac`,其實他就是 $[1,r-1]$ 出現了幾次 `ac` 再加上$[1,r-1]$ 出現了幾次 `a` (如果 $s_r$ 是 `c`)。 長度是 $1$ 的很簡單,長度是 $2$ 的話,假設我們要計算 `s = ac` 在 $[l,r]$ 出現了幾次,那運用前綴和就是 `ac` 在 $[1,r]$ 出現了幾次,扣掉在 $[1,l-1]$ 出現了幾次 `ac`,以及扣掉 $[1,l-1]$ 出現 `a` 的次數乘以 $[l,r]$ 出現的 `c` 的次數。 ### Subtask 4 接下來的測資都是在樹上進行。Subtask 4 & 5 的話因為 $n,q$ 很小,可以每筆在 $O(n)$ 時間內算出。這筆只需要用 DFS 求出 $u_i$ 到 $v_i$ 的路徑,之後算出有幾個字元是 $qs_i[0]$。 ### Subtask 5 和 Subtask 4 類似,只是要在 $O(n)$ 時間內算一條路徑上有多少個子字串是 $qs_i$。可以用前面 Subtask 3 算前綴和的方法求出。 ### Subtask 6 這個 Subtask 相當於以下問題:給你一棵樹,其中有些節點已經塗黑,請求出一條路徑上有多少個染色點。這個問題其中一個最經典的做法就是倍增:對於每個節點 $i$,我們求出他到 $2^j, j = 0,\cdots,\log n$ 層祖先有多少個染黑點,之後用類似求 LCA 的方法算出答案,只是我們在算 LCA 的同時要同時算有多少個黑點。[這裡](https://yuihuang.com/lowest-common-ancestor/)有一個很完整的倍增法求 LCA 的說明。 ### Subtask 7 和 Subtask 6 一樣用倍增法,只是要維護的東西變多很多。可以看 AC code 取得更完整的細節理解。 :::spoiler AC code ```cpp= #include <iostream> #include <vector> using namespace std; constexpr int MAXN = 300'007, LOG_MAXN = 21; vector<int> G[MAXN]; string str; int depth[MAXN]; int par[MAXN][LOG_MAXN], s[MAXN][LOG_MAXN][3]; long long d[MAXN][LOG_MAXN][9]; void dfs(int now, int p) { par[now][0] = p; for (int i : G[now]) { if (i == p) continue; depth[i] = depth[now] + 1; dfs(i, now); } } int solve_single(int u, int v, string query) { if (depth[u] < depth[v]) swap(u, v); int target = query[0] - 'a'; int cntu = 0, cntv = 0; for (int i = 20; i >= 0; i--) { if ((depth[u] - depth[v]) >> i & 1) { cntu += s[u][i][target]; u = par[u][i]; } } if (u == v) return cntu + s[u][0][target]; for (int i = 20; i >= 0; i--) { if (par[u][i] != par[v][i]) { cntu += s[u][i][target]; cntv += s[v][i][target]; u = par[u][i]; v = par[v][i]; } } return cntu + cntv + s[u][1][target] + s[v][0][target]; } long long solve_double(int u, int v, string query) { if (depth[u] < depth[v]) { swap(u, v); swap(query[0], query[1]); } int target1 = query[0] - 'a', target2 = query[1] - 'a'; int u_s1 = target1, u_s2 = target2, u_d = target1 * 3 + target2; int v_s1 = target2, v_s2 = target1, v_d = target2 * 3 + target1; long long cntu_s = 0, cntu_d = 0, cntv_s = 0, cntv_d = 0; for (int i = 20; i >= 0; i--) { if ((depth[u] - depth[v]) >> i & 1) { cntu_d += d[u][i][u_d] + cntu_s * s[u][i][u_s2]; cntu_s += s[u][i][u_s1]; u = par[u][i]; } } if (u == v) { return cntu_d + cntu_s * s[v][0][v_s1]; } for (int i = 20; i >= 0; i--) { if (par[u][i] != par[v][i]) { cntu_d += d[u][i][u_d] + cntu_s * s[u][i][u_s2]; cntu_s += s[u][i][u_s1]; u = par[u][i]; cntv_d += d[v][i][v_d] + cntv_s * s[v][i][v_s2]; cntv_s += s[v][i][v_s1]; v = par[v][i]; } } cntu_d += d[u][1][u_d] + cntu_s * s[u][1][u_s2]; cntu_s += s[u][1][u_s1]; cntv_d += d[v][0][v_d] + cntv_s * s[v][0][v_s2]; cntv_s += s[v][0][v_s1]; return cntu_d + cntv_d + cntu_s * cntv_s; } int main() { ios::sync_with_stdio(0); cin.tie(0); int n, q; cin >> n >> q; cin >> str; for (int i = 1; i <= n - 1; i++) { int u, v; cin >> u >> v; G[u].push_back(v); G[v].push_back(u); } dfs(1, -1); for (int i = 1; i <= n; i++) s[i][0][str[i - 1] - 'a'] = 1; for (int level = 1; level <= 20; level++) for (int i = 1; i <= n; i++) { int prev_par = par[i][level - 1]; if (prev_par == -1) { par[i][level] = -1; for (int k = 0; k < 3; k++) s[i][level][k] = s[i][level - 1][k]; for (int k = 0; k < 9; k++) d[i][level][k] = d[i][level - 1][k]; } else { par[i][level] = par[prev_par][level - 1]; for (int k = 0; k < 3; k++) s[i][level][k] = s[i][level - 1][k] + s[prev_par][level - 1][k]; for (int k = 0; k < 9; k++) { d[i][level][k] = d[i][level - 1][k] + d[prev_par][level - 1][k]; int first_char = k / 3, last_char = k % 3; d[i][level][k] += s[i][level - 1][first_char] * s[prev_par][level - 1][last_char]; } } } while (q--) { int u, v; string query; cin >> u >> v >> query; if (query.length() == 1) { cout << solve_single(u, v, query) << '\n'; } else { cout << solve_double(u, v, query) << '\n'; } } } ``` ::: ## pH. 大樓拆除 這題原題是 2022-2023 Winter Petrozavodsk Camp, Day 4: KAIST+KOI Contest 的 pD: Building Bombing。([Link](https://codeforces.com/gym/104345/problem/D),有稍微簡化) ### Subtask 1 注意到第一棟樓一定不會被拆掉,所以最後留下的一定只有第一棟樓。因此第二棟以後嚴格比他高的都要拆掉。 ### Subtask 2 第一棟被看到樓一定是最左邊的,那第二棟被看到的會是哪一棟呢?我們可以枚舉所有可能的第二棟樓 $i$ (要比 $h_1$ 高),之後算出 ($[2,i-1]$ 裡比第一棟高的) + ($[i+1,n]$ 裡比第二棟高的) 最小值。前面那一項就是一個前綴和,後面那一項可以使用一個 BIT 或線段樹算出來。 ### Subtask 3 考慮 DP。令 $dp[i][j]$ 代表一共累積了 $j$ 棟樓,而第 $j$ 高被看到的樓是 $i$ 時最少需要拆幾棟樓。我們有以下 DP 轉移: $dp[i][j] = \min_{l > i, h_l > h_i} dp[l][j-1] + sum(i+1,l-1,h_i)$ 其中 $sum(l,r,x)$ 代表區間 $[l,r]$ 中有多少 $h_i$ 大於 $x$。初始值是 $dp[i][1] = sum(i+1,n,h_i)$,答案就是 $dp[1][k]$。 ### Subtask 4 和 Subtask 3 做法很像,只是如果直接依照上面 DP 式做是 $O(n^3k)$,但是可以發現 $sum(\cdot,\cdot,\cdot)$ 可以用前綴和表示,因此時間複雜度就可以降到 $O(n^2k)$。 ### Subtask 5 同樣是 Subtask 3 的轉移式,只是用線段樹加速轉移。 這裡特別說明一下怎麼用線段樹加速轉移。我們的運算順序是從 $dp[\cdot][j]$ 轉到 $dp[\cdot][j+1]$,轉移的順序是 $h_i$ 由大到小。線段樹支援區間加值和區間詢問最小值,一開始時每一格的值是 $dp[i][j-1] + INF$,$INF$ 是一個充分大的常數。當要轉移 $dp[i][j]$ 時,我們假設 $h_l > h_i$ 的 $dp[l][j]$ 都已經算過。我們做以下事情: * $dp[i][j]$ 設為 (線段樹中 $[i+1,n]$ 中最小值) + ($[i+1,n]$ 中比 $h_i$ 大的有幾個) * 線段樹中第 $i$ 個位置減 $INF$ * 線段樹中 $[1,i-1]$ 加 $-1$ 這個的正確性不太好理解,大概描述一下:設置 $INF$ 的目的在於從正確的地方轉移,不要從 $h_l < h_i$ 的地方轉移。至於那個 $-1$ 的部分,可以想成兩個前綴相減,當從 $dp[l][j-1]$ 轉到 $dp[i][j]$ 時,我其實就是 $sum(i+1,n,h_i)$ 減 $sum(l+1,n,h_i)$,而那個減 $sum(l+1,n,h_i)$ 就是 $-1$ 的部分。 :::spoiler AC code ```cpp= #include <algorithm> #include <iostream> #include <vector> using namespace std; constexpr int MAXN = 1e5 + 7; constexpr int INF = 1e9; struct segtree { int mn[MAXN << 2], tag[MAXN << 2]; void init(int n) { for (int i = 0; i <= 4 * n; i++) mn[i] = tag[i] = 0; } void push(int now) { mn[now << 1] += tag[now]; tag[now << 1] += tag[now]; mn[now << 1 | 1] += tag[now]; tag[now << 1 | 1] += tag[now]; tag[now] = 0; } void pull(int now) { mn[now] = min(mn[now << 1], mn[now << 1 | 1]); } void modify(int now, int l, int r, int ql, int qr, int x) { if (ql > qr) return; if (l >= ql && r <= qr) { mn[now] += x; tag[now] += x; return; } int m = (l + r) >> 1; push(now); if (ql <= m) modify(now << 1, l, m, ql, qr, x); if (qr > m) modify(now << 1 | 1, m + 1, r, ql, qr, x); pull(now); } int query(int now, int l, int r, int ql, int qr) { if (ql > qr) return INF; if (l >= ql && r <= qr) { return mn[now]; } int m = (l + r) >> 1; push(now); if (qr <= m) return query(now << 1, l, m, ql, qr); else if (ql > m) return query(now << 1 | 1, m + 1, r, ql, qr); else return min(query(now << 1, l, m, ql, qr), query(now << 1 | 1, m + 1, r, ql, qr)); } } tree; struct bit { int bit[MAXN], n; void init(int n_) { n = n_; for (int i = 0; i <= n; i++) bit[i] = 0; } void add(int x, int dd) { for (int i = x; i <= n; i += (i & (-i))) bit[i] += dd; } int query(int x) { int ret = 0; for (int i = x; i > 0; i -= (i & (-i))) ret += bit[i]; return ret; } } b; int32_t main() { ios::sync_with_stdio(0); cin.tie(0); int n, k; cin >> n >> k; vector<int> a(n + 2); for (int i = 1; i <= n; i++) cin >> a[i]; a[n + 1] = INF + 1; vector<pair<int, int>> ord; for (int i = 1; i <= n + 1; i++) { ord.push_back(make_pair(a[i], -i)); } sort(ord.begin(), ord.end()); reverse(ord.begin(), ord.end()); vector<int> dp(n + 2, INF); dp[n + 1] = 0; for (int _ = 1; _ <= k; _++) { tree.init(n + 1); b.init(n + 1); for (int i = 1; i <= n + 1; i++) tree.modify(1, 1, n + 1, i, i, dp[i] + INF); vector<int> newdp(n + 2, INF); for (auto [val, pos] : ord) { pos = -pos; int base_val = b.query(n + 1) - b.query(pos); newdp[pos] = min(newdp[pos], base_val + tree.query(1, 1, n + 1, pos + 1, n + 1)); tree.modify(1, 1, n + 1, pos, pos, -INF); tree.modify(1, 1, n + 1, 1, pos, -1); b.add(pos, 1); } swap(newdp, dp); } cout << (dp[1] >= INF ? -1 : dp[1]) << '\n'; } ``` :::