树中距离之和:
https://leetcode-cn.com/problems/sum-of-distances-in-tree/
详情题解请看官方题解,而我只不过是将代码与之对应。
说来读题读懂都是一个问题,实际上还是理解能力的限制在作天花板。
树形动态规划其实还是很容易看出的。相互的递推关系也不过是交换的问题,不过最开始我想到的交换,最终写代码的时候却没有加上。还是工具的运用没有到如火纯情的地步。
这是我花费了一个早晨看懂的代码。
总之是这样,其实思路早就明确了,但是总是代码还是看不懂。所以算法越精妙的代码,可读性真的就是越差。这是一个无法回避的事实。那么我有感而发。
越是聪明的头脑,可读性也同样是越差的。
所以,敬佩那些科学伟人们。同样也为他们一生的孤独而感到遗憾。
其实最开始还有一个疑问,我不明白时间复杂度是怎么到了O(n)。因为我看到他遍历了数组,vector< int >后来发现,这是一个散列表。题目所给也是一个散列表。那么其实就可以想一下如何去用树的存储结构去做这个题目了。
作者想的很全面吗?是否还有更加可以优化的地方呢?
我本来觉得可以把所谓的散列变成有向图的形式,但是实际上dfs2中的交换次序使得我的想法变得不可行。考虑到子节点和父节点的交换,其实其相互的交换只是变了一个散列值,而若后来进行修改的话,涉及到的操作可能更加复杂,真的可能不如想办法去continue,而不是去修改散列。保证数据正确的情况下,才能去谈代码算法。
class Solution {
public:
vector<int> ans, sz, dp;
vector<vector<int>> graph;
void dfs(int u, int f) {
sz[u] = 1;//最底层的节点是1
dp[u] = 0;//最底层的节点距离是0
for (auto& v: graph[u]) {
//在每个根的子节点里面寻找距离
if (v == f) {
//碰到计算过的就跳,即只计算树的根和他的的子节点
continue;
}
dfs(v, u);//计算u和他的节点v之间
dp[u] += dp[v] + sz[v];//计算根到其子节点的距离
sz[u] += sz[v];//计算节点数量
}
}
void dfs2(int u, int f) {
ans[u] = dp[u];//首先确定“相对”根节点的情况
for (auto& v: graph[u]) {
//然后接着计算此根节点下属的所有子节点的情况
if (v == f) {
continue;
}//不允许计算根节点,该散列表中只有一个节点非该节点的子节点,即在进行根节点push的时候同时所加上的子节点中的根节点
int pu = dp[u], pv = dp[v];//临时替代
int su = sz[u], sv = sz[v];//临时替代
dp[u] -= dp[v] + sz[v];//此时v为节点,则原本u所对应的子节点中没有了v,所以其dp值将减去v的贡献
sz[u] -= sz[v];//其树的节点数量中将减去v的贡献
dp[v] += dp[u] + sz[u];//v变成根节点之后,加上u的贡献
sz[v] += sz[u];//节点集合加上u的贡献
//由于其他的子节点的下属节点未发生改变,则应当没有变化
dfs2(v, u);//进行v的子节点的答案计算
dp[u] = pu, dp[v] = pv;//每次计算之后应当将原本的dp复原,因为此时需要恢复至最开始的数树的状态
sz[u] = su, sz[v] = sv;//节点集合也应当回到最开始的状态
}
}
vector<int> sumOfDistancesInTree(int N, vector<vector<int>>& edges){
ans.resize(N, 0);
sz.z(N, 0);
dp.resize(N, 0);
graph.resize(N, {
});
for (auto& edge: edges) {
int u = edge[0], v = edge[1];
graph[u].emplace_back(v);
graph[v].emplace_back(u);
}
dfs(0, -1);
dfs2(0, -1);
return ans;
}
};
本题有一个天然顺序是需要先计算子节点才能计算父节点。这应当体现在所有的细节里面,所以在dfs2中,dp和sz的动态规划顺序也应当是按照此顺序进行的。