1 条题解
-
0
魔塔问题 C++ 题解
核心思路
本题核心是 树形动态规划(树形DP) 结合 贪心排序,利用树的依赖关系和防御值单调递增特性,通过线性伤害模型优化子树处理顺序,最小化总伤害。
关键观察
- 树的依赖关系:处理子树必须先处理父节点(父节点是连接根与子树的唯一路径,需先变为空地)。
- 线性伤害模型:每个怪物的伤害可表示为 (, 为处理时的防御值),子树总伤害是关于初始防御的线性函数 ( 为常数项, 为系数)。
- 子树排序贪心:子树处理顺序影响总伤害,最优顺序由子树的「宝石总加成 」和「伤害系数 」决定,排序条件为:子树 应在子树 前处理,当且仅当 (最小化交叉项伤害)。
动态规划状态定义
对每个节点 ,维护三个参数:
- :处理 及其子树的总伤害常数项(与初始防御无关);
- :处理 及其子树的总伤害系数(与初始防御相乘);
- : 及其子树的总防御加成(宝石总和)。
状态转移
- 节点本身处理:
- 宝石节点(opt=1):无伤害,,,;
- 怪物节点(opt=2):计算 ,,,。
- 子树处理:
- 收集子节点的 ,按贪心条件排序;
- 计算子树总常数项 、总系数 、总防御加成 和交叉项 (子树顺序带来的额外伤害);
- 合并得到节点 的参数:
- 最终答案:根节点(1号点)的总伤害为 (初始防御为 )。
C++ 代码实现
#include <iostream> #include <vector> #include <stack> #include <queue> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 3e5 + 5; vector<vector<int>> adj(MAXN); // 邻接表 vector<vector<int>> children(MAXN); // 每个节点的子节点列表(有根树) int parent[MAXN]; // 父节点标记 int opt[MAXN]; // 节点类型(1=宝石,2=怪物) int v[MAXN]; // 宝石的防御加成 int h[MAXN], a[MAXN], d[MAXN]; // 怪物的属性(生命、攻击、防御) // DP状态:C=常数项,K=系数,s=总防御加成 struct NodeDP { ll C, K, s; NodeDP(ll c = 0, ll k = 0, ll s = 0) : C(c), K(k), s(s) {} }; NodeDP dp[MAXN]; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int c; cin >> c; int n, A, D; cin >> n >> A >> D; // 读取边,构建邻接表 for (int i = 0; i < n-1; ++i) { int x, y; cin >> x >> y; adj[x].push_back(y); adj[y].push_back(x); } // 读取2~n号节点的属性 for (int u = 2; u <= n; ++u) { cin >> opt[u]; if (opt[u] == 1) { cin >> v[u]; } else { cin >> h[u] >> a[u] >> d[u]; } } // BFS构建有根树(以1为根),确定父节点和子节点 fill(parent, parent + n + 1, 0); queue<int> q; q.push(1); parent[1] = -1; // 根节点无父节点 while (!q.empty()) { int u = q.front(); q.pop(); for (int v : adj[u]) { if (parent[v] == 0 && v != parent[u]) { parent[v] = u; children[u].push_back(v); q.push(v); } } } // 迭代式后序遍历,计算DP stack<pair<int, bool>> st; st.push({1, false}); while (!st.empty()) { auto [u, visited] = st.top(); st.pop(); if (!visited) { // 第一次弹出:标记为已访问,逆序压入子节点(保证后序遍历顺序) st.push({u, true}); for (int i = children[u].size() - 1; i >= 0; --i) { st.push({children[u][i], false}); } } else { // 第二次弹出:处理当前节点 ll C0, K0, s0; if (u == 1) { // 根节点是空地,无属性 C0 = 0; K0 = 0; s0 = 0; } else if (opt[u] == 1) { // 宝石节点 C0 = 0; K0 = 0; s0 = v[u]; } else { // 怪物节点:计算K_m和初始C0、K0 int attack_power = A - d[u]; ll K_m = (h[u] + attack_power - 1) / attack_power - 1; // ceil(h/attack_power) - 1 C0 = K_m * a[u]; K0 = -K_m; s0 = 0; } // 收集所有子节点的DP状态 vector<NodeDP> child_dps; for (int v : children[u]) { child_dps.push_back(dp[v]); } // 按贪心条件排序子节点:a在b前 iff b.K * a.s <= a.K * b.s sort(child_dps.begin(), child_dps.end(), [](const NodeDP& x, const NodeDP& y) { return (ll)y.K * x.s <= (ll)x.K * y.s; }); // 计算子树的sum_C、sum_K、prefix_s、cross ll sum_C = 0, sum_K = 0, prefix_s = 0, cross = 0; for (const auto& nd : child_dps) { cross += nd.K * prefix_s; sum_C += nd.C; sum_K += nd.K; prefix_s += nd.s; } // 合并当前节点和子树的DP状态 dp[u].C = C0 + sum_C + cross; dp[u].K = K0 + sum_K; dp[u].s = s0 + prefix_s; } } // 根节点1的总伤害 = C1 + K1 * 初始防御D ll total_damage = dp[1].C + dp[1].K * D; cout << total_damage << endl; return 0; }复杂度分析
- 时间复杂度:,其中 为节点最大度数。树的总度数为 ,每个节点的子节点排序时间为 ,整体排序总时间为 ,其余操作均为线性。
- 空间复杂度:,用于存储邻接表、节点属性、DP状态和栈/队列,可应对 的大数据规模。
关键优化点
- 迭代式后序遍历:避免递归栈溢出,适配大规模数据;
- 贪心排序:通过子树排序最小化交叉项伤害,是最优解的核心;
- 线性伤害模型:将子树伤害抽象为线性函数,简化状态合并逻辑;
- 高效输入输出:关闭同步流,适配大规模输入。
- 1
信息
- ID
- 5328
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者