Skip to content

「Nhk R2」链上之链 - 题解

标签与难度

标签: 数据结构, 平衡树, Treap, FHQ-Treap, 动态DP, 序列维护, 树上问题, 嵌套数据结构 难度: 2900

题目大意喵~

主人你好呀,这道题是关于一个奇特的“链上之链”结构的动态维护问题,喵~

我们有一个序列,序列里的每个元素本身就是一条带权值的“小链”。我们需要支持四种操作:

  1. 删除: 0 x,把序列中第 x 个小链删掉。
  2. 插入: 1 x k v1 ... vk,在第 x 个小链后面,插入一条新的、长度为 k 的小链。
  3. 区间修改: 2 x l r v,把第 x 个小链上,从第 l 个节点到第 r 个节点的权值都改成 v
  4. 长度减半: 3 l r,对于序列中从第 l 个到第 r 个的每一条小链,如果它的长度 len 大于1,就把它变短,只保留前 ceil(len / 2) 个节点。

每次操作结束后,我们需要回答一个询问:把所有小链按照它们在序列中的顺序,将相邻小链的第1个节点用边连起来,形成一个大图。请问,这个大图中的最长链(路径上节点权值和最大)是多少呢?注意,路径可以为空,此时权值和为0,喵~

解题思路分析

这道题看起来好复杂呀,又是序列操作,又是图论的最长链,还要维护链中链... 但别怕,让我来一步步拆解它,喵~

关键转换:从图论到序列DP

首先,我们来分析一下每次询问的这个“大图”到底长什么样。它是由一排小链 C1,C2,,CNC_1, C_2, \dots, C_N 组成的。

  • 在每条小链 CiC_i 内部,节点 vi,jv_{i,j}vi,j+1v_{i,j+1} 是相连的。
  • 在小链之间,CiC_i 的第一个节点 vi,1v_{i,1}Ci+1C_{i+1} 的第一个节点 vi+1,1v_{i+1,1} 是相连的。

如果我们将这些边看作是无向的(因为“最长链”通常在无向图上讨论),这个大图的结构就像一只毛毛虫!🐛 v1,1,v2,1,,vN,1v_{1,1}, v_{2,1}, \dots, v_{N,1} 构成了毛毛虫的身体(主干),而每个小链剩下的部分 (vi,2,vi,3,)(v_{i,2}, v_{i,3}, \dots) 就像是挂在身体上的脚。

在这样的树形(毛毛虫也是一种树)结构上求最长链,是一个经典的动态规划问题。但是,我们的“毛毛虫”是动态变化的!插入、删除、修改... 这就需要一种能够支持动态修改的DP,也就是我们常说的“动态DP”。

处理动态序列问题,最强大的武器之一就是平衡二叉搜索树 (BBST),比如Splay或者Treap。考虑到操作的复杂性,使用Treap(特别是无需旋转的FHQ-Treap)会让代码实现更清晰,喵~

嵌套Treap:我的魔法盒子

我们面对的是两个层级的动态序列:

  1. 外层序列: 小链组成的序列。
  2. 内层序列: 每个小链内部的节点序列。

一个非常自然的想法就是用一个数据结构去套另一个。所以,我们可以用一个外层Treap来维护小链的序列,外层Treap的每个节点代表一条小链。而这条小链本身,我们用一个内层Treap来维护它内部的节点序列。就像一个大魔法盒(外层Treap)里装着许多小魔法盒(内层Treap)一样,可爱吧~

内层Treap:维护小链信息

内层Treap相对简单。对于一条小链,我们需要知道哪些信息来帮助外层计算呢?

  • 最大子段和: 这对应着完全在一条小链内部的最长路径。
  • 最大前缀和: 在无向图中,从第一个节点 vi,1v_{i,1} 出发能走到的最长路径(不离开本链),就是这条链的最大前缀和。
  • 第一个节点的值: 这是连接相邻小链的“关节”点。

所以,我们的内层Treap是一个标准的、用于维护序列最大子段和的Treap。每个节点需要维护区间和 sum、最大前缀和 lmax、最大后缀和 rmax、最大子段和 ans。当然,为了支持区间修改,我们还需要加上懒标记 tag

外层Treap:DP状态的定义与合并

这是最核心的部分!外层Treap的每个节点代表一个或多个连续的小链组成的“链段”。我们需要为它设计一套DP状态和合并规则。

假设一个外层Treap节点代表了小链 Ci,,CjC_i, \dots, C_j 组成的链段。我们需要维护以下信息:

  1. chain_head_sum: 链段中所有小链的第一个节点的权值之和,即 k=ijvk,1\sum_{k=i}^{j} v_{k,1}
  2. max_l_path: 从链段的“头”(即 CiC_i 的第一个节点 vi,1v_{i,1})出发,能走到的最长路径的权值和。
  3. max_r_path: 从链段的“尾”(即 CjC_j 的第一个节点 vj,1v_{j,1})出发,能走到的最长路径的权值和。
  4. max_internal_path: 完全包含在该链段内部的最长路径的权值和。

对于只包含一条小链 CkC_k 的“叶子”节点,它的初始DP状态是:

  • chain_head_sum = vk,1v_{k,1} (第一个节点的值)
  • max_l_path = PkP_k (小链 CkC_k 的最大前缀和)
  • max_r_path = PkP_k (因为只有一个连接点,左右对称)
  • max_internal_path = XkX_k (小链 CkC_k 的最大子段和)

这些值 (vk,1,Pk,Xkv_{k,1}, P_k, X_k) 都可以从对应的内层Treap中查询得到。

合并规则 (最重要的魔法!) 假设我们要合并左边的链段 A 和右边的链段 B,得到新的链段 New

  • New.max_internal_path = max(\max(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_sum
  • New.max_l_path = max(\max(A.max_l_path, A.chain_head_sum + B.max_l_path // 穿过整个A,再走B的左路径 ))
  • New.max_r_path = max(\max(B.max_r_path, B.chain_head_sum + A.max_r_path // 穿过整个B,再走A的右路径 ))

有了这些,我们就可以在O(1)时间内合并两个外层Treap节点的信息啦。最终,整个序列的最长链就是外层Treap根节点的 max_internal_path

处理各种操作

  • 删除/插入: 对外层Treap进行 splitmerge 操作,非常标准。
  • 区间修改: split 外层Treap找到对应的小链节点,然后进入其内层Treap,执行标准的区间修改(split -> apply_tag -> merge)。修改后,别忘了更新该外层节点的DP信息。
  • 长度减半: 这是最麻烦的操作。我们需要 split 外层Treap得到对应区间的子树,然后对这个子树里的每一个节点,找到它的内层Treap,split掉后半部分,再更新外层节点的DP信息。我们可以写一个递归函数来遍历这个子树并执行修改。为了优化,可以加一个剪枝:如果一个子树中所有小链的长度都已经是1了,就不用再递归进去了,喵~

总而言之,我们用一个精巧的“Treap套Treap”结构,把复杂的图问题转化为了序列上的动态DP问题,然后用平衡树高效解决!是不是很有趣呢?

代码实现

这是我根据上面的思路,精心重构的一份代码,加了详细的注释,希望能帮助主人理解,喵~

cpp
#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的 splitmerge。外层Treap操作是 O(logN)O(\log N),内层是 O(logL)O(\log L)。所以这些操作的复杂度是 O(logN+logL)O(\log N + \log L)
    • 操作3 (长度减半): 这个操作最特别。我们需要对 [l,r][l, r] 区间的所有小链进行修改。我们先用 O(logN)O(\log N) 的时间 split 出代表这个区间的外层子Treap。然后,我们递归遍历这个子Treap。在最坏情况下,我们需要访问 rl+1r-l+1 个节点,对每个节点的内层Treap做 O(logL)O(\log L) 的 split 操作。所以一次操作3的复杂度是 O((rl+1)logL+logN)O((r-l+1)\log L + \log N)
  • 空间复杂度: 我们需要为每个节点(无论是外层还是内层)都分配空间。总节点数是所有小链长度之和加上小链的数量。设总节点数为 S,空间复杂度是 O(S)O(S)

知识点总结

这道题是数据结构能力的终极考验之一,喵~ 它融合了多种思想:

  1. 问题建模: 将一个看似复杂的图论问题,通过分析其结构(毛毛虫图),转化为一个可以在序列上解决的动态DP问题。这是解题的第一步,也是最重要的一步!
  2. 嵌套数据结构: 使用“Treap套Treap”的结构来管理两个层级的动态序列,是解决此类问题的经典模式。
  3. 动态DP与平衡树: 平衡树(这里是FHQ-Treap)是实现动态DP的有力工具。通过在树的节点上维护DP状态,并定义好状态合并的规则,我们就可以在对数时间内完成查询和大部分修改。
  4. 懒标记思想: 在内层Treap中使用懒标记来支持区间修改,这是平衡树处理区间操作的标准技巧。
  5. 复杂操作的处理: 操作3(长度减半)需要对一个子树进行整体操作,通过递归遍历并修改,展示了平衡树强大的灵活性。同时,通过 needs_halving 标志进行剪枝,是一个很重要的优化技巧。

希望这篇题解能帮到你,主人!如果还有不懂的地方,随时可以再来问我哦,喵~ ❤️