byd ,这么好用的东西怎么我上了大学才会啊 QAQAQAQAQAQAQAQ 。

例题

题目链接:https://codeforces.com/problemset/problem/713/C

题目大意:给一个数字 $±1$ 的代价是 $1$ ,问给一个序列花最小多少代价可以变成严格递增序列。

$O(n^2)$ 的做法在此就不再赘述,主要讲解 slope trick 做法。

做法

注意到一个事情,我们设 $f_{i}$ 表示在处理完当前前缀后,最后一个数值是 $i$ 的最小代价。

那么每次添加一个数值 $a$ 就有两部分 $dp$ 过程:

$f_{i}=f_{i}+|a-i|$

$f_{i}=\min\limits_{j\le i}f_{j}$

可以观察到,$f$ 是长成下面那样的凸函数:

而 slope trick 就是维护斜率 $+1$ 的位置,也就是维护折点,但是有的折点如果要 $+k(k>1)$ 咋整?那就维护 $k$ 个折点。(一般来说还要额外的维护一些值来保证函数能够唯一确定,比如本题就要维护当斜率为 $0$ 的值,用来确定函数的高度,同时也是最后的答案)

可以发现,上面两个过程分别对应:

  1. 在 $a$ 处添加两个折点。
  2. 弹出最靠右的折点,并且维护函数的高度。

然后就做完了,这个过程可以用堆维护,从而得到时间复杂度为 $O(n\log{n})$ 的做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e3 + 5;
int n;
LL a[N];
priority_queue<LL> p;
int main(){
cin >> n;
LL ans = 0ll;
for(int i = 1; i <= n; i++){
cin >> a[i];
a[i] -= i;
p.push(a[i]);
p.push(a[i]);
ans += abs(p.top() - a[i]);
p.pop();
}
cout << ans << "\n";
return 0;
}

slope trick

具体来说,当序列 $dp$ 满足:

  1. 连续。
  2. 分段线性。
  3. 凸函数。

就可以考虑 slope trick 。

而且这样的函数还有很好的性质,若 $f,g$ 满足,则 $f+g$ 也满足,且新的折点可重集合是 $f,g$ 可重集合合并起来。(是合并,不是并)

练习

1. A New Beginning

题目链接:https://codeforces.com/problemset/problem/1534/G

题目大意:在平面上,你初始在原点,每次只能往右往上走,然后有 $n$ 个点,你在 $(x,y)$ 与访问 $(x_i,y_i)$ 的代价是 $\max(|x_i-x|,|y_i-y|)$ ,然后问你访问所有点的最小代价。(访问并不会导致实际的移动)

做法

可以发现,$(x_i,y_i)$ 在 $(x,y)$ $x+y=x_i+y_i$ 的时候访问一定是代价最小的,直接从斜线角度来看这道题目。

也可以换个角度,这个是切比雪夫距离,切比雪夫距离和曼哈顿距离可以互相转化,转化成曼哈顿距离以后就显然了,在 $x$ 坐标相等的时候访问就是代价最小的。

观察一下 $dp$ 式子,可以发现,我们要对这个函数维护两个过程。

  1. 插入一个 V 字。
  2. 将斜率为 $0$ 的平台延长。

用数据结构维护就是维护四个东西:

  1. 斜率为 $0$ 的平台左/右边的折点。(可以用堆维护)
  2. 平台高度。
  3. 平台宽度。

时间复杂度:$O(n\log{n})$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 8e5 + 5;
struct node{
LL pos, val;
}a[N];
int n;
priority_queue<LL, vector<LL>, less<LL> > l;
priority_queue<LL, vector<LL>, greater<LL> > r;
LL lazy, ans = 0ll;
LL gettop(int type){
LL val;
if(!type) val = l.top();
else val = r.top() + lazy;
return val;
}
void poptop(int type){
if(!type) l.pop();
else r.pop();
}
void pushval(LL val, int type){
if(!type) l.push(val);
else r.push(val - lazy);
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
LL x, y;
cin >> x >> y;
a[i].pos = x + y;
a[i].val = x;
}
sort(a + 1, a + n + 1, [](node x, node y){return x.pos < y.pos;});
l.push(a[1].val);
r.push(a[1].val);
for(int i = 2; i <= n; i++){
lazy += a[i].pos - a[i - 1].pos;
if(a[i].val <= gettop(0)){
ans += abs(a[i].val - gettop(0));
pushval(gettop(0), 1);
poptop(0);
pushval(a[i].val, 0);
pushval(a[i].val, 0);
}
else if(a[i].val >= gettop(1)){
ans += abs(a[i].val - gettop(1));
pushval(gettop(1), 0);
poptop(1);
pushval(a[i].val, 1);
pushval(a[i].val, 1);
}
else{
pushval(a[i].val, 0);
pushval(a[i].val, 1);
}
}
cout << ans << "\n";
return 0;
}

参考文献

https://zhuanlan.zhihu.com/p/389740015