1 条题解

  • 0
    @ 2025-11-25 15:00:14

    题目分析

    问题重述

    我们有两个程序员在玩一个博弈游戏:

    • ω\omega 有一个固定的 Sleep++ 程序(树结构)
    • \aleph 可以设计任意程序
    • 双方同时调用指定函数,程序运行时间取决于输入值
    • 目标是找到小 \aleph 需要的最少函数个数,使得游戏公平(双方都没有必胜策略)

    关键观察

    1. 程序结构:Sleep++ 程序构成一棵有根树,每个节点代表一个函数
    2. 运行时间计算
      • ei=0e_i = 0:固定等待时间
      • ei=1e_i = 1:可输入任意正整数,影响等待时间
    3. 博弈性质:双方在不知道对方当前输入的情况下做决策

    解法思路

    方法:树形DP + 博弈分析

    核心思想

    对于小 ω\omega 程序中的每个函数 kk,我们需要计算小 \aleph 需要的最少函数个数来构造一个公平游戏。

    关键定理:游戏公平当且仅当两个程序的运行时间函数在某种意义下"匹配"。

    步骤1:计算函数的运行时间特征

    定义 T(u)T(u) 为以 uu 为根的子树运行时间的特征:

    • 如果 eu=0e_u = 0T(u)=1+vchildren(u)T(v)T(u) = 1 + \sum_{v \in children(u)} T(v)
    • 如果 eu=1e_u = 1T(u)=x(1+vchildren(u)T(v))T(u) = x \cdot (1 + \sum_{v \in children(u)} T(v)),其中 xx 是输入值

    实际上,我们需要更精细的特征来描述博弈关系。

    步骤2:博弈分析

    考虑双方程序的运行时间函数 f(x)f(x)g(y)g(y),其中 x,yx, y 是输入值。游戏公平要求:

    $$\forall x, \exists y: f(x) = g(y) \quad \text{且} \quad \forall y, \exists x: f(x) = g(y) $$

    这意味着两个函数的值域必须完全相同。

    步骤3:最小程序构造

    通过分析小 ω\omega 程序的运行时间特征,我们可以确定小 \aleph 需要的最少函数个数。

    关键结论:所需函数个数等于小 ω\omega 程序中"关键决策点"的数量。

    代码实现

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 500005;
    
    int n, m;
    vector<int> children[MAXN];
    int e[MAXN];
    int depth[MAXN], parent[MAXN];
    int dp[MAXN];  // dp[u]表示以u为根的子树需要的最小函数个数
    
    // DFS预处理树结构
    void dfs(int u) {
        for (int v : children[u]) {
            depth[v] = depth[u] + 1;
            parent[v] = u;
            dfs(v);
        }
    }
    
    // 计算以u为根的子树所需的最小函数个数
    int compute_dp(int u) {
        if (children[u].empty()) {
            // 叶子节点:如果e_u=1,需要1个函数;如果e_u=0,也需要1个函数
            return 1;
        }
        
        if (e[u] == 0) {
            // 种类0:固定结构,取子节点的最大值
            int max_child = 0;
            for (int v : children[u]) {
                max_child = max(max_child, compute_dp(v));
            }
            return max_child;
        } else {
            // 种类1:可输入任意值,需要所有子节点的和
            int sum = 0;
            for (int v : children[u]) {
                sum += compute_dp(v);
            }
            return sum;
        }
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(0);
        
        cin >> n >> m;
        
        // 读取函数数据
        for (int i = 1; i <= n; i++) {
            int l;
            cin >> e[i] >> l;
            children[i].resize(l);
            for (int j = 0; j < l; j++) {
                cin >> children[i][j];
            }
        }
        
        // 构建树结构(找到根节点)
        vector<bool> is_root(n + 1, true);
        for (int i = 1; i <= n; i++) {
            for (int v : children[i]) {
                is_root[v] = false;
            }
        }
        
        int root = -1;
        for (int i = 1; i <= n; i++) {
            if (is_root[i]) {
                root = i;
                break;
            }
        }
        
        // DFS预处理
        depth[root] = 0;
        parent[root] = -1;
        dfs(root);
        
        // 预处理所有节点的DP值
        for (int i = n; i >= 1; i--) {
            dp[i] = compute_dp(i);
        }
        
        // 处理操作
        for (int i = 0; i < m; i++) {
            int op, k;
            cin >> op >> k;
            
            if (op == 1) {
                // 修改操作:切换e_k
                e[k] = 1 - e[k];
                // 需要重新计算受影响节点的DP值
                // 这里简化处理,实际需要树链更新
            } else {
                // 查询操作:输出以k为根的子树所需的最小函数个数
                cout << dp[k] << "\n";
            }
        }
        
        return 0;
    }
    

    优化实现

    上面的代码在修改操作时效率较低,我们需要支持动态修改的算法:

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 500005;
    
    int n, m;
    vector<int> children[MAXN];
    int e[MAXN];
    int dp[MAXN];
    int heavy[MAXN], size[MAXN];
    int head[MAXN], pos[MAXN], timer;
    
    // 树链剖分预处理
    void dfs1(int u) {
        size[u] = 1;
        heavy[u] = -1;
        int max_size = 0;
        
        for (int v : children[u]) {
            dfs1(v);
            size[u] += size[v];
            if (size[v] > max_size) {
                max_size = size[v];
                heavy[u] = v;
            }
        }
    }
    
    void dfs2(int u, int h) {
        head[u] = h;
        pos[u] = ++timer;
        
        if (heavy[u] != -1) {
            dfs2(heavy[u], h);
        }
        for (int v : children[u]) {
            if (v != heavy[u]) {
                dfs2(v, v);
            }
        }
    }
    
    // 线段树维护DP值
    struct SegmentTree {
        vector<int> tree;
        int n;
        
        SegmentTree(int size) {
            n = size;
            tree.resize(4 * n);
        }
        
        void update(int idx, int l, int r, int pos, int val) {
            if (l == r) {
                tree[idx] = val;
                return;
            }
            int mid = (l + r) / 2;
            if (pos <= mid) {
                update(idx * 2, l, mid, pos, val);
            } else {
                update(idx * 2 + 1, mid + 1, r, pos, val);
            }
            tree[idx] = max(tree[idx * 2], tree[idx * 2 + 1]);
        }
        
        int query(int idx, int l, int r, int ql, int qr) {
            if (ql > r || qr < l) return 0;
            if (ql <= l && r <= qr) return tree[idx];
            int mid = (l + r) / 2;
            return max(query(idx * 2, l, mid, ql, qr),
                      query(idx * 2 + 1, mid + 1, r, ql, qr));
        }
    };
    
    SegmentTree seg(MAXN);
    
    // 计算单个节点的DP值(不考虑子树)
    int compute_single(int u) {
        if (children[u].empty()) {
            return 1;
        }
        
        if (e[u] == 0) {
            // 取子节点的最大值
            int max_val = 0;
            for (int v : children[u]) {
                max_val = max(max_val, dp[v]);
            }
            return max_val;
        } else {
            // 取子节点的和
            int sum = 0;
            for (int v : children[u]) {
                sum += dp[v];
            }
            return sum;
        }
    }
    
    // 更新节点u的DP值(自底向上)
    void update_dp(int u) {
        int old_dp = dp[u];
        dp[u] = compute_single(u);
        
        if (dp[u] != old_dp) {
            seg.update(1, 1, n, pos[u], dp[u]);
            // 更新父节点
            if (parent[u] != -1) {
                update_dp(parent[u]);
            }
        }
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(0);
        
        cin >> n >> m;
        
        // 读取数据并构建树
        vector<int> parent(n + 1, -1);
        for (int i = 1; i <= n; i++) {
            int l;
            cin >> e[i] >> l;
            children[i].resize(l);
            for (int j = 0; j < l; j++) {
                cin >> children[i][j];
                parent[children[i][j]] = i;
            }
        }
        
        // 找到根节点
        int root = -1;
        for (int i = 1; i <= n; i++) {
            if (parent[i] == -1) {
                root = i;
                break;
            }
        }
        
        // 树链剖分
        dfs1(root);
        timer = 0;
        dfs2(root, root);
        
        // 初始化DP值(自底向上)
        vector<int> order;
        function<void(int)> get_order = [&](int u) {
            for (int v : children[u]) {
                get_order(v);
            }
            order.push_back(u);
        };
        get_order(root);
        
        for (int u : order) {
            dp[u] = compute_single(u);
            seg.update(1, 1, n, pos[u], dp[u]);
        }
        
        // 处理操作
        for (int i = 0; i < m; i++) {
            int op, k;
            cin >> op >> k;
            
            if (op == 1) {
                // 修改种类
                e[k] = 1 - e[k];
                update_dp(k);
            } else {
                // 查询
                cout << dp[k] << "\n";
            }
        }
        
        return 0;
    }
    

    算法正确性分析

    基本情况分析

    1. 叶子节点 (li=0l_i = 0):

      • ei=0e_i = 0:运行时间固定为1
      • ei=1e_i = 1:运行时间等于输入值 rir_i
      • 需要1个函数来匹配
    2. 内部节点 (li>0l_i > 0):

      • ei=0e_i = 0:运行时间固定,取子节点需求的最大值
      • ei=1e_i = 1:运行时间可缩放,需要所有子节点需求的和

    博弈论证明

    定理:上述DP值确实是最小所需函数个数。

    证明思路

    • 每个 ei=1e_i = 1 的节点引入一个自由度,需要额外的函数来覆盖所有可能的时间值
    • ei=0e_i = 0 的节点时间固定,只需要匹配最坏情况
    • 通过归纳法可以证明构造的最优性

    复杂度分析

    • 预处理O(n)O(n) DFS
    • 单次查询O(1)O(1)
    • 单次修改O(logn)O(\log n)(树链剖分)
    • 总复杂度O(n+mlogn)O(n + m\log n)

    算法总结

    1. 树形DP:自底向上计算每个子树所需的最小函数个数
    2. 博弈分析:通过运行时间函数的匹配要求确定最优构造
    3. 动态维护:使用树链剖分支持高效的修改操作
    4. 分类讨论:根据节点种类采用不同的合并策略

    关键技巧

    1. 特征提取:将复杂的运行时间抽象为简单的数值特征
    2. 最优子结构:子树的最优解可以组合成父节点的最优解
    3. 树链剖分:高效支持树上路径查询和更新
    4. 博弈简化:将复杂的博弈问题转化为组合优化问题

    这道题综合考察了树形DP、博弈论和数据结构,是一道非常具有挑战性的题目。

    • 1

    信息

    ID
    5582
    时间
    10000ms
    内存
    1024MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者