题目
题目概要
对于序列 ,求序列 ,满足 且 ,最小化
数据范围与约定
。
基础设置
语言设置
最优解并非唯一。下文中,如果说某解为最优解
,意思是某解是最优解之一
。
一个序列用 表示。或简写成 。未注明长度,则请联系上下文猜测。
记 为 的中位数。
中位数
一个单调不降序列 的中位数是 。任意一个序列的中位数,就是将其单调不降排序后的序列的中位数。
或,若下标从零开始, 的中位数是 。
思路
基本情况
首先,将 同时减去 ,显然答案不变,但此时 变成了不降,而非严格递增。
结论零
- 如果序列 的中位数是 ,那么去掉其中任意一个数,中位数变成 其中一个。
似乎无需证明?因为太显然了。但是这个结论很有用。
结论一
- 假若最优解是 ,那么 可以取 的中位数。
这是显然的。根据绝对值的几何意义可知。据说是初一的内容?膜一下小学选手 。
当然了,我们也可以有一个推论:
- 如果 是中位数,在 或 时, 不劣于 。
大白话的同义转述:靠近中位数就强!——但是仅限于 这样全部相同的。
我们还可以多推一点:
- 最优解是 ,等价于 。
假设不是这样的,就可以分别考虑这两个子区间的最优解,至少存在一个 的解。这里的 代表这两个子序列。
同时,它也是充分的。如果总是如此,就没有办法将它拆分了。因为在任意拆分下, 总有一个不超过原序列的中位数,而另一个就会不小于原序列的中位数。
给一张图片。每一条横线代表 取值相同的部分。显然这个 会是覆盖部分的 的中位数。
如果最优解是黑色部分所示那般,任选一个空隙切开,图中是绿色的分割线,左右两边的中位数用红色表示。左边部分的中位数小于绿线左侧的区间的中位数,而右边部分是大于的(见图中红色的 )。红线的大小关系已然清晰!
类似地,这也是充分的。如果红线都已经是左边较大,无论怎么拆也拼不回去了。
我感觉这里有一些抽象?不妨将整篇博客看完,回头重新看此处。
结论二
- 设最优解是 。对于另外一个方案 ,在 时,一定不如 ,至少不会更好。
首先要注意到,这样的 满足 。毕竟是不降的。
数学归纳法证明。在 的时候,显然如此,因为二者是同一个方案。
在 时,假设 最后 个元素的最优解是 。显然 ,否则全局最优解为 。因而有 ,根据归纳法, 是一个更优的方案。
然而, ,所以 甚至更优(后 个数更靠近中位数)。
类似地,我们可以说:
- 在 时,不如 。
遗留问题——最后面(最前面) 个数是否存在一个全部相等的最优解?
当然如此。因为 的中位数就是 去头。根据 结论零
,中位数最多变小一个数
,并且这只在原来的长度为奇数时成立。不妨在数轴上画出来,更形象。
中位数原本是黑色的数,去掉一个数,可能变成了红色的那个数。大区间运用 结论一
的最后一个推论,右边的区间的中位数是绿色的那个,要小于黑色的。反证法,假设 不满足条件,那么红色的数就要小于绿色的数。
但是,仔细看图——其实绿色也是 的中位数!因为目前的数量是偶数,最中间的两个数之间,都算作中位数。所以即使如此, 的中位数还是不小于 的中位数,满足条件!
结论三
- 的最优解是 ,而 的最优解是 ,倘若 ,那么整个区间的最优解 满足 且 。
反证法。假设 ,就可以推出 ——这意味着 是被允许的,就会得到更优的解。
类似,可以让右侧取到更优的解。
结论四
- 我们可以合并答案了!
结论三
告诉我们 ,但 结论二
告诉我们,这样的情况,不如 。
所以,其中一个最优解是 ,其中 。
注意到 ,所以 越大越好(接近中位数);类似地, 越小越好。所以,平衡点是 。于是乎,整个区间的最优解是一个全部相同的数字。根据 结论一
,这个数字是中位数!
写出这个结论,就是:
- 每个数字是一个区间。若两个相邻区间的中位数是逆序,那么将二者合并为一个区间。对于每一个区间,取 作为解, 是中位数。
代码
目标
维护很多个区间,及每个区间的中位数。每次在末尾加入一个新区间,并可能将相邻的区间进行合并。
显然,我们有 次合并、 次查询。需要一种数据结构吧?
普兰
使用平衡树,中位数就是第 大的数字。复杂度 ,慢在启发式合并上。
特性
我们寻找一下有什么特性。众所周知,这是一个很难的问题:
- 合并两个可重集。
- 输出一个可重集的中位数。
反正我只会用平衡树 ,我好弱……
但是在这道题里,我们有这样的特性:当前可重集的中位数大于你,与另一个集合合并后,中位数小于你了。
画一张图:
最重要的是绿色写出的:原中位数和新中位数之间没有别人。
假设它需要和前一个区间继续合并,那么前一个区间的中位数就在绿色部分中。
合并后的中位数,肯定也是在绿色部分中的。更具体些,一定在红色之上、前一个区间的中位数之下。
所以,我们放心大胆地说,新区间中大于中位数的数,包含了两个原区间中大于所属区间的中位数的数。有点绕?记原来的序列是 和 ,那么 。
为啥?我不是已经给了图片吗?新区间的中位数在绿色部分,比它大的就是比红色中位数大的。上一个区间?更显然,中位数变小了。
问题来了,这两个合并之后还得接着合并啊?仔细看:当前区间的数字恰好分居上一个区间的中位数两侧!所以,新区间的中位数恰好为上一个区间的中位数!
然后就不会有更多的合并了。只需要执行两次合并检查即可!
普兰
用一个大根堆,总是存储较小的一半元素,这样堆顶自然是根。我们需要支持的操作变成了:
- 区间的合并——堆的合并。共 次。
- 区间中位数的查询——堆顶元素查询。共 次。
- 区间中位数调整——删去堆顶元素。共 次。
直接使用左偏树即可,复杂度 ,可通过。但是我的代码要开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
,经实验,有效增加 机率!