逆拓扑+dp

背景

随着新版百度空间的上线,Blog宠物绿豆蛙完成了它的使命,去寻找它新的归宿。

描述

给出一个有向无环图,起点为1终点为N,每条边都有一个长度,并且从起点出发能够到达所有的点,所有的点也都能够到达终点。绿豆蛙从起点出发,走向终点。
到达每一个顶点时,如果有K条离开该点的道路,绿豆蛙可以选择任意一条道路离开该点,并且走向每条路的概率为 1/K 。
现在绿豆蛙想知道,从起点走到终点的所经过的路径总长度期望是多少?

输入格式

第一行: 两个整数 N M,代表图中有N个点、M条边
第二行到第 1+M 行: 每行3个整数 a b c,代表从a到b有一条长度为c的有向边

输出格式

从起点到终点路径总长度的期望值,四舍五入保留两位小数。

测试样例1

输入

4 4 
1 2 1 
1 3 2 
2 3 3 
3 4 4

输出

7.00

此题用记忆化会爆栈,故需要逆拓扑后,进行dp。

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

struct edge{
    int to,w,next;
}p[200010];
int tot,n;
int head[100010];
void addedge(int a,int b,int c){
    p[tot].to=b;
    p[tot].w=c;
    p[tot].next=head[a];
    head[a]=tot++;
}
double dp[100010];
int out[100010],q[100010];
int out1[100010];
int main()
{
    int m,a,b,c;
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof head);
    while(m--){
        scanf("%d%d%d",&a,&b,&c);
        addedge(b,a,c);
        out[a]=++out1[a];
    }
    int s=0,e=-1;
    q[++e]=n;
    while(s<=e){
        int u=q[s++];
        for(int i=head[u];i!=-1;i=p[i].next){
            int v=p[i].to;
            dp[v]+=(dp[u]+p[i].w)/out[v];
            if(--out1[v]==0) q[++e]=v;
        }
    }
    printf("%.2f\n",dp[1]);
    return 0;
}




























<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)。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值