题解:CF2205D Simons and Beating Peaks
注意到,数组的最大值必须置于两端。否则,我们必须将其左侧或右侧的所有元素全部删除,然后得到一个新的子数组。每次操作都会导致区间的缩小,因而可以递归求解。
具体地,对于区间 [l,r],如果 ax=maxl≤i≤rai,那么我们有两种选择:
删去下标在 l∼x−1 内的所有元素,区间缩小为 [x+1,r]。
删去下标在 x+1∼r 内的所有元素,区间缩小为 [l,x−1]。
因此,递推式为:
fl,r=min(r−x+fl,x−1,x−l+fx+1,r)
如果暴力求解区间最大值,那么平均复杂度为 O(nlogn),但若原数组单调,则会被卡成 O(n2)。
故考虑用 ST 表维护区间最大值,递归过程可优化至 O(n)。
时间复杂度为 O(∑nlogn),瓶颈在 ST 表的初始化。
#include<bits/stdc++.h> usingnamespace std; int t, n, a[500001], pos[500001], st[500001][19]; intquery(int l, int r){ int j = __lg(r - l + 1); returnmax(st[l][j], st[r-(1<<j)+1][j]); } intsolve(int l, int r){ if (r - l <= 1) return0; int x = pos[query(l,r)]; returnmin(r - x + solve(l, x - 1), x - l + solve(x + 1, r)); } intmain(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin >> t; while (t--) { cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; pos[a[i]] = i; } for (int i = 1; i <= n; i++) st[i][0] = a[i]; for (int j = 1; (1 << j) <= n; j++) for (int i = 1; i + (1 << j) - 1 <= n; i++) st[i][j] = max(st[i][j-1], st[i+(1<<(j-1))][j-1]); cout << solve(1, n) << '\n'; } return0; }
笛卡尔树
笛卡尔树
引入
笛卡尔树是一种二叉树,每一个节点由一个键值二元组 (k,w) 构成。要求 k 满足二叉搜索树(BST)的性质,而 w 满足堆的性质。如果笛卡尔树的 k,w 键值确定,且 k 互不相同,w 也互不相同,那么这棵笛卡尔树的结构是唯一的。如下图:
(图源自维基百科)
上面这棵笛卡尔树相当于把数组元素值当作键值 w,而把数组下标当作键值 k。可以发现,这棵树的键值 k 满足 BST 的性质,而键值 w 满足小根堆的性质。同时根据二叉搜索树的性质,可以发现这种特殊的笛卡尔树满足一棵子树内的下标是一个连续区间。
竞赛中使用笛卡尔树时,常用数组下标作为二元组的键值 k,数组下标 k 满足 BST 性质。
下文使用 k,w 时,默认 k 满足 BST 性质,w 满足堆的性质。
单调栈构建笛卡尔树
过程
我们考虑将元素按 k 升序依次插入到当前的笛卡尔树中。
对于一棵笛卡尔树,定义「右链」为从根节点开始一直走右儿子,走到叶节点形成的链。则插入节点后,这个节点一定在右链上。因为是按照满足 BST 性质的 k 升序插入,那么这个新插入的节点必然在树的最右端。这个节点不可能是一个左儿子,也没有右儿子。
于是我们执行这样一个过程,从下往上比较右链节点与当前节点 u 的 w,如果找到了一个右链上的节点 x 满足 wx<wu,就把 u 接到 x 的右儿子上,而 x 原本的右子树就变成 u 的左子树。
图中红框部分就是我们始终维护的右链:
显然每个数最多进出右链一次(或者说每个点在右链中存在的是一段连续的时间)。这个过程可以用单调栈维护,栈中维护当前笛卡尔树的右链上的节点。一个点不在右链上了就把它弹掉。这样每个点最多进出一次,复杂度 O(n)。
笛卡尔树与 Treap
实际上,Treap 是笛卡尔树的一种,只不过 Treap 中 w 的值完全随机。Treap 有线性的构建算法,如果提前将键值 k 排好序,是可以使用上述单调栈算法完成构建过程的,只不过很少会这么用。
C++ 实现
1 2 3 4 5 6 7 8 9
// stk 维护笛卡尔树中节点对应到序列中的下标 for (int i = 1; i <= n; i++) { int k = top; // top 表示操作前的栈顶,k 表示当前栈顶 while (k > 0 && w[stk[k]] > w[i]) k--; // 维护右链上的节点 if (k) rs[stk[k]] = i; // 栈顶元素.右儿子 := 当前元素 if (k < top) ls[i] = stk[k + 1]; // 当前元素.左儿子 := 上一个被弹出的元素 stk[++k] = i; // 当前元素入栈 top = k; }
每次操作是"选一个峰 ai,删掉 ai−1 或 ai+1"。被删除的只能是峰的邻居,而不是峰本身。全局最大值 n 永远不可能是某个峰的邻居(因为没有比它更大的元素),所以 n永远不会被删除。
既然 n 一定留在最终数组中,而最终数组是 V 形的,那么 n 作为最大值如果出现在内部,它一定是峰值——矛盾。所以 n必须在最终数组的左端点或右端点。
第三步:递归结构 = 笛卡尔树
假设 n 在位置 p:
n放左端点:p 左边的所有元素全部删掉,只保留 n + 右半部分的某个 V 形子序列。
n放右端点:p 右边的所有元素全部删掉,只保留左半部分的某个 V 形子序列 + n。
无论哪种情况,问题递归到了 n 的一侧。而那一侧的最大值(笛卡尔树对应子树的根)面临完全相同的约束——它也必须在该子问题最终数组的端点,否则它也是峰值。
这恰好是笛卡尔树的递归结构: