算法能解决的问题
它主要解决的是可并堆(Mergeable Heap)的问题, 如果问题是要取最值并且还要合并堆, 适合用左偏树计算
数据结构的操作
- 插入一个数, O ( log n ) O(\log n) O(logn)
- 求最小值, O ( 1 ) O(1) O(1)
- 删除最小值, O ( 1 ) O(1) O(1)
- *合并两个左偏树, O ( log n ) O(\log n) O(logn)
左偏树记录的信息
以小根堆为例
- 当前节点的权值 v v v
- d i s dis dis表示距离最近空节点的距离, 并且对于每棵子树左儿子的距离大于等于右儿子的距离
形式化表示为 l s . d i s ≥ r s . d i s ls.dis \ge rs.dis ls.dis≥rs.dis
n n n个节点的左偏树最坏情况下高度是 log n \log n logn
核心函数
merge(a, b)合并 a , b a, b a,b两棵子树并且返回合并后的根节点

假设合并 a , b a, b a,b两棵子树, 并且 a . v ≤ b . v a.v \le b.v a.v≤b.v, 左边子树的权值 ≥ a \ge a ≥a, 右边子树的权值 ≥ b \ge b ≥b

尝试将 b b b和 a a a的右子树进行合并, 是递归的过程
如果发现右边子树的 d i s dis dis大于了左边的 d i s dis dis, 交换两棵子树
(1) 插入一个数
- 建立只有一个点的左偏树
- 将这个点与原树合并
(2) 删除根节点
- 首先先删除掉根节点
- 合并两棵左右子树
(3) 获得最小值
直接返回左偏树根节点的值
示例代码

并查集维护哪些点在一个集合中, 用左偏树的根节点作为并查集的代表元素
比较复杂的操作是从集合中删除一个点, 对于左偏树来说假设根节点的左右儿子是 a , b a, b a,b, 删除根节点后, 合并 a , b a, b a,b两棵树

但是, 并查集没办法删除点, 需要做如下操作
不妨设新的左偏树根节点是 a a a, 在并查集中找到对应的位置 p p p, 将并查集的代表元素与 p p p交换

并查集的换根操作, 这样就可以维护这样的集合
核心代码
始终将节点值较大的合并到节点值较小的, 也就是交换后的 x x x是被合并的新的根, 然后将 r s [ x ] rs[x] rs[x]也就是 x x x的右子树和 y y y树进行合并
int merge(int x, int y) {
if (!x || !y) return x + y;
if (cmp(y, x)) swap(x, y);
rs[x] = merge(rs[x], y);
if (dis[rs[x]] > dis[ls[x]]) swap(ls[x], rs[x]);
dis[x] = dis[rs[x]] + 1;
return x;
}
示例代码
对于左偏树来说假设左儿子 l s ls ls或者右儿子 r s rs rs为空, 那就是节点 0 0 0, 但是数据范围是 [ 1 , 1 0 9 ] [1, 10 ^ 9] [1,109], 0 0 0小于任意一个数, 0 0 0会作为根节点, 但是 0 0 0不能作为根节点因为数值 0 0 0不存在
因此需要初始化 v [ 0 ] = + ∞ v[0] = +\infty v[0]=+∞
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n;
int v[N], dis[N], ls[N], rs[N];
int p[N], idx;
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
bool cmp(int a, int b) {
if (v[a] != v[b]) return v[a] < v[b];
return a < b;
}
int merge(int x, int y) {
if (!x || !y) return x + y;
if (cmp(y, x)) swap(x, y);
rs[x] = merge(rs[x], y);
if (dis[rs[x]] > dis[ls[x]]) swap(ls[x], rs[x]);
dis[x] = dis[rs[x]] + 1;
return x;
}
void insert(int x) {
v[++idx] = x;
p[idx] = idx;
dis[idx] = 1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
// 初始化不存在节点
v[0] = 2e9;
cin >> n;
while (n--) {
int op;
cin >> op;
if (op == 1) {
int x;
cin >> x;
insert(x);
}
else if (op == 2) {
int x, y;
cin >> x >> y;
x = find(x), y = find(y);
if (x != y) {
if (cmp(y, x)) swap(x, y);
p[y] = x;
merge(x, y);
}
}
else if (op == 3) {
int x;
cin >> x;
cout << v[find(x)] << '\n';
}
else {
int x;
cin >> x;
x = find(x);
if (cmp(rs[x], ls[x])) swap(ls[x], rs[x]);
// 并查集换根
p[x] = ls[x], p[ls[x]] = ls[x];
merge(ls[x], rs[x]);
}
}
return 0;
}
*数字序列

将问题转化为非下降子序列
原数列 a i a_i ai, 希望求的数列是 b i b_i bi
现在做如下变化, 将 a i ′ = a i − i a_i' = a_i - i ai′=ai−i, 使得 b i ′ = b i − i b_i ' = b_i - i bi′=bi−i
因为 b i − b i − 1 ≥ 1 b_i - b_{i - 1} \ge 1 bi−bi−1≥1, 我们希望的是 b i ′ − b i − 1 ′ ≥ 0 b_i' - b_{i - 1}' \ge 0 bi′−bi−1′≥0, 因此
b i − b i − 1 ′ − 1 ≥ 0 b_i - b_{i - 1}' - 1 \ge 0 bi−bi−1′−1≥0
也就是
( b i − i ) − ( b i − 1 − ( i − 1 ) ) ≥ 0 (b_i - i) - (b_{i - 1} - (i - 1)) \ge 0 (bi−i)−(bi−1−(i−1))≥0
这样变换之后就有 b 1 ′ ≤ b 2 ′ ≤ . . . ≤ b n ′ b_1' \le b_2' \le ... \le b_n' b1′≤b2′≤...≤bn′
转换之前结果是
∑ ∣ a i − b i ∣ \sum \left | a_i - b_i \right | ∑∣ai−bi∣
转化之后的结果也是这个, 因此问题可以合法的转化过来
从递增序列转化为非下降序列
现在令 a i = a i ′ , b i = b i ′ a_i = a_i',b_i = b_i' ai=ai′,bi=bi′
将 a i a_i ai序列分为两段, 假设最优解的 b i b_i bi取相同值, 类似于货仓选择问题(数轴上有一些位置, 选择一个位置使得到其他位置的距离之和最短, 位置就是中位数位置)
假设前一段 b 1 = b 2 = . . . = b n = u b_1 = b_2 = ... = b_n = u b1=b2=...=bn=u, 后一段最优解 b n + 1 = b n + 1 = . . . = b m = v b_{n + 1} = b_{n + 1} = ... = b_m = v bn+1=bn+1=...=bm=v
因为要求所有 b i b_i bi相等, u u u就是第一段中位数, 同理 v v v是第二段中位数
( 1 ) (1) (1) u ≤ v u \le v u≤v

那么前半段取 u u u, 后半段取 v v v就是最优解, 假设前一段最优解是 A A A, 后一段最优解是 B B B, 那么最优解的下界是 A + B A + B A+B, 因为 u , v u, v u,v保持非下降, 可以取到下界, 因此 u , v u, v u,v就是整体最优解
( 2 ) (2) (2) u > v u > v u>v

这种情况最优解是两段的中位数(红色线)

猜想(1): 最优解的情况 b n ≤ u b_n \le u bn≤u并且 b n + 1 ≥ v b_{n + 1} \ge v bn+1≥v

证明猜想(1): 超过 u u u的部分可以变为 u u u, 并且结果不会变差, v v v部分同理

因为前后两段是对称的, 先将后半段取出

证明: 将虚线部分替换最优解结果不会变差

假设给定的解 b i b_i bi有 k k k段
假设后面的值是 b i = w > v b_i = w > v bi=w>v, 意味着后半段最优解不取 v v v应该取 w w w会变得更好, 因此后面一段的中位数 ≤ v \le v ≤v

意味着红色框住的部分至少有一半的数 ≤ v \le v ≤v, 向下移动 x x x, 也就是 b i − x b_i - x bi−x, 拉近了 ∣ b i − a i ∣ |b_i - a_i| ∣bi−ai∣的值, 使得 a i a_i ai与 b i b_i bi更近了

但是对于另一半来说变化值一定在 [ − x , x ] [-x, x] [−x,x]之间, 最坏情况每个数字 + x +x +x, 但是至少有一半的数字 − x -x −x了, 结果不会变差
但是操作的影响是将第二段移动到了第一段的位置, 少了一段

这样操作后, 最终就剩一段, 值为 b 1 b_1 b1

猜想(2): 选择虚线部分 [ v , b 1 ] [v, b_1] [v,b1], 结果不会更差

答案等于
$$
|a_{\frac{n}{2}} - x| + |a_{\frac{n}{2} + 1} - x|
- |a_{\frac{n}{2} - 1} - x| + |a_{\frac{n}{2} + 2} - x| + …
$$
证明猜想(2): 一共 n 2 \frac{n}{2} 2n对数, x x x靠近中位数 v v v对答案没影响

结论是将 b i b_i bi替换为 [ b 1 , n ] [b_1, n] [b1,n]的任意数, 结果不会变差

假设前面已经维护了最优解, 对于当前新加入的位置看作是一段, 如果是单调上升的不需要处理

合并两段, 如果发现新合并的中位数还是低于前面的段, 那么继续合并

合并结束后, 一定是递增的段, 每一段的最优解是 X 0 , X 1 , . . . , X k X_0, X_1, ..., X_k X0,X1,...,Xk, 那么整体的最优解就是 X 0 + X 1 + . . . + X k X_0 + X_1 + ... + X_k X0+X1+...+Xk

合并中位数的过程可以使用左偏树维护, 较少一半数的最大值就是中位数

合并两段, 中位数一定在这两段之间, 如果合并后的中位数是上半段被左偏树维护就是正确的, 但是如果是下半段不在左偏树中得到的中位数就是错误的!
但是, 在当前问题中是左偏树是可以维护两段的中位数的, 分为两种情况
- 当前合并的线段只有一个数字
不会产生错误的情况
- 当前合并的是一段数

因为插入一个数最多只会在原中位数左右一个位置处发生变化

也就是说新的中位数和原中位数之间是空的, 不会产生错误的情况
因此, 才可以使用左偏树维护中位数的情况
示例代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
int n;
int v[N], dis[N], ls[N], rs[N];
struct Seg {
int ed, root, sz;
} stk[N];
int ans[N];
int merge(int x, int y) {
if (!x || !y) return x + y;
if (v[y] > v[x]) swap(x, y);
rs[x] = merge(rs[x], y);
if (dis[rs[x]] > dis[ls[x]]) swap(ls[x], rs[x]);
dis[x] = dis[rs[x]] + 1;
return x;
}
int pop(int x) {
return merge(ls[x], rs[x]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> v[i];
v[i] -= i;
}
int t = 0;
for (int i = 1; i <= n; ++i) {
Seg cur = {
i, i, 1};
dis[i] = 1;
while (t && v[cur.root] < v[stk[t].root]) {
cur.root = merge(cur.root, stk[t].root);
if (cur.sz & 1 && stk[t].sz & 1) cur.root = pop(cur.root);
cur.sz += stk[t].sz;
t--;
}
stk[++t] = cur;
}
// 计算每一段中位数
for (int i = 1, j = 1; i <= t; ++i) {
while (j <= stk[i].ed) ans[j++] = v[stk[i].root];
}
LL res = 0;
for (int i = 1; i <= n; ++i) res += abs(ans[i] - v[i]);
cout << res << '\n';
for (int i = 1; i <= n; ++i) cout << ans[i] + i << ' ';
cout << '\n';
return 0;
}
*K单调

分解为 k k k个不相交相邻的的单调序列, 问最小代价
一般来说分解 + 计算极值, 尝试用动态规划解决
数据范围最大 1000 1000 1000, 可以预处理 c o s t ( i , j ) cost(i, j) cost(i,j), 代表将 [ i , j ] [i, j] [i,j]这一段变为单调序列的最小代价, 暴力预处理 O ( n 2 log n ) O(n ^ 2 \log n) O(n2logn)
定义状态表示 f ( i , j ) f(i, j) f(i,j)表示从 1 ∼ a i 1 \sim a_i 1∼ai变为 k k k段单调序列的最小代价
状态转移看最后位置不同点, 因为分为了 j j j段, 按照最后一段长度能从什么位置转移过来, 形式化的表示为
f ( i , j ) = min ( f ( i , j ) , f ( i − k , j − 1 ) + c o s t ( i − k + 1 , i ) ) f(i, j) = \min(f(i, j), f(i - k, j - 1) + cost(i - k + 1, i)) f(i,j)=min(f(i,j),f(i−k,j−1)+cost(i−k+1,i))
但是因为每一段区间可以单调上升也可以单调下降, 需要处理单调下降的情况
示例代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 12;
int n, m;
int f[N][M], cost[N][N];
int a[N], v[N], dis[N], ls[N], rs[N];
struct Seg {
int root;
int tot_sz, tot_sum;
int tr_sz, tr_sum;
int calc_cost() const {
int mid = v[root];
return mid * tr_sz - tr_sum + tot_sum - tr_sum - (tot_sz - tr_sz) * mid;
}
} stk[N];
int merge(int x, int y) {
if (!x || !y) return x + y;
if (v[y] > v[x]) swap(x, y);
rs[x] = merge(rs[x], y);
if (dis[rs[x]] > dis[ls[x]]) swap(ls[x], rs[x]);
dis[x] = dis[rs[x]] + 1;
return x;
}
int pop(int x) {
return merge(ls[x], rs[x]);
}
void get_cost(int u) {
int t = 0, ans = 0;
for (int i = u; i <= n; ++i) {
Seg cur = {
i, 1, v[i], 1, v[i]};
ls[i] = rs[i] = 0, dis[i] = 1;
while (t && v[cur.root] < v[stk[t].root]) {
ans -= stk[t].calc_cost();
cur.root = merge(cur.root, stk[t].root);
// 两段是奇数需要将cur段的中位数弹出
bool flag = cur.tot_sz & 1 && stk[t].tot_sz & 1;
cur.tot_sz += stk[t].tot_sz;
cur.tot_sum += stk[t].tot_sum;
cur.tr_sz += stk[t].tr_sz;
cur.tr_sum += stk[t].tr_sum;
if (flag) {
cur.tr_sz--;
cur.tr_sum -= v[cur.root];
cur.root = pop(cur.root);
}
t--;
}
stk[++t] = cur;
ans += cur.calc_cost();
cost[u][i] = min(cost[u][i], ans);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i];
memset(cost, 0x3f, sizeof cost);
for (int i = 1; i <= n; ++i) v[i] = a[i] - i;
for (int i = 1; i <= n; ++i) get_cost(i);
for (int i = 1; i <= n; ++i) v[i] = -a[i] - i;
for (int i = 1; i <= n; ++i) get_cost(i);
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
for (int k = 1; k <= i; ++k) {
f[i][j] = min(f[i][j], f[i - k][j - 1] + cost[i - k + 1][i]);
}
}
}
cout << f[n][m] << '\n';
return 0;
}

京公网安备 11010502036488号