--- tags: APCS --- # APCS 20210905 題解 [題目](https://hackmd.io/@yungyao/HyFRzzWfF) ## pC 這題唯一的難點是要找區間裡最小的數,其他都很簡單。 可以暴力地用能夠 RMQ 的資料結構來做,時間是 $O(n \log n)$,不過注意到這題的詢問區間肯定被上一個詢問包含,利用這個性質就可以不寫資料結構。 ### $O(n \log n)$ 版 把 $1$ 到 $n$ 按 $p_i$ sort,在算 $f(l,r)$ 的時候,看剛剛 sort 的那堆裡的第一個,假設它是 $p$,如果 $p$ 不在 $[l,r]$ 裡就可以把它丟了,以後也不會再用到,因為之後的詢問範圍也會在 $[l,r]$ 裡面,如果 $p$ 在 $[l,r]$ 裡,那 $p$ 就是你要的最小值位置。至於區間和用前綴和算。 :::spoiler code ```cpp= #include <bits/stdc++.h> #define StarBurstStream ios_base::sync_with_stdio(false); cin.tie(0); #define iter(a) a.begin(), a.end() using namespace std; typedef long long ll; int n; vector<int> a, p; vector<ll> s; int np = 0; int f(int l, int r){ if(l == r) return a[l]; while(p[np] < l || r < p[np]) np++; int m = p[np]; if(s[m - 1] - s[l - 1] > s[r] - s[m]){ return f(l, m - 1); } else{ return f(m + 1, r); } } int main(){ StarBurstStream cin >> n; a.resize(n + 1); p.resize(n); s.resize(n + 1); for(int i = 1; i <= n; i++){ cin >> a[i]; s[i] = s[i - 1] + a[i]; p[i - 1] = i; } sort(iter(p), [](int u, int v){ return a[u] < a[v]; }); cout << f(1, n) << "\n"; return 0; } ``` ::: ### $O(n)$ 版 (感謝 dreamoon 在某篇貼文下的留言讓我想到有這個作法) > Cartesian Tree 定義: > 一個數字皆相異的序列 $a_1,a_2,\dots,a_n$ 的 Cartesian Tree 是一個有 $n$ 個節點的數,每個節點上都有一個值,根節點的值為 $\min_{1 \leq i \leq n} a_i$,若這個值是 $a_m$,則左子樹為 $a_1,a_2,\dots,a_{m-1}$ 的 Cartesian Tree、右子樹為 $a_{m+1},a_{m+2},\dots,a_n$ 的 Cartesian Tree。空序列的 Cartesian Tree 定義為空樹。 可以發現到,題目的 $f(l,r)$ 遞迴過程就是從 Cartesian Tree 的根節點走的某個葉節點的路徑,根節點就是 $[1,n]$ 內的最小值,它的左右子樹就是最小值的左側和右側,因此 $f(l,r)$ 就變成了:如果左子樹的總和較右子樹大,就往左子樹走,否則往右子樹走。 $O(n)$ 蓋 Cartesian Tree 的方法是,從左邊看到右邊,把現在看到的數加進 Cartesian Tree 裡。因為加進 $a_i$ 的時候,它就是序列最右邊的那個數,所以它在 Cartesian Tree 上肯定是由根節點一路向右,直到沒有右子節點為止所到達的節點,因此只要在原本的樹上找到它應該放的位置即可。維護根節點一路往右走的路徑,這條路徑上的值是遞增的,找到(上到下)第一個 $> a_i$ 的節點,把它變成 $a_i$ 的左子節點,而 $a_i$ 的父節點就是它原本的父節點(或根)。 區間和可以邊蓋 Cartesian Tree 邊算。 :::spoiler code ```cpp= #include <bits/stdc++.h> #define StarBurstStream ios_base::sync_with_stdio(false); cin.tie(0); using namespace std; typedef long long ll; int n; vector<int> a; vector<ll> s; vector<int> l, r; // 左右子節點 int f(int now){ if(l[now] == 0 && r[now] == 0) return a[now]; if(s[l[now]] > s[r[now]]) return f(l[now]); else return f(r[now]); } int main(){ StarBurstStream cin >> n; a.resize(n + 1); s.resize(n + 1); l.resize(n + 1); r.resize(n + 1); stack<int> st; for(int i = 1; i <= n; i++){ cin >> a[i]; s[i] = a[i]; int lst = 0; while(!st.empty() && a[st.top()] > a[i]){ lst = st.top(); st.pop(); s[lst] += s[r[lst]]; } if(!st.empty()){ r[st.top()] = i; } st.push(i); l[i] = lst; s[i] += s[lst]; } int root; while(!st.empty()){ root = st.top(); s[root] += s[r[root]]; st.pop(); } cout << f(root) << "\n"; return 0; } ``` ::: ~~應該是不會有人想要在考試中寫這個~~ ## pD 定義如果一個區間當中沒有重複數字,則它是一個合法區間。 肯定有一個最佳解滿足其中每一個肯定滿足其中任兩個區間不相交,首先,當然不會有區間完全包含另一個區間,否則只要留下比較大的那個就好了,如果有兩個區間 $[l_1,r_1]$、$[l_2,r_2]$ 部分相交,也就是 $l_1 \leq l_2 \leq r_1 \leq r_2$,那可以換成 $[l_1,l_2-1]$、$[l_2,r_2]$,讓這兩個區間變得不相交、聯集範圍不變。 所以我們只要考慮所有區間都不重疊的狀況,令 $dp[i][j]=$ 選了 $i$ 個區間、最後一個區間右端點在 $j$ 或之前的答案,$\forall 0 \leq j \leq n,\ dp[0][j]=0$,轉移式: \\[dp[i][j]=\max\{dp[i-1][k-1] + j - k + 1 \mid k \leq j,\ [k,j] \text{ is valid}\}\\] 也就是在 $k$ 之前選 $i-1$ 個區間,再選 $[k, j]$。 令 $t(j)=$ 以 $j$ 為右端點的合法區間中,最左邊的左端點,上式中的 $k$ 範圍就是 $t(j) \leq k \leq j$,轉移式可以改為 \\[dp[i][j]=j+1 + \max\{dp[i-1][k-1] - k \mid t(j) \leq k \leq j\}\\] 所以目標就是在 $t(j) \leq k \leq j$ 的範圍裡找到最小的 $dp[i-1][k-1]-k$。注意到 $t(j)$ 是遞增的,因此可以用雙指標搭配 multiset 來做,時間複雜度是 $O(kn \log n)$。也可以用單調隊列,時間複雜度會降為 $O(nk)$。 計算 $t(j)$ 的方法是,從 $1$ 看到 $n$,記錄每一種數字出現的位置,$t(j)$ 的值就是「最右邊的某種數字倒數第二次出現的位置 $+1$」,因此可以在看到 $j$ 時,用 $a_j$ 上一次出現的位置更新這個值。 :::spoiler 單調隊列版 code ```cpp= #include <bits/stdc++.h> #define StarBurstStream ios_base::sync_with_stdio(false); cin.tie(0); #define eb(a) emplace_back(a) #define pob pop_back() #define pof pop_front() #define mp(a, b) make_pair(a, b) #define F first #define S second using namespace std; using pii = pair<int, int>; int main(){ StarBurstStream int n, k; cin >> n >> k; vector<int> a(n + 1); for(int i = 1; i <= n; i++) cin >> a[i]; vector<int> lst(100001, -1); vector<int> t(n + 1); int now = 0; for(int i = 1; i <= n; i++){ if(lst[a[i]] != -1) now = max(now, lst[a[i]]); t[i] = now + 1; lst[a[i]] = i; } vector<vector<int>> dp(k + 1, vector<int>(n + 1)); for(int i = 1; i <= k; i++){ deque<pii> dq; for(int j = 1; j <= n; j++){ while(!dq.empty() && dq.front().S < t[j]) dq.pof; while(!dq.empty() && dq.back().F <= dp[i - 1][j - 1] - j) dq.pob; dq.eb(mp(dp[i - 1][j - 1] - j, j)); dp[i][j] = max(dp[i][j - 1], j + 1 + dq.front().F); } } cout << dp[k][n] << "\n"; return 0; } ``` :::