1 条题解

  • 0
    @ 2025-11-18 11:16:50

    题解:Restorani

    题目分析

    问题核心

    给定一棵有 (n) 个节点的树(景点),(m) 家餐厅和 (m) 家甜品店(位置互不重复)。Malnar 先生从节点 1 出发,需按“餐厅→甜品店→餐厅→甜品店……”的顺序遍历所有餐厅和甜品店(各遍历一次),最后返回节点 1,求最短总用时及对应的遍历顺序。

    关键约束与观察

    1. 路径特性:树中两点间路径唯一,路径长度为两点间边数(每边用时 1 分钟)。
    2. 遍历规则
      • 奇数位置必须是餐厅(1~m 的排列),偶数位置必须是甜品店(1~m 的排列)。
      • 总用时为路径长度之和(含最后返回节点 1 的路程)。
    3. 最短路径核心:树的遍历中,重复经过的边是耗时的主要来源。最优策略需最小化重复路径,即利用“子树内餐厅与甜品店配对”减少往返次数。

    数据范围挑战

    • (n, m \leq 3 \times 10^5),需设计 (O(n + m)) 或 (O(n \log n)) 复杂度的树形 DP 算法,避免暴力枚举。

    算法设计

    核心思路

    1. 问题转化:将餐厅和甜品店视为两类“需求点”,树的每个子树需处理内部的需求点配对,未配对的需求点需向上传递到父节点,与其他子树的需求点配对。
    2. 配对与路径优化
      • 子树内若同时有餐厅和甜品店,优先配对,配对后无需往返父节点(仅需遍历子树一次)。
      • 未配对的需求点(仅餐厅或仅甜品店)需传递到父节点,传递过程会产生往返路径(耗时为子树深度×2)。
    3. 数据结构辅助:用栈(stek 结构体)维护每个节点的未配对餐厅和甜品店,支持高效合并与配对操作;用双向链表维护最终的遍历顺序。

    具体步骤

    1. 输入处理与初始化

    • 读取树的结构、餐厅位置((a_i))、甜品店位置((b_i))。
    • 对每个节点 (x),维护 zivi[x][0](该节点的餐厅编号列表)、zivi[x][1](该节点的甜品店编号列表)。
    • 初始化计数数组 cnt[x][0](子树 (x) 内未配对的餐厅数)、cnt[x][1](子树 (x) 内未配对的甜品店数)。

    2. 树形 DP(DFS 遍历)

    按后序遍历处理每个子树,核心逻辑:

    • 子树合并:遍历当前节点的所有子节点,递归处理子树后,合并子树的未配对餐厅栈(A[y])和甜品店栈(B[y])到当前节点的栈(A[x]B[x])。
    • 路径耗时计算:子树 (y) 中未配对的餐厅数与甜品店数的差值决定了往返路径耗时。若差值为 (d),则需往返父节点 (d) 次,耗时为 (2 \times \max(1, |d|))(max(1, ...) 确保至少往返一次,除非子树无需求点)。
    • 需求点配对:当前节点的栈中若同时有餐厅和甜品店,持续配对(餐厅→甜品店),配对后的序列通过双向链表链接,减少重复路径。
    • 未配对需求点传递:若配对后仍有未配对的餐厅或甜品店,保留在当前节点的栈中,传递到父节点继续配对。

    3. 遍历顺序构建

    • 用双向链表(prvnxt 数组)维护配对后的序列,kraj[i] 记录编号 (i) 的需求点在链表中的终点。
    • 配对时,将餐厅的终点链接到甜品店的起点,形成“餐厅→甜品店”的连续路径。
    • 最终链表的起点为 ret.X,按 nxt 数组遍历即可得到完整的遍历顺序。

    4. 结果输出

    • 总耗时 sol 为所有子树往返耗时之和。
    • 遍历双向链表,将餐厅编号(0~m-1)转化为 1~m,甜品店编号(m~2m-1)转化为 1~m,输出遍历顺序。

    代码关键模块解析

    1. 栈结构(stek 结构体)

    用于维护未配对的需求点,支持高效合并与弹出操作:

    struct stek {
        int dno, vrh; // dno:栈底,vrh:栈顶
        void push(int x) { /* 压栈,维护栈底和栈顶指针 */ }
        void pop() { /* 弹栈 */ }
        void ocisti() { /* 清空栈 */ }
        void merge(stek &tko) { /* 合并另一个栈到当前栈 */ }
    };
    
    • 合并操作(merge):将另一个栈的所有元素链接到当前栈的栈顶,确保未配对需求点的有序性。

    用于构建遍历顺序,记录需求点的前后关联:

    inline void link(int A, int B) {
        nxt[A] = B; // A 的下一个需求点是 B
        prv[B] = A; // B 的上一个需求点是 A
    }
    
    • 配对时,link(kraj[a], b) 表示餐厅 (a) 的终点链接到甜品店 (b) 的起点,形成连续路径。

    3. 配对逻辑(ubaci_novi 函数)

    将未配对的需求点(或子树传递的配对序列)加入当前节点的栈,参与配对:

    void ubaci_novi(stek &A, stek &B, pii novi) {
        if (A.vrh != -1) {
            int t = A.vrh;
            A.pop();
            link(novi.Y, t); // 链接新序列的终点到栈顶餐厅
            kraj[novi.X] = kraj[t]; // 更新新序列的终点
            A.push(novi.X);
        } else {
            link(kraj[B.vrh], novi.X); // 链接栈顶甜品店的终点到新序列
            kraj[B.vrh] = novi.Y; // 更新栈顶甜品店的终点
        }
    }
    
    • 处理子树传递的配对序列(smece),将其与当前节点的未配对需求点合并,优化路径。

    4. 树形 DP 核心(dfs 函数)

    pii dfs(int x, int lst) {
        // 初始化当前节点的餐厅和甜品店栈
        for (int t : zivi[x][0]) A[x].push(t);
        for (int t : zivi[x][1]) B[x].push(t);
    
        pii smece = {-1, -1}; // 子树传递的配对序列(起点,终点)
    
        for (int y : v[x]) {
            if (y == lst) continue;
            pii ret = dfs(y, x); // 递归处理子树
    
            // 计算子树往返耗时
            if (cnt[y][0] + cnt[y][1])
                sol += 2LL * max(1, abs(cnt[y][0] - cnt[y][1]));
    
            // 合并子树的未配对计数
            cnt[x][0] += cnt[y][0];
            cnt[x][1] += cnt[y][1];
    
            // 处理子树传递的配对序列
            if (ret.X != -1) {
                if (smece.X == -1) smece = ret;
                else link(smece.Y, ret.X), smece.Y = ret.Y;
            } else {
                // 合并子树的未配对栈
                if (A[y].vrh != -1) A[x].merge(A[y]);
                else B[x].merge(B[y]);
            }
        }
    
        // 将传递的配对序列加入当前节点的栈
        if (smece.X != -1) ubaci_novi(A[x], B[x], smece);
    
        // 持续配对当前节点的餐厅和甜品店
        while (A[x].vrh != -1 && B[x].vrh != -1) {
            int a = A[x].vrh; A[x].pop();
            int b = B[x].vrh; B[x].pop();
            link(kraj[a], b); // 餐厅a → 甜品店b
            pii novi = {a, kraj[b]};
            if (A[x].vrh == -1 && B[x].vrh == -1) return novi; // 无未配对,返回当前序列
            else ubaci_novi(A[x], B[x], novi); // 继续合并配对
        }
    
        // 更新当前节点的未配对计数
        cnt[x][0] = (A[x].vrh != -1);
        cnt[x][1] = (B[x].vrh != -1);
        return {-1, -1}; // 有未配对,不传递序列
    }
    
    • 后序遍历确保子树优先处理,合并未配对需求点并计算耗时。
    • 配对过程通过双向链表链接,形成最优遍历顺序,最小化重复路径。

    时间复杂度与空间复杂度

    • 时间复杂度:(O(n + m))。树形 DFS 遍历所有节点 (O(n)),每个需求点入栈、出栈、配对各一次 (O(m)),栈合并和链表操作均为线性时间。
    • 空间复杂度:(O(n + m))。存储树的邻接表、栈结构、计数数组、链表指针,均为线性空间。

    关键结论

    1. 子树配对优化:优先在子树内配对餐厅和甜品店,减少往返父节点的路径,是缩短总耗时的核心。
    2. 数据结构选型:栈用于高效维护未配对需求点,双向链表用于构建连续遍历顺序,确保算法在大规模数据下的效率。
    3. 耗时计算逻辑:子树未配对需求点的差值决定往返次数,耗时为 (2 \times \max(1, |d|)),确保覆盖所有必要的往返路径。
    • 1

    信息

    ID
    5433
    时间
    2000ms
    内存
    512MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者