神圣庄严的古战场 - 题解
标签与难度
标签: 数据结构, 线段树, 动态开点线段树, 线段树合并, 栈 难度: 2400
题目大意喵~
你好呀,指挥官!战场的情况是这样的呐:我们有 n 个区块,排成一列。每个区块 i 都有一个属性 a_i(要么是 0 要么是 1)和一个权值 w_i。
神奈子大人会下达 m 次指令,分为两种:
- 修改指令:
1 x v,表示将第x个区块的权值w_x修改为v。 - 询问指令:
2 l r,表示询问区间[l, r]的“危险程度”。
“危险程度”是这么定义的:在一个区间里,我们可以选择任意一个相邻的、属性为 (0, 1) 的区块对,然后把它们“消去”。这个操作可以进行任意多次。我们的目标是通过一系列消去操作,让最终剩下的区块中,权值最大的那个区块的权值尽可能小。这个最小可能的最大权值,就是这个区间的危险程度啦!
简单来说,对于每次询问,我们要找到一种最优的消去策略,使得留下的元素中的最大权值最小,然后输出这个值,喵~
解题思路分析
这道题看起来有点复杂,但别怕,让我带你一步步拆解它,很快就能找到思路的!
Step 1: 理解“消去”的本质
首先,我们来分析这个“消去”操作。一个属性为 0 的区块和它右边相邻的一个属性为 1 的区块可以配对消去。这听起来是不是很像括号匹配呀?喵~ 我们可以把 0 看作左括号 (,把 1 看作右括号 )。
一个序列能够被完全消去,当且仅当它是一个合法的括号序列。比如 0 1 0 1 (( )),可以完全消去。0 0 1 1 (()) 也可以。而 1 0 )( 或者 0 0 1 ( ( ) 就会有剩余。
关键在于,对于一个固定的序列,无论我们按什么顺序消去相邻的 (0, 1) 对,最终剩下的元素集合是唯一的!就像括号匹配一样,一个左括号只会和唯一一个右括号匹配。我们可以用一个栈来找到所有能成功配对的 (0, 1)。
Step 2: 预处理匹配关系
既然 a 数组是固定的,我们可以预先处理出整个战场 [1, n] 中所有能配对的 (0, 1)。
我们可以用一个栈来模拟这个过程:
- 从左到右遍历所有区块
ifrom1ton。 - 如果
a_i是0,就把它的下标i推入栈中。 - 如果
a_i是1,并且栈不为空,那么就从栈顶弹出一个下标j。这就意味着j位置的0和i位置的1成功配对啦!我们记录下来,比如match[j] = i且match[i] = j。 - 如果
a_i是1但栈是空的,或者遍历结束后栈里还剩下一些0的下标,那这些区块就是“孤儿”,它们在整个战场里都找不到伴侣。
通过这个预处理,我们就得到了一个 match 数组,它告诉我们每个区块的配对伙伴在哪里。对于没有伙伴的区块,我们可以让 match[i] 等于一个特殊值,比如 0 或者 n+1。
Step 3: 重新定义“危险程度”
现在,对于一个查询区间 [l, r],哪些区块会留下呢? 一个在 [l, r] 内的区块 i,如果它的配对伙伴 match[i] 也在 [l, r] 内,那么它们俩就可以在区间内部“互相消化”,被消去。 反之,如果区块 i 在 [l, r] 内,但它根本没有伙伴,或者它的伙伴 match[i] 在区间外部(即 match[i] < l 或 match[i] > r),那它在区间 [l, r] 内就找不到自己的另一半,注定要被留下来。
所以,查询 [l, r] 的危险程度,就等价于寻找所有满足下面条件的区块 i,并求出它们权值 w_i 的最大值:
i在区间[l, r]内。i没有伙伴,或者match[i]不在[l, r]内。
Step 4: 将问题转化为数据结构问题
这个问题现在变成了一个带有单点修改的复杂范围查询问题。每次查询 [l, r],我们要找的是:
我们可以把这个问题拆成三部分,分别求最大值,最后再取一次总的最大值:
- 情况一 (完全失配):
i \in [l, r]且i是全局都无法匹配的。 - 情况二 (向左匹配):
i \in [l, r]且match[i] < l。 - 情况三 (向右匹配):
i \in [l, r]且match[i] > r。
注意到,如果 a_i = 0,它的伙伴 match[i] 一定在它右边,即 match[i] > i。如果 a_i = 1,则 match[i] < i。所以情况二只会是 a_i=1 的区块,情况三只会是 a_i=0 的区块。
这三个子问题都像是二维平面上的范围查询。例如,情况三是在查询满足 i \in [l, r] 和 match[i] \in [r+1, n] 的点 (i, match[i]) 的最大权值。
直接用二维数据结构(如二维线段树)可能会因为时空复杂度过高而无法通过。一个更优化的方法是使用线段树套动态开点线段树,或者叫线段树合并的技巧。
Step 5: 设计我们的“猫爪”数据结构
我们的核心武器是一个外层线段树,它以区块的下标 i 进行划分。
- 外层线段树的每个节点
p,代表一个下标区间[L, R]。 - 在这个节点
p里,我们维护两种信息:unmatched_max: 一个整数,表示在[L, R]区间内所有全局失配区块的最大权值。matched_root: 一个指针,指向一棵内层动态开点线段树的根。这棵内层树是为[L, R]区间内所有有配对的区块建立的。内层树以它们的伙伴下标match[i]作为索引,存储对应的权值w_i。
建树 (Build):
- 我们从下往上建立外层线段树。
- 对于叶子节点
i,如果i是失配的,就更新unmatched_max。如果i是有配对的,就为它创建一个只含一个元素的内层树(在match[i]位置插入w_i),并让matched_root指向它。 - 对于非叶子节点,它的
unmatched_max是左右儿子unmatched_max的最大值。它的matched_root是通过合并左右儿子的内层线段树得到的。线段树合并是一种神奇的魔法,它可以在 的时间内(对于单点插入形成的树)将两棵动态开点线段树的信息整合在一起!
修改 (Update):
- 当
w_x改变时,我们对外层线段树进行一次单点修改。 - 从根节点往下走到代表
x的叶子节点。路径上的每个节点p,如果x是失配的,就更新unmatched_max;如果x是有配对的,就在p的内层树中,对match[x]位置的值进行修改。这个过程的复杂度是 。
查询 (Query):
- 对于查询
[l, r],我们在外层线段树上查询[l, r]。这会把查询分解到 个外层节点上。 - 对于每个被完整覆盖的外层节点
p:- 我们把它的
unmatched_max作为一个候选答案。 - 我们查询它的内层树(从
matched_root开始),寻找match值在[1, l-1]和[r+1, n]范围内的最大权值。
- 我们把它的
- 将所有这些 个节点得到的结果取最大值,就是最终答案啦!查询的复杂度也是 。
这种结构巧妙地将二维查询分解,并用线段树合并技术优化了时空效率,是不是很厉害呀,喵~
代码实现
这是我根据上面的思路,精心为你准备的一份代码哦~ 注释写得很详细,希望能帮到你!
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
const int INF = 0; // 用0作为权值的最小值
// --- 动态开点线段树 (内层树) ---
struct InnerNode {
int left_child = 0, right_child = 0;
int max_val = INF;
};
std::vector<InnerNode> inner_tree;
int inner_node_count = 0;
// 创建一个新的内层树节点
int new_inner_node() {
inner_tree.emplace_back();
return ++inner_node_count;
}
// 内层树:更新 (单点修改)
void update_inner(int& p, int l, int r, int pos, int val) {
if (!p) p = new_inner_node();
if (l == r) {
inner_tree[p-1].max_val = val;
return;
}
int mid = l + (r - l) / 2;
if (pos <= mid) {
update_inner(inner_tree[p-1].left_child, l, mid, pos, val);
} else {
update_inner(inner_tree[p-1].right_child, mid + 1, r, pos, val);
}
int l_max = inner_tree[p-1].left_child ? inner_tree[inner_tree[p-1].left_child-1].max_val : INF;
int r_max = inner_tree[p-1].right_child ? inner_tree[inner_tree[p-1].right_child-1].max_val : INF;
inner_tree[p-1].max_val = std::max(l_max, r_max);
}
// 内层树:查询 (范围最大值)
int query_inner(int p, int l, int r, int ql, int qr) {
if (!p || ql > qr || l > qr || r < ql) return INF;
if (ql <= l && r <= qr) {
return inner_tree[p-1].max_val;
}
int mid = l + (r - l) / 2;
int res = INF;
if (ql <= mid) {
res = std::max(res, query_inner(inner_tree[p-1].left_child, l, mid, ql, qr));
}
if (qr > mid) {
res = std::max(res, query_inner(inner_tree[p-1].right_child, mid + 1, r, ql, qr));
}
return res;
}
// 内层树:合并两棵树
int merge_inner(int p1, int p2, int l, int r) {
if (!p1) return p2;
if (!p2) return p1;
int new_p = new_inner_node();
if (l == r) {
inner_tree[new_p-1].max_val = std::max(inner_tree[p1-1].max_val, inner_tree[p2-1].max_val);
return new_p;
}
int mid = l + (r - l) / 2;
inner_tree[new_p-1].left_child = merge_inner(inner_tree[p1-1].left_child, inner_tree[p2-1].left_child, l, mid);
inner_tree[new_p-1].right_child = merge_inner(inner_tree[p1-1].right_child, inner_tree[p2-1].right_child, mid + 1, r);
int l_max = inner_tree[new_p-1].left_child ? inner_tree[inner_tree[new_p-1].left_child-1].max_val : INF;
int r_max = inner_tree[new_p-1].right_child ? inner_tree[inner_tree[new_p-1].right_child-1].max_val : INF;
inner_tree[new_p-1].max_val = std::max(l_max, r_max);
return new_p;
}
// --- 线段树 (外层树) ---
struct OuterNode {
int matched_root = 0;
int unmatched_max = INF;
};
std::vector<OuterNode> outer_tree;
int N;
std::vector<int> a, w, match;
// 外层树:建树
void build_outer(int p, int l, int r) {
if (l == r) {
if (match[l] != 0) { // 有配对
update_inner(outer_tree[p-1].matched_root, 1, N, match[l], w[l]);
} else { // 无配对
outer_tree[p-1].unmatched_max = w[l];
}
return;
}
int mid = l + (r - l) / 2;
build_outer(2 * p, l, mid);
build_outer(2 * p + 1, mid + 1, r);
outer_tree[p-1].unmatched_max = std::max(outer_tree[2*p-1].unmatched_max, outer_tree[2*p+1-1].unmatched_max);
outer_tree[p-1].matched_root = merge_inner(outer_tree[2*p-1].matched_root, outer_tree[2*p+1-1].matched_root, 1, N);
}
// 外层树:更新
void update_outer(int p, int l, int r, int pos, int val) {
if (l == r) {
w[pos] = val; // 更新原始权重数组
if (match[pos] != 0) { // 有配对
update_inner(outer_tree[p-1].matched_root, 1, N, match[pos], val);
} else { // 无配对
outer_tree[p-1].unmatched_max = val;
}
return;
}
int mid = l + (r - l) / 2;
if (pos <= mid) {
update_outer(2 * p, l, mid, pos, val);
} else {
update_outer(2 * p + 1, mid + 1, r, pos, val);
}
if (match[pos] != 0) {
update_inner(outer_tree[p-1].matched_root, 1, N, match[pos], val);
} else {
outer_tree[p-1].unmatched_max = std::max(outer_tree[2*p-1].unmatched_max, outer_tree[2*p+1-1].unmatched_max);
}
}
// 外层树:查询
int query_outer(int p, int l, int r, int ql, int qr) {
if (l > qr || r < ql) return INF;
if (ql <= l && r <= qr) {
int res = outer_tree[p-1].unmatched_max;
// 匹配到左边
res = std::max(res, query_inner(outer_tree[p-1].matched_root, 1, N, 1, ql - 1));
// 匹配到右边
res = std::max(res, query_inner(outer_tree[p-1].matched_root, 1, N, qr + 1, N));
return res;
}
int mid = l + (r - l) / 2;
int res = INF;
res = std::max(res, query_outer(2 * p, l, mid, ql, qr));
res = std::max(res, query_outer(2 * p + 1, mid + 1, r, ql, qr));
return res;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int M;
std::cin >> N >> M;
a.resize(N + 1);
w.resize(N + 1);
match.assign(N + 1, 0);
for (int i = 1; i <= N; ++i) std::cin >> a[i];
for (int i = 1; i <= N; ++i) std::cin >> w[i];
// 预处理匹配关系
std::stack<int> s;
for (int i = 1; i <= N; ++i) {
if (a[i] == 0) {
s.push(i);
} else {
if (!s.empty()) {
match[s.top()] = i;
match[i] = s.top();
s.pop();
}
}
}
// 预估内层树节点数量,避免频繁的vector扩容
inner_tree.reserve(N * 20 * 2);
outer_tree.resize(4 * N);
build_outer(1, 1, N);
for (int k = 0; k < M; ++k) {
int type;
std::cin >> type;
if (type == 1) {
int x, val;
std::cin >> x >> val;
update_outer(1, 1, N, x, val);
} else {
int l, r;
std::cin >> l >> r;
std::cout << query_outer(1, 1, N, l, r) << "\n";
}
}
return 0;
}复杂度分析
时间复杂度:
- 预处理: 使用栈寻找所有匹配对,需要遍历一次数组,复杂度为 。
- 建树: 外层树有 个节点。在合并内层树时,
merge_inner的复杂度与两棵树中较小的那棵的大小成正比。总的建树时间复杂度是 。 - 修改: 一次修改操作会影响外层树中从根到叶子的一条路径,共 个节点。在每个节点上,我们都需要在它的内层树中进行一次单点修改,内层树的修改也是 。所以总的修改时间复杂度是 。
- 查询: 一次查询会分解到外层树的 个节点上。在每个节点上,我们对它的内层树进行两次范围查询,每次 。所以总的查询时间复杂度是 。
- 总时间复杂度: 。
空间复杂度:
- 外层线段树本身需要 的空间。
- 内层动态开点线段树的总节点数是关键。在建树过程中,通过线段树合并,总共创建的节点数是 。所以空间复杂度为 。
知识点总结
这道题是数据结构能力的绝佳试炼场,喵~ 它融合了多种技巧:
- 问题转化: 核心一步是将抽象的“消去”规则转化为具体的“括号匹配”模型,并进一步明确了“留下”元素的条件,把题目变成了数据结构可以处理的形式。
- 栈的应用: 使用栈来预处理匹配关系是解决这类括号匹配问题的经典方法,高效且直观。
- 线段树套线段树: 对于二维或类二维的查询问题,这是非常强大的工具。外层和内层分别处理一个维度。
- 动态开点与线段树合并: 为了解决内层线段树可能带来的巨大空间开销,我们使用了动态开点。而线段树合并则是在建树时高效地从子节点汇总信息的关键技术,它避免了在每个节点上从头构建内层树,大大优化了时空效率。
希望这篇题解能让你对这些复杂的“猫爪”魔法有更深的理解!继续加油哦,指挥官!喵~