題目網址:https://codeforces.com/gym/101982
題目中給定的
以下都以 表示。
性質 1
填的數字為非嚴格遞減。
證明
可以透過貪心交換證明,暫時略。
接著,定義逆序數對的貢獻分成三類:
性質 2
第 1 類的數字貢獻是固定的。
因此我們可以直接算出第 1 類的貢獻,接下來只要考慮第 2、3 類的貢獻。
性質 3
我們可以快速得知一段區間
證明
我們有辦法快速知道
每個未知的數字放
性質 4
我們可以快速得知一段區間
性質 5
我們可以快速得知一段區間
我們只會考慮
如果
否則根據性質 1,
根據上面的性質們,我們先定義這些陣列:
只要是有
前綴的,都是指前綴和、前綴 max 等等。
為了方便書寫,我們也定義這些函式
性質 6 - DP 的基底
因為
最後,來到最關鍵的轉移,這裡會依序考慮複雜度比較差的情況,接著優化轉移,最後優化空間複雜度。
轉移方式可以這樣思考:
這個藍色的區塊的最大值,我們也用一個陣列紀錄:
把上面的轉移寫成式子,可知:
這樣,我們就得到了一個
下一步我們會將轉移式簡化,並觀察有沒有我們可以優化的地方。
可以看到,透過一系列簡化,我們可以清楚的看到這是斜率優化的式子。
其中
性質 7
有了性質 7,我們就可以進一步使用單調隊列維護轉移點,讓轉移的複雜度從
這樣,我們就得到了一個
最後,因為記憶體大小限制,上面許多的陣列都要使用狀態壓縮,筆者僅有
#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;
}