「Nhk R2」链上之链 - 题解
标签与难度
标签: 数据结构, 平衡树, Treap, FHQ-Treap, 动态DP, 序列维护, 树上问题, 嵌套数据结构 难度: 2900
题目大意喵~
主人你好呀,这道题是关于一个奇特的“链上之链”结构的动态维护问题,喵~
我们有一个序列,序列里的每个元素本身就是一条带权值的“小链”。我们需要支持四种操作:
- 删除:
0 x,把序列中第x个小链删掉。 - 插入:
1 x k v1 ... vk,在第x个小链后面,插入一条新的、长度为k的小链。 - 区间修改:
2 x l r v,把第x个小链上,从第l个节点到第r个节点的权值都改成v。 - 长度减半:
3 l r,对于序列中从第l个到第r个的每一条小链,如果它的长度len大于1,就把它变短,只保留前ceil(len / 2)个节点。
每次操作结束后,我们需要回答一个询问:把所有小链按照它们在序列中的顺序,将相邻小链的第1个节点用边连起来,形成一个大图。请问,这个大图中的最长链(路径上节点权值和最大)是多少呢?注意,路径可以为空,此时权值和为0,喵~
解题思路分析
这道题看起来好复杂呀,又是序列操作,又是图论的最长链,还要维护链中链... 但别怕,让我来一步步拆解它,喵~
关键转换:从图论到序列DP
首先,我们来分析一下每次询问的这个“大图”到底长什么样。它是由一排小链 组成的。
- 在每条小链 内部,节点 和 是相连的。
- 在小链之间, 的第一个节点 和 的第一个节点 是相连的。
如果我们将这些边看作是无向的(因为“最长链”通常在无向图上讨论),这个大图的结构就像一只毛毛虫!🐛 构成了毛毛虫的身体(主干),而每个小链剩下的部分 就像是挂在身体上的脚。
在这样的树形(毛毛虫也是一种树)结构上求最长链,是一个经典的动态规划问题。但是,我们的“毛毛虫”是动态变化的!插入、删除、修改... 这就需要一种能够支持动态修改的DP,也就是我们常说的“动态DP”。
处理动态序列问题,最强大的武器之一就是平衡二叉搜索树 (BBST),比如Splay或者Treap。考虑到操作的复杂性,使用Treap(特别是无需旋转的FHQ-Treap)会让代码实现更清晰,喵~
嵌套Treap:我的魔法盒子
我们面对的是两个层级的动态序列:
- 外层序列: 小链组成的序列。
- 内层序列: 每个小链内部的节点序列。
一个非常自然的想法就是用一个数据结构去套另一个。所以,我们可以用一个外层Treap来维护小链的序列,外层Treap的每个节点代表一条小链。而这条小链本身,我们用一个内层Treap来维护它内部的节点序列。就像一个大魔法盒(外层Treap)里装着许多小魔法盒(内层Treap)一样,可爱吧~
内层Treap:维护小链信息
内层Treap相对简单。对于一条小链,我们需要知道哪些信息来帮助外层计算呢?
- 最大子段和: 这对应着完全在一条小链内部的最长路径。
- 最大前缀和: 在无向图中,从第一个节点 出发能走到的最长路径(不离开本链),就是这条链的最大前缀和。
- 第一个节点的值: 这是连接相邻小链的“关节”点。
所以,我们的内层Treap是一个标准的、用于维护序列最大子段和的Treap。每个节点需要维护区间和 sum、最大前缀和 lmax、最大后缀和 rmax、最大子段和 ans。当然,为了支持区间修改,我们还需要加上懒标记 tag。
外层Treap:DP状态的定义与合并
这是最核心的部分!外层Treap的每个节点代表一个或多个连续的小链组成的“链段”。我们需要为它设计一套DP状态和合并规则。
假设一个外层Treap节点代表了小链 组成的链段。我们需要维护以下信息:
chain_head_sum: 链段中所有小链的第一个节点的权值之和,即 。max_l_path: 从链段的“头”(即 的第一个节点 )出发,能走到的最长路径的权值和。max_r_path: 从链段的“尾”(即 的第一个节点 )出发,能走到的最长路径的权值和。max_internal_path: 完全包含在该链段内部的最长路径的权值和。
对于只包含一条小链 的“叶子”节点,它的初始DP状态是:
chain_head_sum= (第一个节点的值)max_l_path= (小链 的最大前缀和)max_r_path= (因为只有一个连接点,左右对称)max_internal_path= (小链 的最大子段和)
这些值 () 都可以从对应的内层Treap中查询得到。
合并规则 (最重要的魔法!) 假设我们要合并左边的链段 A 和右边的链段 B,得到新的链段 New:
New.max_internal_path=A.max_internal_path,B.max_internal_path,A.max_r_path+B.max_l_path// 关键!连接两个链段的最长路径New.chain_head_sum=A.chain_head_sum+B.chain_head_sumNew.max_l_path=A.max_l_path,A.chain_head_sum+B.max_l_path// 穿过整个A,再走B的左路径New.max_r_path=B.max_r_path,B.chain_head_sum+A.max_r_path// 穿过整个B,再走A的右路径
有了这些,我们就可以在O(1)时间内合并两个外层Treap节点的信息啦。最终,整个序列的最长链就是外层Treap根节点的 max_internal_path!
处理各种操作
- 删除/插入: 对外层Treap进行
split和merge操作,非常标准。 - 区间修改:
split外层Treap找到对应的小链节点,然后进入其内层Treap,执行标准的区间修改(split->apply_tag->merge)。修改后,别忘了更新该外层节点的DP信息。 - 长度减半: 这是最麻烦的操作。我们需要
split外层Treap得到对应区间的子树,然后对这个子树里的每一个节点,找到它的内层Treap,split掉后半部分,再更新外层节点的DP信息。我们可以写一个递归函数来遍历这个子树并执行修改。为了优化,可以加一个剪枝:如果一个子树中所有小链的长度都已经是1了,就不用再递归进去了,喵~
总而言之,我们用一个精巧的“Treap套Treap”结构,把复杂的图问题转化为了序列上的动态DP问题,然后用平衡树高效解决!是不是很有趣呢?
代码实现
这是我根据上面的思路,精心重构的一份代码,加了详细的注释,希望能帮助主人理解,喵~
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
using namespace std;
typedef long long ll;
// 使用mt19937生成随机优先级,让Treap更稳定
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
// -------------------- 内层Treap: 维护小链节点 --------------------
namespace InnerTreap {
struct Node {
int priority, size;
ll value;
ll sum, lmax, rmax, ans; // 标准最大子段和DP信息
ll tag;
bool has_tag;
int ch[2]; // 左右孩子
};
Node tree[1000005];
int node_count = 0;
int new_node(ll val) {
int p = ++node_count;
tree[p].priority = uniform_int_distribution<int>()(rng);
tree[p].size = 1;
tree[p].value = val;
tree[p].sum = val;
tree[p].lmax = max(0LL, val);
tree[p].rmax = max(0LL, val);
tree[p].ans = max(0LL, val);
tree[p].tag = 0;
tree[p].has_tag = false;
tree[p].ch[0] = tree[p].ch[1] = 0;
return p;
}
void push_up(int p) {
if (!p) return;
int l = tree[p].ch[0], r = tree[p].ch[1];
tree[p].size = tree[l].size + tree[r].size + 1;
tree[p].sum = tree[l].sum + tree[r].sum + tree[p].value;
tree[p].lmax = max(tree[l].lmax, tree[l].sum + tree[p].value + tree[r].lmax);
tree[p].rmax = max(tree[r].rmax, tree[r].sum + tree[p].value + tree[l].rmax);
tree[p].ans = max({tree[l].ans, tree[r].ans, tree[l].rmax + tree[p].value + tree[r].lmax});
}
void apply_tag(int p, ll tag_val) {
if (!p) return;
tree[p].value = tag_val;
tree[p].sum = (ll)tree[p].size * tag_val;
tree[p].lmax = tree[p].rmax = tree[p].ans = max(0LL, tree[p].sum);
tree[p].tag = tag_val;
tree[p].has_tag = true;
}
void push_down(int p) {
if (!p || !tree[p].has_tag) return;
int l = tree[p].ch[0], r = tree[p].ch[1];
if (l) apply_tag(l, tree[p].tag);
if (r) apply_tag(r, tree[p].tag);
tree[p].has_tag = false;
}
// 按大小分裂,k个节点给x, 剩下的给y
void split(int p, int k, int& x, int& y) {
if (!p) {
x = y = 0;
return;
}
push_down(p);
if (tree[tree[p].ch[0]].size < k) {
x = p;
split(tree[p].ch[1], k - tree[tree[p].ch[0]].size - 1, tree[x].ch[1], y);
} else {
y = p;
split(tree[p].ch[0], k, x, tree[y].ch[0]);
}
push_up(p);
}
int merge(int x, int y) {
if (!x || !y) return x | y;
push_down(x);
push_down(y);
if (tree[x].priority > tree[y].priority) {
tree[x].ch[1] = merge(tree[x].ch[1], y);
push_up(x);
return x;
} else {
tree[y].ch[0] = merge(x, tree[y].ch[0]);
push_up(y);
return y;
}
}
// 初始化时,节点值是0,占位用
void init() { tree[0].sum = 0; tree[0].lmax = tree[0].rmax = tree[0].ans = 0;}
}
// -------------------- 外层Treap: 维护小链序列 --------------------
namespace OuterTreap {
struct Node {
int priority, size;
int inner_root; // 指向内层Treap的根
bool needs_halving; // 优化op3: 标记子树中是否有长度>1的链
// DP状态
ll chain_head_sum;
ll max_l_path, max_r_path;
ll max_internal_path;
int ch[2];
};
Node tree[200005];
int node_count = 0;
void update_from_inner(int p) {
if (!p) return;
int inner_p = tree[p].inner_root;
if (!inner_p) { // 空链
tree[p].chain_head_sum = 0;
tree[p].max_l_path = tree[p].max_r_path = tree[p].max_internal_path = 0;
tree[p].needs_halving = false;
return;
}
int first_node_root, rest;
InnerTreap::split(inner_p, 1, first_node_root, rest);
tree[p].chain_head_sum = InnerTreap::tree[first_node_root].value;
tree[p].max_l_path = tree[p].max_r_path = InnerTreap::tree[inner_p].lmax;
tree[p].max_internal_path = InnerTreap::tree[inner_p].ans;
tree[p].needs_halving = (InnerTreap::tree[inner_p].size > 1);
// 别忘了合并回去
InnerTreap::merge(first_node_root, rest);
}
int new_node(int inner_root) {
int p = ++node_count;
tree[p].priority = uniform_int_distribution<int>()(rng);
tree[p].size = 1;
tree[p].inner_root = inner_root;
tree[p].ch[0] = tree[p].ch[1] = 0;
update_from_inner(p);
return p;
}
void push_up(int p) {
if (!p) return;
int l = tree[p].ch[0], r = tree[p].ch[1];
tree[p].size = tree[l].size + tree[r].size + 1;
// 合并DP状态
const auto& node_l = tree[l];
const auto& node_r = tree[r];
const auto& node_self = tree[p];
tree[p].chain_head_sum = node_l.chain_head_sum + node_self.chain_head_sum + node_r.chain_head_sum;
ll l_part_l_path = node_l.max_l_path;
ll l_part_r_path = node_l.max_r_path;
ll l_part_sum = node_l.chain_head_sum;
ll l_part_ans = node_l.max_internal_path;
ll self_part_l_path = node_self.max_l_path;
ll self_part_r_path = node_self.max_r_path;
ll self_part_sum = node_self.chain_head_sum;
ll self_part_ans = node_self.max_internal_path;
ll r_part_l_path = node_r.max_l_path;
ll r_part_r_path = node_r.max_r_path;
ll r_part_sum = node_r.chain_head_sum;
ll r_part_ans = node_r.max_internal_path;
// A = l, B = self
ll mid_l_path = max(l_part_l_path, l_part_sum + self_part_l_path);
ll mid_r_path = max(self_part_r_path, self_part_sum + l_part_r_path);
ll mid_sum = l_part_sum + self_part_sum;
ll mid_ans = max({l_part_ans, self_part_ans, l_part_r_path + self_part_l_path});
// A = mid, B = r
tree[p].max_l_path = max(mid_l_path, mid_sum + r_part_l_path);
tree[p].max_r_path = max(r_part_r_path, r_part_sum + mid_r_path);
tree[p].max_internal_path = max({mid_ans, r_part_ans, mid_r_path + r_part_l_path});
tree[p].needs_halving = node_l.needs_halving || node_r.needs_halving || node_self.needs_halving;
}
void split(int p, int k, int& x, int& y) {
if (!p) {
x = y = 0;
return;
}
if (tree[tree[p].ch[0]].size < k) {
x = p;
split(tree[p].ch[1], k - tree[tree[p].ch[0]].size - 1, tree[x].ch[1], y);
} else {
y = p;
split(tree[p].ch[0], k, x, tree[y].ch[0]);
}
push_up(p);
}
int merge(int x, int y) {
if (!x || !y) return x | y;
if (tree[x].priority > tree[y].priority) {
tree[x].ch[1] = merge(tree[x].ch[1], y);
push_up(x);
return x;
} else {
tree[y].ch[0] = merge(x, tree[y].ch[0]);
push_up(y);
return y;
}
}
// 递归地对子树中所有需要减半的链进行操作
void halve_chains(int p) {
if (!p || !tree[p].needs_halving) return;
// 先处理孩子,再处理自己
halve_chains(tree[p].ch[0]);
halve_chains(tree[p].ch[1]);
if (tree[p].needs_halving) { // 再次检查,因为孩子可能已经被处理
int inner_p = tree[p].inner_root;
int current_len = InnerTreap::tree[inner_p].size;
if (current_len > 1) {
int new_len = (current_len + 1) / 2;
int kept_part, discarded_part;
InnerTreap::split(inner_p, new_len, kept_part, discarded_part);
tree[p].inner_root = kept_part;
}
}
update_from_inner(p); // 更新自己的DP信息
push_up(p); // 重新聚合信息
}
void init() { tree[0].max_internal_path = 0; }
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
InnerTreap::init();
OuterTreap::init();
int n, m;
cin >> n >> m;
int outer_root = 0;
for (int i = 0; i < n; ++i) {
int k;
cin >> k;
int inner_root = 0;
for (int j = 0; j < k; ++j) {
ll val;
cin >> val;
inner_root = InnerTreap::merge(inner_root, InnerTreap::new_node(val));
}
outer_root = OuterTreap::merge(outer_root, OuterTreap::new_node(inner_root));
}
cout << OuterTreap::tree[outer_root].max_internal_path << "\n";
for (int q = 0; q < m; ++q) {
int type;
cin >> type;
if (type == 0) {
int x;
cin >> x;
int left, mid, right;
OuterTreap::split(outer_root, x, mid, right);
OuterTreap::split(mid, x - 1, left, mid);
outer_root = OuterTreap::merge(left, right);
} else if (type == 1) {
int x, k;
cin >> x;
int inner_root = 0;
cin >> k;
for (int j = 0; j < k; ++j) {
ll val;
cin >> val;
inner_root = InnerTreap::merge(inner_root, InnerTreap::new_node(val));
}
int new_outer_node = OuterTreap::new_node(inner_root);
int left, right;
OuterTreap::split(outer_root, x, left, right);
outer_root = OuterTreap::merge(OuterTreap::merge(left, new_outer_node), right);
} else if (type == 2) {
int x, l, r;
ll v;
cin >> x >> l >> r >> v;
int left, mid, right;
OuterTreap::split(outer_root, x, mid, right);
OuterTreap::split(mid, x - 1, left, mid);
int inner_root = OuterTreap::tree[mid].inner_root;
int inner_l, inner_m, inner_r;
InnerTreap::split(inner_root, r, inner_m, inner_r);
InnerTreap::split(inner_m, l - 1, inner_l, inner_m);
InnerTreap::apply_tag(inner_m, v);
inner_root = InnerTreap::merge(InnerTreap::merge(inner_l, inner_m), inner_r);
OuterTreap::tree[mid].inner_root = inner_root;
OuterTreap::update_from_inner(mid);
outer_root = OuterTreap::merge(OuterTreap::merge(left, mid), right);
} else { // type == 3
int l, r;
cin >> l >> r;
int left, mid, right;
OuterTreap::split(outer_root, r, mid, right);
OuterTreap::split(mid, l - 1, left, mid);
OuterTreap::halve_chains(mid);
outer_root = OuterTreap::merge(OuterTreap::merge(left, mid), right);
}
cout << OuterTreap::tree[outer_root].max_internal_path << "\n";
}
return 0;
}复杂度分析
- 时间复杂度: 设
N是小链的数量,M是操作次数,L是小链的最大长度。- 操作0 (删除), 1 (插入), 2 (修改): 每次操作涉及对外层Treap和(可能的)内层Treap的
split和merge。外层Treap操作是 ,内层是 。所以这些操作的复杂度是 。 - 操作3 (长度减半): 这个操作最特别。我们需要对 区间的所有小链进行修改。我们先用 的时间 split 出代表这个区间的外层子Treap。然后,我们递归遍历这个子Treap。在最坏情况下,我们需要访问 个节点,对每个节点的内层Treap做 的 split 操作。所以一次操作3的复杂度是 。
- 操作0 (删除), 1 (插入), 2 (修改): 每次操作涉及对外层Treap和(可能的)内层Treap的
- 空间复杂度: 我们需要为每个节点(无论是外层还是内层)都分配空间。总节点数是所有小链长度之和加上小链的数量。设总节点数为
S,空间复杂度是 。
知识点总结
这道题是数据结构能力的终极考验之一,喵~ 它融合了多种思想:
- 问题建模: 将一个看似复杂的图论问题,通过分析其结构(毛毛虫图),转化为一个可以在序列上解决的动态DP问题。这是解题的第一步,也是最重要的一步!
- 嵌套数据结构: 使用“Treap套Treap”的结构来管理两个层级的动态序列,是解决此类问题的经典模式。
- 动态DP与平衡树: 平衡树(这里是FHQ-Treap)是实现动态DP的有力工具。通过在树的节点上维护DP状态,并定义好状态合并的规则,我们就可以在对数时间内完成查询和大部分修改。
- 懒标记思想: 在内层Treap中使用懒标记来支持区间修改,这是平衡树处理区间操作的标准技巧。
- 复杂操作的处理: 操作3(长度减半)需要对一个子树进行整体操作,通过递归遍历并修改,展示了平衡树强大的灵活性。同时,通过
needs_halving标志进行剪枝,是一个很重要的优化技巧。
希望这篇题解能帮到你,主人!如果还有不懂的地方,随时可以再来问我哦,喵~ ❤️