一遍深度优先遍历+一遍树形dp。
class Solution {
public:
vector<vector<int>> g;
vector<int> cnt; // 用来记录每个节点要经过多少次。因为虽然节点价格会变,但是节点经过的次数不会变
vector<int> price;
int minimumTotalPrice(int n, vector<vector<int>>& edges, vector<int>& _price, vector<vector<int>>& trips) {
g = vector<vector<int>>(n); // 邻接链表
price = _price;
cnt = vector<int>(n);
for(auto &e : edges){ // 记录边
g[e[0]].push_back(e[1]);
g[e[1]].push_back(e[0]);
}
for(auto &e : trips){
dfs(e[0], -1, e[1]); // 寻找从e0到e1的路径,并且记录经过每个节点的次数
}
auto [a, b] = dp(0, -1); // 以0为根节点,求解问题
return min(a, b);
}
bool dfs(int i, int fa, int k){ // 当前节点为i,目标节点为k的dfs;如果从当前节点可以到k,那么在cnt中记录路径,并返回true(也就是子树中包含k)
++cnt[i]; // 尝试将当前节点加入路径
if(i == k)return true;
for(int &v : g[i]){ // 遍历i节点的子节点
if(v != fa){ // 不重复遍历父节点
if(dfs(v, i, k))return true; // 从v节点可以到k,那么从本节点也能到k,尝试成功,返回true
}
}
--cnt[i]; // 尝试失败,把节点退出路径
return false;
}
// 要解决的问题是,根节点在价格保持时的最小路径数价格,和价格减半时的最小路径树价格。
// dp具有最优子结构,也就是可以通过求解子树的以上两个子问题,来得到根节点的解
// 以下也是个dfs,当前节点有两个状态,即当前节点价格保持,所能得到的最小值,记为a,和当前节点价格减半所能得到的最小值,记为b。
// 当选择状态a时,子树的所有节点可以保持价格不变,也可以价格减半,minprice = a + min(child_a, child_b)
// 当选择状态b时,子树的所有节点只能保持价格不变,minprice = b + child_a
// 以上状态转移是否成立,可以从初始状态开始推算,初始状态显然是叶子节点时,只需要计算当前节点的a和b即可,此时子问题成立。
// 从叶子节点往上转移,是始终可以保持子问题的结构的,因为父节点的最小值就是子树的最小值加上当前值。
pair<int, int> dp(int i, int fa){
int a = cnt[i] * price[i]; // 当前节点保持原价
int b = a / 2; // 当前节点价格减半
for(int &v : g[i]){
if(v != fa){
auto [child_a, child_b] = dp(v, i); // 得到下一个节点减半和不减半的最小价格
a += min(child_a, child_b); // 当选择状态a时,子树的所有节点可以保持价格不变,也可以价格减半,minprice = a + min(child_a, child_b)
b += child_a; // 当选择状态b时,子树的所有节点只能保持价格不变,minprice = b + child_a
}
}
return {a, b};
}
};