# [solution] 2018-2019 ACM-ICPC Pacific Northwest Regional Contest (Div. 1) [TOC] 題目網址:https://codeforces.com/gym/101982 ## pI - [Inversions](https://codeforces.com/gym/101982/problem/I) > 題目中給定的 $k$ 以下都以 $p$ 表示。 :::success 性質 1 填的數字為非嚴格遞減。 **證明** 可以透過貪心交換證明,暫時略。 ::: 接著,定義逆序數對的貢獻分成三類: 1. 已填好的數字與已填好的數字 2. 已填好的數字與未知的數字 3. 未知的數字和未知的數字 :::success 性質 2 第 1 類的數字貢獻是固定的。 ::: 因此我們可以直接算出第 1 類的貢獻,接下來只要考慮第 2、3 類的貢獻。 :::success 性質 3 我們可以快速得知一段區間 $[l, r]$ 中,未知的數字都設成 $x$ 時,第 2 類的貢獻是多少。 **證明** 我們有辦法快速知道 每個未知的數字放 $x$ 的貢獻,因此也可以透過前綴和知道一個區間未知的數字都設成 $x$ 是多少。 ::: :::success 性質 4 我們可以快速得知一段區間 $[l, r]$ 中,有幾個 $0$。 ::: :::success 性質 5 我們可以快速得知一段區間 $[l, r]$ 中,未知的數字都設成 $x$ 時,第 3 類的貢獻是多少。 我們只會考慮 $[l, r]$ 區間中未知數字向前的貢獻(因為向後的會被重複算到,因此不考慮)。 如果 $x = p$,則貢獻為 $0$。 否則根據性質 1,$[1, l-1]$ 中的未知數字 都比 $x$ 大。因此貢獻就是 $[1, l-1]$ 中 $0$ 的數量乘上 $[l, r]$ 中 $0$ 的數量。 ::: 根據上面的性質們,我們先定義這些陣列: > 只要是有 $pre$ 前綴的,都是指前綴和、前綴 max 等等。 - $dp[i][j]$: 前 $i$ 個 column 中最後一個未知數字放 $j$ (==並且,後面不會有未知數放 $j$==)時,最大的逆序數對數量(第 2, 3 類) - $pre\_inv[i][j]$: 前 $i$ 個 column 如果未知數字都放 $j$,則這些數字對已經寫好的數字的貢獻(第 2 類) - $pre\_zero[i]$: 前 $i$ 個 column 所有未知數字的數量 為了方便書寫,我們也定義這些函式 - $get\_range\_inv(l, r, j)$: 在 $[l, r]$ 的區間中的未知數字都放 $j$ 的貢獻(第 2 類) - 定義:$pre\_inv[r][j] - pre\_inv[l-1][j]$ - $get\_range\_zero(l, r)$: 在 $[l, r]$ 的區間中的未知數字的數量 - 定義:$pre\_zero[r][j] - pre\_zero[l-1]$ :::success 性質 6 - DP 的基底 $dp$ 的基底:$dp[i][p] = pre\_inv[i][p]$ 因為 $p$ 以上無法填其他數字,在 $j=p$ 的情況下,未知數字的填法是固定的,因此可以直接從 $pre\_inv[i][p]$ 得知。 ::: 最後,來到最關鍵的轉移,這裡會依序考慮複雜度比較差的情況,接著優化轉移,最後優化空間複雜度。 轉移方式可以這樣思考: 1. 因為有性質 1,因此我們從大到小考慮每個要填的數值,接著從前到後。假設現在要處理 $dp[i][j]$ 2. 我們每到一個新的位置 $i$(圖中的紅色區塊),會考慮一段後綴 $[l, r]$ 都填 $j$,並取後綴開頭的前一行取最大值做轉移 3. 取一段的人都當 $x$ 的貢獻我們分兩類考慮: 1. 第 2 類(已知對未知):貢獻就是 $get\_range\_inv(l, r, j)$ 2. 第 3 類(未知對位置):貢獻就是 $get\_range\_zero(1, l-1) \times get\_range\_zero(l, r)$(by 性質 5) ![image](https://hackmd.io/_uploads/rkWyi4mnJl.png) 這個藍色的區塊的最大值,我們也用一個陣列紀錄: - $pre\_max\_dp[i][j]$: $\max\limits_{k=j}^p dp[k]$。 把上面的轉移寫成式子,可知: $$ \begin{aligned} dp[i][j] &= \max_{k=0}^{i-1} (pre\_max\_dp[k][j+1] + get\_range\_inv(k+1, i, j)+ get\_range\_zero(1, k) \times get\_range\_zero(k+1, i))\\ \end{aligned} $$ 這樣,我們就得到了一個 $O(nk \times n)$ 的演算法了。 --- 下一步我們會將轉移式簡化,並觀察有沒有我們可以優化的地方。 $$ \begin{aligned} dp[i][j] &= \max_{k=0}^{i-1} (pre\_max\_dp[k][j+1] + get\_range\_inv(k+1, i, j)+ get\_range\_zero(1, k) \times get\_range\_zero(k+1, i))\\ &= \max_{k=0}^{i-1} (pre\_max\_dp[k][j+1] + (pre\_inv[i][j] - pre\_inv[k][j]) + (pre\_zero[k]-pre\_zero[0]) \times (pre\_zero[i]-pre\_zero[k]))\\ &= \max_{k=0}^{i-1} (pre\_max\_dp[k][j+1] + pre\_inv[i][j] - pre\_inv[k][j] + pre\_zero[k] \times pre\_zero[i] - pre\_zero[k]^2\\ &= pre\_inv[i][j] + \max_{k=0}^{i-1} (pre\_zero[k] \times pre\_zero[i]) + (pre\_max\_dp[k][j+1] - pre\_inv[k][j] - pre\_zero[k]^2)\\ \end{aligned} $$ 可以看到,透過一系列簡化,我們可以清楚的看到這是斜率優化的式子。 其中 $pre\_zero[k]$ 是斜率,$(pre\_max\_dp[k][j+1] - pre\_inv[k][j] - pre\_zero[k]^2)$ 是截距。 :::success 性質 7 1. 斜率($pre\_zero[k]$)單調 2. 詢問($i$)單調 ::: 有了性質 7,我們就可以進一步使用單調隊列維護轉移點,讓轉移的複雜度從 $O(n)$ 變成 $O(1)$。 這樣,我們就得到了一個 $O(nk)$ 的演算法了。 --- 最後,因為記憶體大小限制,上面許多的陣列都要使用狀態壓縮,筆者僅有 $pre\_inv$ 是使用二維陣列。 ### Full Code :::spoiler ```cpp #include <bits/stdc++.h> #define int long long using namespace std; const int MAX_N = 2e5+10; const int MAX_K = 100+10; const int INF = 2e18; int n, p; vector<int> v(MAX_N); vector<vector<int>> pre_inv(MAX_N, vector<int>(MAX_K)); vector<int> pre_zero(MAX_N); vector<int> pre_max_dp(MAX_N); void solve1(){ // input cin >> n >> p; for (int i=1 ; i<=n ; i++){ cin >> v[i]; } // pre-process int ans1 = 0; // 已經填好的數字之間的逆數數對貢獻 { vector<int> _greater(MAX_K); // _greater[i][j] = v[1]~v[i] 中大於 j 的人有幾個 vector<int> _less(MAX_K); // _less[i][j] = v[i]~v[n] 中小於 j 的人有幾個 for (int i=1 ; i<=n ; i++){ for (int j=1 ; j<=p ; j++){ _greater[j] += v[i]!=0 ? j<v[i] : 0; pre_inv[i][j] += (v[i]==0 ? _greater[j] : 0); } if (v[i]!=0) ans1 += _greater[v[i]]; } for (int i=n ; i>=1 ; i--){ for (int j=1 ; j<=p ; j++){ _less[j] += v[i]!=0 ? j>v[i] : 0; pre_inv[i][j] += (v[i]==0 ? _less[j] : 0); } } for (int i=1 ; i<=n ; i++){ pre_zero[i] = pre_zero[i-1]+(v[i]==0); for (int j=1 ; j<=p ; j++){ pre_inv[i][j] += pre_inv[i-1][j]; } } } // process // ax+b = 要插入的直線,cx+d = 最後一條直線,ex+f 倒數第二條直線 auto check = [](int a, int b, int c, int d, int e, int f){ return (int)(b-d)*(a-e) >= (int)(b-f)*(a-c); }; { vector<int> dp(MAX_N); for (int i=1 ; i<=n ; i++){ if (v[i]!=0){ dp[i] = dp[i-1]; pre_max_dp[i] = max(pre_max_dp[i], dp[i]); continue; }else{ dp[i] = pre_inv[i][p]; pre_max_dp[i] = max(pre_max_dp[i], dp[i]); } } } for (int j=p-1 ; j>=1 ; j--){ vector<int> dp(MAX_N); deque<pair<int, int>> dq; // 維護線段 (a, b) 代表 ax+b 的線段,其中 a 為嚴格遞增,b 為嚴格遞減 dq.push_back({0, 0}); for (int i=1 ; i<=n ; i++){ if (v[i]!=0){ dp[i] = dp[i-1]; pre_max_dp[i] = max(pre_max_dp[i], dp[i]); continue; } int x = pre_zero[i]; // 刪掉過期的線段 while (dq.size()>=2 && dq[0].first*x+dq[0].second <= dq[1].first*x+dq[1].second){ dq.pop_front(); } int ma = dq[0].first * x + dq[0].second; dp[i] = pre_inv[i][j] + ma; // 插入線段 int new_a = pre_zero[i]; int new_b = pre_max_dp[i] - pre_inv[i][j] - pre_zero[i]*pre_zero[i]; while (dq.size()>=2 && check(new_a, new_b, dq.back().first, dq.back().second, dq[dq.size()-2].first, dq[dq.size()-2].second)){ dq.pop_back(); } dq.push_back({new_a, new_b}); // 更新最大值 pre_max_dp[i] = max(pre_max_dp[i], dp[i]); } } int ans2 = 0; ans2 = *max_element(pre_max_dp.begin(), pre_max_dp.end()); // output cout << ans1+ans2 << "\n"; return; } signed main(){ cin.tie(0), ios::sync_with_stdio(0); int t = 1; while (t--){ solve1(); } return 0; } ``` ::: {%hackmd @temmie950807/r1KCzgjFh %} {%hackmd @temmie950807/Prove_Style %}