【区间最值线段树】Atcoder 408 F - Athletic
题目
https://atcoder.jp/contests/abc408/tasks/abc408_f
题意
输入 \(n,d,r(1 \leq n \leq 5 \times 10^5, 1 \leq d \leq n,1 \leq r \leq n)\) 和一个大小为 \((n-1)\) 的排列 \(a\)。下标从 \(1\) 开始。
选择一个 \(1~n\) 中的下标,作为起点 \(i\),每次跳跃,选择一个下标 \(j\),满足 \(a[j] \leq a[i] - d \text{ 且 } 1 \leq |i-j| \leq r\)。重复上述步骤,直至无法跳跃。
输出最大跳跃次数。
题解
从下标 \(i\) 向下标 \(j\) 跳跃,首先需要满足 \(a[j] \leq a[i] - d\),这就说明只会从值较大的向值较小的跳跃,不妨用以下数据结构维护排列 \(a\) 的每一个节点:
struct node {
int x;// 节点值
int idx;// 节点下标
} a[N];
那么可以考虑对排列 \(a\) 的下标按照元素值从大到小进行排序(下文提及的排列 \(a\) 若未专门标注,则默认表示的是排序后的排列 \(a\)),随后使用快慢指针进行维护,快指针维护下标 \(i\),慢指针维护下标 \(j\)。
对于条件 \(1 \leq |i-j| \leq r\),可以使用最大值线段树进行维护。具体地,创建一颗大小为 \(n + 1\) 的最大值线段树(这里的大小指的是维护共有 \(n + 1\) 个叶子节点,线段树的节点个数需要开 \(4 \times (n + 1)\) 个节点),将线段树的每个叶子节点的值初始化为 \(0\),第 \(x\) 个叶子结点的值代表以排列 \(a\) 的第 \(x\) 个节点为跳跃终点的最大跳跃次数。线段树的第 \(x\) 个叶子节点,对应的是初始未排序的排列 \(a\) 的元素下标。
此外,再创建一个大小为 \(n+1\) 的数组 \(mx\),其中 \(mx[j]\) 代表跳跃终点为排列 \(a\) 第 \(j\) 个元素的最大跳跃次数,初始化为 \(0\)。
当慢指针维护到下标 \(j\) 时,将 \(mx[j]\) 更新到最值线段树的第 \(a[j].idx\) 个叶子节点。当慢指针无法再移动时,求出在最值线段树的区间 \([max(1, a[i].idx-r), min(n,a[i].idx+r)]\) 上的最大值,加上 \(1\) (因为节点发生跳跃,所以节点多了一个)以后加在 \(mx[i]\) 上。
因为跳跃不计入起点,因此最后的统计结果,需要再减去 \(1\)。
参考代码
#include<bits/stdc++.h>
#define lc(x) ((x)<<1)
#define rc(x) ((x)<<1|1)
using namespace std;
constexpr int N = 5e5 + 7;
int ans, n, d, r;
int mx[N];
struct node {
int x;// 节点值
int idx;// 节点下标
} a[N];
struct treeNode {
int l, r;// 区间[l, r]
int max;// 最大值
int lazy;// 懒修改值
} tr[N << 2];
void pushUp(int p) {
tr[p].max = max(tr[lc(p)].max, tr[rc(p)].max);
}
void pushDown(int p) {
if (tr[p].lazy) {
int l = lc(p), r = rc(p);
tr[l].max = max(tr[l].max, tr[p].lazy);
tr[l].lazy = max(tr[l].lazy, tr[p].lazy);
tr[r].max = max(tr[r].max, tr[p].lazy);
tr[r].lazy = max(tr[r].lazy, tr[p].lazy);
tr[p].lazy = 0;
}
}
void build(int p/*线段树节点编号*/, int l/*该节点管理的区间左端点*/, int r/*该节点管理的区间右端点*/) {
tr[p] = {l, r, 0, 0};// 节点 p 维护区间 [l, r] 的信息
if (l == r) return ;// 若已经是叶子节点,无需再次递归到子节点,可以直接返回
int m = l + r >> 1;// 二分线段 [l, r],将线段划分为左子树 [l, m] 和右子树 [m + 1, r]
build(lc(p), l, m);// 建立左子树
build(rc(p), m + 1, r);// 建立右子树
pushUp(p);// 根据左右子树信息,更新当前节点信息
}
void update(int p, int l, int r, int k) {
if (l <= tr[p].l && tr[p].r <= r) {// 节点 p 的管辖区间 [tr[p].l, tr[p].r] 位于 [l, r] 之间
tr[p].lazy = max(tr[p].lazy, k);// 更新懒标记信息,记录子节点需要更新的数据
tr[p].max = max(tr[p].max, k);// 更新当前节点的最大值
return ;
}
pushDown(p);// 将懒标记信息传给左右子节点
int m = tr[p].l + tr[p].r >> 1;// 二分区间 [tr[p].l, tr[p].r],划分为 [tr[p].l, m] 和 [m + 1, tr[p],r]
if (l <= m) update(lc(p), l, r, k);// 如果左子树维护的区间 [tr[lc(p).l, tr[lc(p)].r] 和 [l, r] 有交集则递归左子树
if (m < r) update(rc(p), l, r, k);// 如果右子树维护的区间 [tr[rc(p).l, tr[rc(p)].r] 和 [l, r] 有交集则递归右子树
pushUp(p);// 左右子树更新完,需要更新当前节点信息
}
int query(int p, int l, int r) {
if (l <= tr[p].l && tr[p].r <= r) return tr[p].max;// 节点 p 的管辖区间 [tr[p].l, tr[p].r] 位于 [l, r] 之间
int ans = 0;
pushDown(p);// 将懒标记信息传给左右子节点
int m = tr[p].l + tr[p].r >> 1;// 二分区间 [tr[p].l, tr[p].r],划分为 [tr[p].l, m] 和 [m + 1, tr[p],r]
if (l <= m) ans = query(lc(p), l, r);// 如果左子树维护的区间 [tr[lc(p).l, tr[lc(p)].r] 和 [l, r] 有交集则递归左子树
if (m < r) ans = max(ans, query(rc(p), l, r));// 如果右子树维护的区间 [tr[rc(p).l, tr[rc(p)].r] 和 [l, r] 有交集则递归右子树
return ans;
}
int main() {
ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
cin >> n >> d >> r;
for (int i = 1; i <= n; ++ i) {
cin >> a[i].x;
a[i].idx = i;
}
// 先按值从大到小排,再按下班从大到小排
sort(a + 1, a + 1 + n, [&](node &x, node &y) {
return x.x != y.x ? x.x > y.x : x.idx > y.idx;
});
// 建立最值线段树
build(1, 1, n);
for (int i = 1, j = 1; i <= n; ++ i) {
while (a[i].x <= a[j].x - d) {
update(1, a[j].idx, a[j].idx, mx[j]);
++ j;
}
mx[i] = 1 + query(1, max(a[i].idx - r, 1), min(n, a[i].idx + r));
ans = max(ans, mx[i]);
}
cout << ans - 1 << '\n';
return 0;
}
浙公网安备 33010602011771号