零、引言
昨天晚上,我看这篇论文,竟然看到头疼……
壹、保序回归
1.偏序关系
用
如果不懂请上度娘搜,而且不懂也可以直接理解成实数的比较。
2.问题描述
给出正整数 和代价函数
满足
。给出定义在
上的非严格偏序关系
求实数序列 满足
则
,最小化回归代价
贰、特殊情形下的算法
1.一种贪心算法
当偏序关系为简单的链即
首先,定义 。再定义
为使得
最小的
,即
1.1.理论推导
引理1一: 的导数(忽略
处的奇点)单增,即
引理二:,即
为导数
的零点。在
时零点只有一个;在
时零点有多个,不妨令
定理一:若 ,则
证明:考察导数即可。由 引理一
和 引理二
知 处
导数为负,故
,同理可证
证明了 定理一
,其实问题已经与
推论一:对于集族 ,有
定理二:若最优解的 的某个极长等值段为
,则
证明:否则可以给 到
统一增加充分小的偏移量
使其逼近
。由
引理一
知,这是更优的。
定理三:原问题最优解为 ,当且仅当
。——注意
证明:必要性毋庸置疑,否则前后可以各自取到最优点 。下证充分性。若实际上存在
,令
为满足条件的最小
。那么
一定由若干等值段构成,且每段的值都是
,根据
推论一
可知 。根据限制条件,
就是第一个等值段,即
,又由
的定义
,故其不符合题目要求,矛盾。
定理四:若 和
都满足
定理三
的判定条件,且 ,则
满足
定理三
判定条件。
证明:分割点 时,右侧加上了较小的
,仍成立;分割点
时,左侧加上了较大的
,仍成立。
1.2.算法流程
维护单调栈,存储每一个目前得到的 等值段,以及这些等值段的
新加入一个数时,首先将其自己视为单独的 等值段,放入栈中。重复检查栈顶两个元素:若序列中靠前的那个的
大于靠后者的
,根据
定理四
必然会合并成大等值段,合并即可。
时间复杂度是 次计算
。当
时,用
做到均摊
,当
为偶数时使用二分等方式求导数零点,是
或
2.维护折线算法
在 上被称为
,比较常见,即维护类似绝对值函数的图像。
只适用于树形(递归子问题后合并)的
叁、一般问题的算法
1.整体二分
这又是个让人摸不着头脑的巧妙的 。为了方便叙述,定义
向
取整的结果是:若
则结果为
,否则结果为
我们构造一个新问题,叫做 型问题,表示额外添加限制
定理五:若 ,且存在最优解
使得
,则
型问题的最优解是将
向
取整后的序列
证明:类似 定理二
,极大等值连通块的 应当取到
。那么
型问题当然要保持靠近
的性质,即向
取整呗。显然方案仍然是合法的。2
所以,我们求解 型问题后,看看
还是
就知道了原问题中
还是
,实现了二分的目标。
当 时,由
定理二
,,即存在最优解
。设
已排序,求解
当 时,解可能在实数范围内,故只能在实数域上二分,并且采用
2.网络流模型
由于 向
取整后,
,所以决策只有两种,可以用最大权闭合子图模型解决,偏序关系都可以变成
容量的边。当
时,两种决策的价值差太小,精度误差过大。需要另辟蹊径:价值只在物品上,将一个物品选
和选
的价值都除以
,选择方案不变。而建边时,权值是
,除以
在 的奇点上,由于
从正方向逼近
,导数也是有定义的。
3.整数解
类似 定理五
,若规定 ,则考虑
型问题,即可知原最优解
证明过程与 定理五
无异:靠近
肆、代码实现
一般来说,可以直接将 直接建成边
当我们需要对点集 进行整体二分时,任取
,则
在有向图上的任意路径上的所有点都在
有向图中的边,直接变为容量 的边,表示
则
。最小割中的
为选择
的,所以
的容量是
。——若该值为负,则改为
例题:限制 ,核心代码如下。
# define _go(i,x) for(int i=head[x]; ~i; i=e[i].nxt)
const int MAXN = 1003, MAXM = 200005;
const int INF = 0x3fffffff;
namespace Network{
struct Edge{ int to, nxt, val; };
Edge e[MAXM]; int head[MAXN], cntEdge;
void _addEdge(int a, int b, int c){
e[cntEdge].to = b, e[cntEdge].nxt = head[a];
e[cntEdge].val = c, head[a] = cntEdge ++;
}
void addEdge(int a, int b, int c = 0){
_addEdge(a,b,c), _addEdge(b,a,0);
}
void clear(const int &n){
cntEdge = 0, memset(head+1,-1,n<<2);
}
int dis[MAXN], cur[MAXN];
# define _PARAM_ const int &source,\
const int &sink, const int &n
bool bfs(_PARAM_){
static int que[MAXN];
memset(dis+1,-1,n<<2); // clear
dis[source] = 0, que[1] = source;
for(int x,*fro=que,*bac=que+1; fro!=bac; )
_go(i,x=*(++fro)) if(e[i].val)
if(!(~dis[e[i].to])){
dis[e[i].to] = dis[x]+1;
++ bac, *bac = e[i].to;
}
return dis[sink] != -1;
}
int dfs(int x, int inFlow, const int &sink){
int sum = 0; if(x == sink) return inFlow;
for(int &i=cur[x]; ~i; i=e[i].nxt)
if(e[i].val && dis[e[i].to] == dis[x]+1){
int d = dfs(e[i].to,std::min(
inFlow-sum,e[i].val),sink);
e[i].val -= d, e[i^1].val += d;
if((sum += d) == inFlow) return sum;
}
dis[x] = -1; return sum;
}
int dinic(_PARAM_){
int res = 0;
while(bfs(source,sink,n)){
memcpy(cur+1,head+1,n<<2);
res += dfs(source,INF,sink);
}
return res;
}
bool is_inS(const int &x){ return ~dis[x]; }
}
struct Edge{ int to, nxt; };
Edge e[MAXM]; int head[MAXN], cntEdge;
void addEdge(int a, int b){
e[cntEdge].to = b, e[cntEdge].nxt = head[a];
head[a] = cntEdge ++;
}
int v[MAXN], ans[MAXN], haxi[MAXN];
void solve(int *l, int *r, int ql, int qr){
if(l == r || ql == qr){
for(int *i=l; i!=r; ++i) ans[*i] = ql;
return ; // answer found
}
const int mid = (ql+qr)>>1;
for(int *i=l,p=1; i!=r; ++i,++p) haxi[*i] = p;
const int source = int(r-l)+1, sink = source+1;
Network::clear(sink); // recalc
for(int *i=l,p=1; i!=r; ++i,++p){
int w = (mid-v[*i])<<1|1; // if choose (mid+1)
if(w > 0) Network::addEdge(source,p,w);
else if(w) Network::addEdge(p,sink,-w);
_go(j,*i) if(haxi[e[j].to]) // in the range
Network::addEdge(p,haxi[e[j].to],INF);
}
Network::dinic(source,sink,sink);
for(int *i=l; i!=r; ++i) haxi[*i] = 0;
static int _tmp[MAXN]; // auxiliary
std::copy(l,r,_tmp); // backup
int *pl = l, *pr = r; ///< refill
for(int *i=_tmp,p=1; pl!=pr; ++i,++p)
if(Network::is_inS(p)) *pl = *i, ++ pl;
else -- pr, *pr = *i; // choose mid+1
solve(l,pl,ql,mid), solve(pr,r,mid+1,qr);
}
- “引理” 的意思就是,过于显然而略去证明,
只引述一下就差不多得了。 ↩︎ - 论文中,这里用了不少的篇幅,我便猜我的证明多有不严谨之处,希望读者能够简略指出。
虽说修锅的可能性不大,顶多让它变为 “参见原论文”。↩︎