喵哈喽~ 主人!今天我们来解决一个关于守卫黄金的有趣问题喵!诺丁汉郡长需要我们的帮助来对抗罗宾汉,保护他的财宝。让我们一起来看看怎么用聪明的策略帮他保住最多的黄金吧!
F. Sheriff's Defense
题目大意
简单来说,我们有一棵由 n 个营地和 n-1 条小径组成的树,每个营地 i 最初有 a[i] 的黄金,喵。
郡长可以选择加固 (strengthen) 任意数量的营地。加固一个营地 u 会产生一个效果:u 的所有相邻营地 v 都会失去 c 的黄金。加固操作本身不会改变 u 自己的黄金数量。
在罗宾汉攻击之后,只有被加固的营地能够幸存下来,其他营地都会被摧毁。我们的任务是,选择一个最优的加固方案,使得所有幸存营地(也就是被加固的营地)的黄金总和最大。一个营地最终的黄金数量是它初始的黄金数量,减去因其邻居被加固而损失的部分。
举个例子,如果营地 i 被加固了,它的邻居 j 和 k 也被加固了,那么营地 i 幸存下来,它最终的黄金是 a[i] - 2*c。如果只有 j 被加固,那么 i 最终的黄金就是 a[i] - c。
我们的目标就是求这个最大的黄金总和,喵~
题解方法
看到这种在树上做决策、求最优解的问题,喵酱的DNA就动了,这不就是典型的树形动态规划 (Tree DP) 嘛!
我们可以把营地关系看作一棵树。对于树上的每一个节点(营地),我们都有两种选择:
- 加固这个营地。
- 不加固这个营地。
这个决定会影响到它的子节点和父节点。因此,我们可以用一次深度优先搜索(DFS)来遍历整棵树,并在回溯的过程中计算出以每个节点为根的子树能获得的最大黄金。
为了做到这一点,我们需要为每个节点 u 定义DP状态:
dp[u][0]:表示在以u为根的子树中,当营地u不加固时,能获得的最大黄金总和。dp[u][1]:表示在以u为根的子树中,当营地u加固时,能获得的最大黄金总和。
通过从叶子节点向上计算,我们最终就能得到根节点的 dp 值,从而得到整个问题的答案,喵!
题解
首先,我们来仔细分析一下最终的黄金总和是怎么计算的。
假设我们选择了一个营地集合 S 来加固。
- 幸存营地的初始黄金总和是
sum(a[i])foriinS。 - 现在考虑损失。对于一条连接营地
u和v的小径,如果u和v都被加固了,那么u会因为v的加固而损失c黄金,v也会因为u的加固而损失c黄金。这条边导致的总损失是2c。 - 如果
u和v中只有一个被加固,那么这条边不会产生损失(因为未被加固的一方不会计入最终总和,而被加固的一方,它的邻居未被加固,所以也不会有损失)。
所以,我们的目标函数是最大化: Σ (a[i] for i in S) - 2 * c * (S中两个端点都被连接的边的数量)
现在,我们来定义DP的状态转移方程:
我们任意选择一个节点(比如节点1)作为根节点,然后进行DFS。对于当前节点 u 和它的父节点 p:
1. 计算 dp[u][0] (不加固 u)
如果 u 不被加固,它对最终的黄金总和贡献为 0。我们只需要考虑它的每个子节点 v 的子树能贡献的最大价值。对于每个子节点 v,我们可以选择加固它(贡献 dp[v][1])或者不加固它(贡献 dp[v][0])。我们当然是选择其中较大的那个啦,喵! 所以,状态转移方程是: dp[u][0] = Σ max(dp[v][0], dp[v][1]) (对所有 u 的子节点 v 求和)
2. 计算 dp[u][1] (加固 u)
如果 u 被加固,它首先会贡献自己的初始黄金 a[u]。然后,我们再考虑它的每个子节点 v 的贡献。
- 如果选择不加固
v,那么v的子树贡献dp[v][0]。此时边(u, v)的两个端点只有一个被加固,所以没有2c的惩罚。 - 如果选择加固
v,那么v的子树贡献dp[v][1]。但是,因为u和v都被加固了,我们需要支付2c的代价。所以来自v子树的净贡献是dp[v][1] - 2*c。
对于每个子节点 v,我们在这两种选择中取一个最优的,也就是 max(dp[v][0], dp[v][1] - 2*c)。 所以,状态转移方程是: dp[u][1] = a[u] + Σ max(dp[v][0], dp[v][1] - 2*c) (对所有 u 的子节点 v 求和)
3. 边界条件和最终答案
- 对于叶子节点
u,它没有子节点。所以dp[u][0] = 0,dp[u][1] = a[u]。 - 在DFS结束后,我们回到根节点(比如节点1)。最终的答案就是
max(dp[1][0], dp[1][1]),代表在根节点加固与不加固两种情况下的最优解。如果营地为空,答案当然是0啦。
下面是实现这个思路的代码喵~
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include <array>
void solve() {
int n;
long long c;
std::cin >> n >> c;
std::vector<long long> a(n + 1);
for (int i = 1; i <= n; ++i) {
std::cin >> a[i];
}
std::vector<std::vector<int>> adj(n + 1);
for (int i = 0; i < n - 1; ++i) {
int u, v;
std::cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 如果没有营地,就没有黄金喵
if (n == 0) {
std::cout << 0 << "\n";
return;
}
// dp[u][0]: u不加固,子树最大黄金
// dp[u][1]: u加固,子树最大黄金
std::vector<std::array<long long, 2>> dp(n + 1);
// 使用 std::function 定义一个递归的 lambda 函数
std::function<void(int, int)> dfs =
[&](int u, int p) {
// 初始化当前节点的DP值
dp[u][0] = 0;
dp[u][1] = a[u];
// 遍历所有邻居
for (int v : adj[u]) {
if (v == p) continue; // 不走回头路
// 先递归计算子节点的DP值
dfs(v, u);
// 根据状态转移方程更新当前节点的DP值
// 如果 u 不加固
dp[u][0] += std::max(dp[v][0], dp[v][1]);
// 如果 u 加固
dp[u][1] += std::max(dp[v][0], dp[v][1] - 2 * c);
}
};
// 从节点1开始DFS,父节点设为0
dfs(1, 0);
// 最终答案是根节点两种选择的较大值
std::cout << std::max(dp[1][0], dp[1][1]) << "\n";
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}知识点介绍
树形动态规划 (Tree DP)
树形DP是一种在树状结构上进行动态规划的算法思想,喵。它非常适合解决那些需要为树上每个节点做出决策,并求全局最优解的问题。
核心思想:
- 自底向上计算:通常通过深度优先搜索(DFS)实现。当DFS访问到一个节点
u时,它会先递归地访问完u的所有子节点。在从子节点的递归调用返回后,子节点的最优解(即DP值)就已经计算出来了。 - 状态合并:节点
u利用它所有子节点v的DP值,来计算出自身的DP值。这个过程就是状态转移。 - 状态定义:
dp[u][state]的定义是关键。state需要包含足够的信息,以便父节点可以根据u的状态来计算自己的状态。在这个问题里,state就是0(不加固) 和1(加固),因为u是否加固会影响到它和父节点之间的边是否产生2c的惩罚。
树形DP的应用非常广泛,比如求解树的最大独立集、树的直径、树的重心等问题。只要问题满足最优子结构和无后效性(即子树的决策不会影响到子树之外的其他不相关子树),就可以考虑使用树形DP来解决,喵!
好啦,这样诺丁汉郡长就能保住最多的黄金啦,喵~ 主人学会了吗?如果还有其他问题,随时可以再来问我哦!