題目
題目概要
對於序列 ,求序列 ,滿足 且 ,最小化
數據範圍與約定
。
基礎設置
語言設置
最優解並非唯一。下文中,如果說某解爲最優解
,意思是某解是最優解之一
。
一個序列用 表示。或簡寫成 。未註明長度,則請聯繫上下文猜測。
記 爲 的中位數。
中位數
一個單調不降序列 的中位數是 。任意一個序列的中位數,就是將其單調不降排序後的序列的中位數。
或,若下標從零開始, 的中位數是 。
思路
基本情況
首先,將 同時減去 ,顯然答案不變,但此時 變成了不降,而非嚴格遞增。
結論零
- 如果序列 的中位數是 ,那麼去掉其中任意一個數,中位數變成 其中一個。
似乎無需證明?因爲太顯然了。但是這個結論很有用。
結論一
- 假若最優解是 ,那麼 可以取 的中位數。
這是顯然的。根據絕對值的幾何意義可知。據說是初一的內容?膜一下小學選手 。
當然了,我們也可以有一個推論:
- 如果 是中位數,在 或 時, 不劣於 。
大白話的同義轉述:靠近中位數就強!——但是僅限於 這樣全部相同的。
我們還可以多推一點:
- 最優解是 ,等價於 。
假設不是這樣的,就可以分別考慮這兩個子區間的最優解,至少存在一個 的解。這裏的 代表這兩個子序列。
同時,它也是充分的。如果總是如此,就沒有辦法將它拆分了。因爲在任意拆分下, 總有一個不超過原序列的中位數,而另一個就會不小於原序列的中位數。
給一張圖片。每一條橫線代表 取值相同的部分。顯然這個 會是覆蓋部分的 的中位數。
如果最優解是黑色部分所示那般,任選一個空隙切開,圖中是綠色的分割線,左右兩邊的中位數用紅色表示。左邊部分的中位數小於綠線左側的區間的中位數,而右邊部分是大於的(見圖中紅色的 )。紅線的大小關係已然清晰!
類似地,這也是充分的。如果紅線都已經是左邊較大,無論怎麼拆也拼不回去了。
我感覺這裏有一些抽象?不妨將整篇博客看完,回頭重新看此處。
結論二
- 設最優解是 。對於另外一個方案 ,在 時,一定不如 ,至少不會更好。
首先要注意到,這樣的 滿足 。畢竟是不降的。
數學歸納法證明。在 的時候,顯然如此,因爲二者是同一個方案。
在 時,假設 最後 個元素的最優解是 。顯然 ,否則全局最優解爲 。因而有 ,根據歸納法, 是一個更優的方案。
然而, ,所以 甚至更優(後 個數更靠近中位數)。
類似地,我們可以說:
- 在 時,不如 。
遺留問題——最後面(最前面) 個數是否存在一個全部相等的最優解?
當然如此。因爲 的中位數就是 去頭。根據 結論零
,中位數最多變小一個數
,並且這隻在原來的長度爲奇數時成立。不妨在數軸上畫出來,更形象。
中位數原本是黑色的數,去掉一個數,可能變成了紅色的那個數。大區間運用 結論一
的最後一個推論,右邊的區間的中位數是綠色的那個,要小於黑色的。反證法,假設 不滿足條件,那麼紅色的數就要小於綠色的數。
但是,仔細看圖——其實綠色也是 的中位數!因爲目前的數量是偶數,最中間的兩個數之間,都算作中位數。所以即使如此, 的中位數還是不小於 的中位數,滿足條件!
結論三
- 的最優解是 ,而 的最優解是 ,倘若 ,那麼整個區間的最優解 滿足 且 。
反證法。假設 ,就可以推出 ——這意味着 是被允許的,就會得到更優的解。
類似,可以讓右側取到更優的解。
結論四
- 我們可以合併答案了!
結論三
告訴我們 ,但 結論二
告訴我們,這樣的情況,不如 。
所以,其中一個最優解是 ,其中 。
注意到 ,所以 越大越好(接近中位數);類似地, 越小越好。所以,平衡點是 。於是乎,整個區間的最優解是一個全部相同的數字。根據 結論一
,這個數字是中位數!
寫出這個結論,就是:
- 每個數字是一個區間。若兩個相鄰區間的中位數是逆序,那麼將二者合併爲一個區間。對於每一個區間,取 作爲解, 是中位數。
代碼
目標
維護很多個區間,及每個區間的中位數。每次在末尾加入一個新區間,並可能將相鄰的區間進行合併。
顯然,我們有 次合併、 次查詢。需要一種數據結構吧?
普蘭
使用平衡樹,中位數就是第 大的數字。複雜度 ,慢在啓發式合併上。
特性
我們尋找一下有什麼特性。衆所周知,這是一個很難的問題:
- 合併兩個可重集。
- 輸出一個可重集的中位數。
反正我只會用平衡樹 ,我好弱……
但是在這道題裏,我們有這樣的特性:當前可重集的中位數大於你,與另一個集合合併後,中位數小於你了。
畫一張圖:
最重要的是綠色寫出的:原中位數和新中位數之間沒有別人。
假設它需要和前一個區間繼續合併,那麼前一個區間的中位數就在綠色部分中。
合併後的中位數,肯定也是在綠色部分中的。更具體些,一定在紅色之上、前一個區間的中位數之下。
所以,我們放心大膽地說,新區間中大於中位數的數,包含了兩個原區間中大於所屬區間的中位數的數。有點繞?記原來的序列是 和 ,那麼 。
爲啥?我不是已經給了圖片嗎?新區間的中位數在綠色部分,比它大的就是比紅色中位數大的。上一個區間?更顯然,中位數變小了。
問題來了,這兩個合併之後還得接着合併啊?仔細看:當前區間的數字恰好分居上一個區間的中位數兩側!所以,新區間的中位數恰好爲上一個區間的中位數!
然後就不會有更多的合併了。只需要執行兩次合併檢查即可!
普蘭
用一個大根堆,總是存儲較小的一半元素,這樣堆頂自然是根。我們需要支持的操作變成了:
- 區間的合併——堆的合併。共 次。
- 區間中位數的查詢——堆頂元素查詢。共 次。
- 區間中位數調整——刪去堆頂元素。共 次。
直接使用左偏樹即可,複雜度 ,可通過。但是我的代碼要開O2
代碼
兩段以 /**/
開頭的代碼,使用其中一個即可。第一個是穩妥的方式,第二個是證明我所說非假。
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
inline int readint() {
int a = 0; char c = getchar(), f = 1;
for(; c<'0' or c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c and c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
void writeint(int x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar((x%10)^48);
}
# define MB template < typename T >
MB void getMax(T &a,const T &b){ if(a < b) a = b; }
MB void getMin(T &a,const T &b){ if(b < a) a = b; }
int ABS(int x){ return x < 0 ? -x : x; }
# define FOR(i,n) for(int i=0; i<(n); ++i)
typedef long long int_;
template < class T, class CMP = less<T> >
class ZPS{ // 左偏樹(默認大根,類似優先隊列)
protected:
CMP comparer; unsigned siz;
struct Node{
T data; int dist; Node* son[2];
Node(T val):data(val),dist(1){
son[0] = son[1] = NULL;
}
} *root;
Node* merge(Node* a,Node* b){
if(a == NULL) return b;
if(b == NULL) return a;
if(comparer(a->data,b->data)) swap(a,b);
a->son[1] = merge(a->son[1],b);
int ld = 0; if(a->son[0] != NULL)
ld = a->son[0]->dist;
int rd = a->son[1]->dist;
if(ld < rd) swap(a->son[0],a->son[1]);
a->dist = min(ld,rd)+1; return a;
}
public:
ZPS(){ root = NULL, siz = 0; }
void push(T val){
root = merge(root,new Node(val));
++ siz; // 人工統計,大大的好!
}
T top() const { return root->data; }
unsigned size() const { return siz; }
void pop(){
if(root == NULL) return ;
-- siz; // manually count
Node* p = root->son[0];
p = merge(p,root->son[1]);
delete root; root = p;
}
void operator += (ZPS<T,CMP> &that){
siz += that.siz; // 小小的好!
root = merge(root,that.root);
}
};
ZPS<int> a[1000000];
int b[1000000];
vector<int> sta;
vector<int> way;
int main(){
int n = readint();
for(int i=0; i<n; ++i){
b[i] = readint()-i; // 改寫成 <=
a[i].push(b[i]); int now = i;
/**/while(not sta.empty()){
/**/ int last = sta.back();
/**/ if(a[last].top() <= a[now].top())
/**/ break; // valid
/**/ a[last] += a[now]; // union
/**/ if(a[last].size() > (i-last+2u)/2)
/**/ a[last].pop();
/**/ now = last, sta.pop_back();
/**/}
/**/for(int ZXY=0; ZXY<2; ++ZXY)
/**/ if(not sta.empty()){
/**/ int last = sta.back();
/**/ if(a[last].top() > a[now].top()){
/**/ sta.pop_back();
/**/ a[last] += a[now];
/**/ if(a[last].size() > (i-last+2u)/2)
/**/ a[last].pop();
/**/ now = last;
/**/ }
/**/ }
sta.push_back(now);
}
int_ ans = 0;
for(int i=n-1,zxy; ~i; --i){
if(sta.back() > i)
sta.pop_back();
zxy = a[sta.back()].top();
ans += ABS(zxy-b[i]);
way.push_back(zxy+i);
}
printf("%lld\n",ans);
while(not way.empty()){
writeint(way.back());
putchar(' ');
way.pop_back();
}
return 0;
}
後記
別的做法
傳送門 to 百度文庫,我看不懂。😦
以及,思路顯然、優化困難,動態規劃的方法。傳送門 to 洛谷題解,我也看不懂。😭
我の神明
在代碼中多多使用 zxy
,尤其是模數——多打 %zxy
,經實驗,有效增加 機率!