<think>题目要求计算树中每个节点到其他所有节点的距离之和。由于树是无向且连通的,我们可以使用树形动态规划(Tree DP)来解决这个问题。
思路:
我们通常使用两次深度优先搜索(DFS)来解决这类问题:
1. 第一次DFS(后序遍历)计算以每个节点为根的子树的大小(节点个数),并计算根节点到其他所有节点的距离之和(即dp[u]表示以u为根的子树中,所有节点到u的距离之和,同时计算子树大小sz[u])。
2. 第二次DFS(前序遍历)利用父节点的信息,通过换根操作计算出每个节点作为根节点时的总距离(即answer[u])。
具体步骤:
设:
sz[u]表示以u为根的子树中的节点个数(包括u)。
dp[u]表示以u为根的子树中,所有节点到u的距离之和。
第一次DFS(后序遍历):
对于节点u,初始化sz[u]=1,dp[u]=0。
遍历u的所有子节点v(注意:要避免回到父节点):
递归处理v:DFS(v, u)
然后更新:
sz[u] += sz[v]
dp[u] += dp[v] + sz[v] # 因为从u到v的距离为1,所以v子树中的每个节点到u的距离等于到v的距离再加1,因此需要加上sz[v](每个节点都多走了一步)
此时,dp[0]就是根节点0的总距离和,但其他节点的总距离和还没有计算。
第二次DFS(换根DP):
设ans[u]表示节点u到其他所有节点的距离之和。
首先,ans[0] = dp[0]。
然后,考虑将根从u转移到子节点v:
当根从u转移到v时,我们观察到:
- 在v子树中的节点,距离都减少了1(因为从v出发更近了一步),所以这部分贡献为:dp[v](原来在u的dp中v子树部分就是dp[v]+sz[v],但这里我们考虑整体)
- 不在v子树中的节点(即除了v子树以外的节点,共有n-sz[v]个),距离都增加了1(因为从v到这些节点需要先到u,然后再到这些节点,所以比原来多了一步)
因此,我们可以得到:
ans[v] = ans[u] - sz[v] + (n - sz[v]);
解释:ans[u]是u作为根时的总距离,当我们转移到v时:
对于v子树中的节点(共sz[v]个),它们到v的距离比到u的距离少1,所以总距离减少sz[v]。
对于不在v子树中的节点(共n-sz[v]个),它们到v的距离比到u的距离多1,所以总距离增加n-sz[v]。
因此:ans[v] = ans[u] - sz[v] + (n - sz[v]) = ans[u] + n - 2 * sz[v]
但是注意:在第一次DFS中,我们计算dp[u]时,只考虑了以u为根的子树。而换根公式的推导是基于整个树的,所以上述公式成立。
算法步骤:
1. 建图(无向图)。
2. 第一次DFS(从0号节点开始,父节点为-1)计算sz和dp数组。
3. 第二次DFS(从0号节点开始,父节点为-1)利用换根公式计算每个节点的ans。
注意:节点数n最大为30000,所以使用邻接表存储图,并且使用DFS递归时要注意递归深度(可能会爆栈?在C++中可以通过设置栈大小或者使用非递归DFS,但一般n=30000,递归深度可能达到30000,在C++中默认栈空间可能不够,所以需要避免递归过深。这里我们可以使用BFS或者栈来模拟递归,或者使用非递归DFS。但是题目要求C++,我们可以使用非递归DFS?或者使用迭代的BFS来模拟DFS的后序遍历和前序遍历?)
然而,为了简化,我们使用递归DFS,并注意在C++中提交时可能需要扩栈。但这里我们按照常规方法写,如果遇到栈溢出,可以考虑使用非递归方法。不过题目没有特别说明,且n=30000,递归深度可能达到30000,在C++中默认栈大小通常是8MB,可能不够。因此,我们可以使用非递归DFS(即用栈模拟)或者使用BFS进行层次遍历然后逆序处理(后序遍历)。
这里我们使用两次BFS来模拟DFS,以避免递归爆栈。
具体非递归方法:
第一次DFS(后序遍历):
我们可以用栈来模拟递归,但是需要记录每个节点的子节点处理情况。我们可以先进行一次BFS(层次遍历)得到每个节点的父节点,然后按从叶子到根的顺序(逆拓扑序)处理。
步骤:
1. 建立图,并记录每个节点的度(用于拓扑排序?)或者直接通过BFS得到父节点关系。
2. 进行BFS(从0开始),得到每个节点的父节点,并得到层次遍历序列。然后逆序(从最深的节点开始)处理节点,更新其父节点的sz和dp。
第二次DFS(换根):
我们可以使用队列,从根节点0开始,然后依次处理子节点,利用换根公式计算ans。
但是,为了简单且避免递归爆栈,我们也可以使用递归DFS(在C++中,我们可以使用#pragma comment(linker, "/STACK:1024000000,1024000000")来扩栈,但这里我们按照非递归方法写)。
非递归实现第一次DFS(后序遍历):
1. 构建图,然后从0开始BFS,得到每个节点的父节点和深度,同时记录每个节点的子节点列表(在遍历时,对于每个节点,将其邻居中排除父节点的作为子节点)。
2. 然后,按深度从大到小(即从叶子节点开始向上)将节点加入队列(或栈),这样我们可以保证在处理一个节点时,其子节点都已经处理过了。
3. 初始化sz[u]=1, dp[u]=0。
4. 对于每个节点u,遍历其子节点v(在构建的子节点列表中),然后更新:
sz[u] += sz[v]
dp[u] += dp[v] + sz[v]
第二次DFS(换根):
我们使用一个队列,首先将根节点0加入队列,然后依次处理每个节点,对于当前节点u,遍历其所有子节点v(同样,不包括父节点):
ans[v] = ans[u] + n - 2 * sz[v]
然后将v加入队列,继续处理。
注意:第二次遍历(换根)时,我们不需要按深度顺序,只需要按层次(BFS)即可。
步骤总结:
1. 读取n,然后n-1条边。
2. 构建图(邻接表)。
3. 初始化:
sz = vector<int>(n, 0);
dp = vector<long long>(n, 0); // 注意:距离和可能很大,用long long
ans = vector<long long>(n, 0);
4. 第一次BFS(或DFS)构建树结构(得到每个节点的父节点和子节点列表):
使用队列q,从0开始。
parent[0] = -1;
同时,我们用一个数组children来记录每个节点的子节点(不包括父节点)。
另外,还需要一个数组depth记录深度(可选,但可以用来排序)。
但是,为了得到逆序,我们可以记录每个节点的深度,然后按深度从大到小排序。或者我们可以直接使用一个队列,在BFS后,将节点按深度从大到小排序(用栈存储,或者用vector然后逆序遍历)。
具体:
queue<int> q;
vector<int> order; // 记录BFS顺序
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
order.push_back(u);
for (int v : graph[u]) {
if (v == parent[u]) continue;
parent[v] = u;
children[u].push_back(v); // 记录u的子节点v
q.push(v);
}
}
然后,我们按order的逆序(从最后一个节点开始)处理节点,更新sz和dp。
5. 更新sz和dp:
for (int i = order.size()-1; i>=0; i--) {
int u = order[i];
sz[u] = 1; // 初始化为1(自己)
dp[u] = 0;
for (int v : children[u]) {
sz[u] += sz[v];
dp[u] += dp[v] + sz[v]; // 因为v子树中每个节点到u的距离比到v多1,所以加上sz[v]
}
}
6. 第二次BFS(换根):
queue<int> q;
ans[0] = dp[0];
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : children[u]) {
// 换根:从u到v
ans[v] = ans[u] + n - 2 * sz[v];
q.push(v);
}
}
7. 输出ans数组(按节点顺序0到n-1)。
注意:特殊情况,当n=1时,只有0号节点,ans[0]=0。
验证样例1:n=6
输入边: [0,1], [0,2], [2,3], [2,4], [2,5]
树结构:0是根,1和2是0的子节点;2的子节点有3,4,5。
第一次DFS(后序遍历):
节点顺序(BFS顺序):0,1,2,3,4,5 -> 逆序:5,4,3,2,1,0
处理叶子节点(5,4,3):
sz[5]=1, dp[5]=0
sz[4]=1, dp[4]=0
sz[3]=1, dp[3]=0
处理节点2:
sz[2] = 1 + sz[3]+sz[4]+sz[5] = 1+1+1+1=4
dp[2] = (dp[3]+sz[3]) + (dp[4]+sz[4]) + (dp[5]+sz[5]) = (0+1)+(0+1)+(0+1)=3
处理节点1:
sz[1]=1, dp[1]=0
处理节点0:
sz[0]=1+sz[1]+sz[2]=1+1+4=6
dp[0] = (dp[1]+sz[1]) + (dp[2]+sz[2]) = (0+1)+(3+4)=1+7=8 -> 所以ans[0]=8
然后换根:
计算ans[1]:ans[1] = ans[0] + n - 2*sz[1] = 8 + 6 - 2*1 = 12
计算ans[2]:ans[2] = ans[0] + n - 2*sz[2] = 8 + 6 - 2*4 = 14-8=6
计算ans[3]:需要从节点2转移?但是节点2的子节点3,4,5,但是我们在换根时,是从0->2,然后2->3?
注意:我们的换根是从父节点到子节点,所以节点3的父节点是2,所以我们要先计算ans[2],然后再计算ans[3]。
那么节点3:ans[3] = ans[2] + n - 2*sz[3] = 6 + 6 - 2*1 = 10
同理,节点4:ans[4]=ans[2]+6-2=6+6-2=10
节点5:10
所以得到:ans[0]=8, ans[1]=12, ans[2]=6, ans[3]=10, ans[4]=10, ans[5]=10 -> 8 12 6 10 10 10
但是注意:在换根公式中,我们要求子节点v的sz[v]是第一次DFS中计算出来的以v为根的子树大小。在换根时,我们并没有改变子树大小,所以直接使用。
然而,在换根时,我们只考虑了直接子节点。但是节点3,4,5是节点2的子节点,所以在第二次BFS中,从0开始,先处理0的子节点1和2,然后处理2的子节点3,4,5。
所以,我们使用队列进行BFS,先处理0,然后处理1和2,然后处理3,4,5。这样顺序正确。
但是注意:节点1没有子节点,所以不会加入队列?所以不会处理1的子节点(实际上也没有)。
因此,代码实现:
步骤:
1. 读入n。
2. 如果n==1,直接输出0。
3. 构建图:vector<vector<int>> graph(n);
for (int i=0; i<n-1; i++) {
int a, b;
cin >> a >> b;
graph[a].push_back(b);
graph[b].push_back(a);
}
4. 初始化:
vector<long long> dp(n,0), ans(n,0);
vector<int> sz(n,0);
vector<int> parent(n, -1);
vector<vector<int>> children(n); // 每个节点的子节点列表
vector<int> order;
// BFS得到层次遍历顺序和父节点关系,以及子节点列表
queue<int> q;
q.push(0);
parent[0] = -1;
while (!q.empty()) {
int u = q.front(); q.pop();
order.push_back(u);
for (int v : graph[u]) {
if (v == parent[u]) continue;
parent[v] = u;
children[u].push_back(v);
q.push(v);
}
}
// 逆序遍历order(从最后一个节点开始)
for (int i = order.size()-1; i>=0; i--) {
int u = order[i];
sz[u] = 1; // 节点本身
dp[u] = 0;
for (int v : children[u]) {
sz[u] += sz[v];
dp[u] += dp[v] + sz[v]; // 注意:这里加上dp[v]和sz[v](因为v子树中每个节点到u的距离比到v多1)
}
}
// 第二次BFS(换根)
ans[0] = dp[0];
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : children[u]) {
ans[v] = ans[u] + n - 2 * sz[v];
q.push(v);
}
}
5. 输出ans数组:按节点顺序0到n-1输出。
注意:输出格式为一行n个整数,空格分隔。
样例输入2:n=1,输出0。
样例输入3:n=2,边为1 0(即节点1和0相连)
BFS:0(父节点为-1),然后遍历0的邻居:1(parent[1]=0,children[0]={1}),然后1没有邻居(除了0,所以children[1]为空)。
逆序:先1后0。
处理1:sz[1]=1, dp[1]=0
处理0:sz[0]=1+sz[1]=2, dp[0]=dp[1]+sz[1]=0+1=1 -> 所以ans[0]=1
然后换根:从0到1,ans[1]=ans[0]+2-2*sz[1]=1+2-2=1
输出:1 1
符合样例。
代码实现(C++):</think>### 解法思路
为了解决树中每个节点到其他所有节点距离之和的问题,我们使用树形动态规划(Tree DP)结合换根法(Re-rooting)。主要分为两个步骤:
1. **第一次DFS(后序遍历)**:计算每个节点的子树大小(`sz[u]`)和以该节点为根的子树中所有节点到该节点的距离之和(`dp[u]`)。
2. **第二次BFS(换根操作)**:利用父节点的信息,通过公式 `ans[v] = ans[u] + n - 2 * sz[v]` 计算每个节点作为根时的总距离和。
### 代码实现(C++)
```cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
if (n == 1) {
cout << 0 << endl;
return 0;
}
vector<vector<int>> graph(n);
for (int i = 0; i < n - 1; i++) {
int a, b;
cin >> a >> b;
graph[a].push_back(b);
graph[b].push_back(a);
}
vector<long long> dp(n, 0);
vector<long long> ans(n, 0);
vector<int> sz(n, 0);
vector<int> parent(n, -1);
vector<vector<int>> children(n);
vector<int> order;
queue<int> q;
q.push(0);
parent[0] = -1;
while (!q.empty()) {
int u = q.front();
q.pop();
order.push_back(u);
for (int v : graph[u]) {
if (v == parent[u]) continue;
parent[v] = u;
children[u].push_back(v);
q.push(v);
}
}
for (int i = order.size() - 1; i >= 0; i--) {
int u = order[i];
sz[u] = 1;
dp[u] = 0;
for (int v : children[u]) {
sz[u] += sz[v];
dp[u] += dp[v] + sz[v];
}
}
ans[0] = dp[0];
q.push(0);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : children[u]) {
ans[v] = ans[u] + n - 2 * sz[v];
q.push(v);
}
}
for (int i = 0; i < n; i++) {
cout << ans[i];
if (i < n - 1) cout << " ";
}
cout << endl;
return 0;
}
```
### 算法分析
- **时间复杂度**:O(n),其中 n 是节点数。两次遍历(BFS 和逆序处理)均线性时间完成。
- **空间复杂度**:O(n),用于存储图、子树大小、距离和等数组。
### 解释样例
- **样例输入1**:节点6,边为 `(0,1)`, `(0,2)`, `(2,3)`, `(2,4)`, `(2,5)`。输出 `8 12 6 10 10 10` 符合计算:
- 节点0:距离和为 1(到1)+1(到2)+2(到3)+2(到4)+2(到5)= 8。
- 其他节点通过换根公式计算,如节点1:`ans[1] = 8 + 6 - 2*1 = 12`。
- **样例输入2**:单节点,输出 `0`。
- **样例输入3**:两个节点,输出 `1 1`(互距为1)。
###