1 条题解
-
0
题目分析
问题重述
我们有两个程序员在玩一个博弈游戏:
- 小 有一个固定的 Sleep++ 程序(树结构)
- 小 可以设计任意程序
- 双方同时调用指定函数,程序运行时间取决于输入值
- 目标是找到小 需要的最少函数个数,使得游戏公平(双方都没有必胜策略)
关键观察
- 程序结构:Sleep++ 程序构成一棵有根树,每个节点代表一个函数
- 运行时间计算:
- :固定等待时间
- :可输入任意正整数,影响等待时间
- 博弈性质:双方在不知道对方当前输入的情况下做决策
解法思路
方法:树形DP + 博弈分析
核心思想
对于小 程序中的每个函数 ,我们需要计算小 需要的最少函数个数来构造一个公平游戏。
关键定理:游戏公平当且仅当两个程序的运行时间函数在某种意义下"匹配"。
步骤1:计算函数的运行时间特征
定义 为以 为根的子树运行时间的特征:
- 如果 :
- 如果 :,其中 是输入值
实际上,我们需要更精细的特征来描述博弈关系。
步骤2:博弈分析
考虑双方程序的运行时间函数 和 ,其中 是输入值。游戏公平要求:
$$\forall x, \exists y: f(x) = g(y) \quad \text{且} \quad \forall y, \exists x: f(x) = g(y) $$这意味着两个函数的值域必须完全相同。
步骤3:最小程序构造
通过分析小 程序的运行时间特征,我们可以确定小 需要的最少函数个数。
关键结论:所需函数个数等于小 程序中"关键决策点"的数量。
代码实现
#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
- :运行时间等于输入值
- 需要1个函数来匹配
-
内部节点 ():
- :运行时间固定,取子节点需求的最大值
- :运行时间可缩放,需要所有子节点需求的和
博弈论证明
定理:上述DP值确实是最小所需函数个数。
证明思路:
- 每个 的节点引入一个自由度,需要额外的函数来覆盖所有可能的时间值
- 的节点时间固定,只需要匹配最坏情况
- 通过归纳法可以证明构造的最优性
复杂度分析
- 预处理: DFS
- 单次查询:
- 单次修改:(树链剖分)
- 总复杂度:
算法总结
- 树形DP:自底向上计算每个子树所需的最小函数个数
- 博弈分析:通过运行时间函数的匹配要求确定最优构造
- 动态维护:使用树链剖分支持高效的修改操作
- 分类讨论:根据节点种类采用不同的合并策略
关键技巧
- 特征提取:将复杂的运行时间抽象为简单的数值特征
- 最优子结构:子树的最优解可以组合成父节点的最优解
- 树链剖分:高效支持树上路径查询和更新
- 博弈简化:将复杂的博弈问题转化为组合优化问题
这道题综合考察了树形DP、博弈论和数据结构,是一道非常具有挑战性的题目。
- 1
信息
- ID
- 5582
- 时间
- 10000ms
- 内存
- 1024MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者