题目链接

绿豆蛙的归宿

题目描述

给定一个 个点、 条边的有向无环连通图(DAG)。起点为 ,终点为 。每条边 都有一个非负长度

当绿豆蛙位于顶点 时,如果该点有 条出边,它会以等概率()选择其中一条边离开。

请计算绿豆蛙从起点 出发,到达终点 的路径总长度的期望值。

输入:

  • 第一行输入两个整数 ,分别表示点数与边数。
  • 接下来 行,每行输入三个整数 ,表示存在一条从 指向 、长度为 的有向边。
  • 保证图是 DAG,且从 可以到达

输出:

  • 输出一个实数,表示期望路径长度,四舍五入保留两位小数。

解题思路

这是一个在有向无环图(DAG)上求期望的经典问题,可以使用动态规划(或记忆化搜索)来解决。

1. 定义状态

  • 我们要求的是从起点 到终点 的期望路径长度。
  • 一个很自然的想法是定义 dp[u] 表示从起点 到达点 的期望路径长度。但是这个状态很难转移,因为一个点 可能有多个前驱节点,计算 的期望需要综合考虑所有到达它的路径及其概率,非常复杂。
  • 更巧妙的方法是反向定义状态。设 dp[u] 为从点 出发到达终点 所需要的期望路径长度。我们的目标就是求 dp[1]

2. 状态转移方程

  • 基本情况:对于终点 ,它已经到达了归宿,所以从 出发到 的期望长度为 。即 dp[N] = 0
  • 递推关系:对于任意一个非终点 u,考虑它下一步的走向。
    • 设点 条出边,分别通向 ,对应的边长为
    • 根据题意,绿豆蛙会以 的概率选择任意一条出边
    • 如果选择了边 ,那么从 出发的总路径长度,就是这条边的长度 加上从 再走到终点 的期望长度 dp[v_j]
    • 根据全期望公式,从 出发的总期望长度,就是所有可能选择的期望长度的加权平均:

3. 计算顺序

  • 观察状态转移方程,我们发现要计算 dp[u],必须先知道它所有后继节点 dp 值。
  • 在一个 DAG 中,所有节点的后继节点的拓扑序都在该节点之后。因此,我们需要按照拓扑序的逆序来进行计算。
  • 实现上,最简洁的方式是使用记忆化搜索(DFS)。我们从 dfs(1) 开始调用,函数会递归地深入到图的末端,当遇到终点 时返回 。在回溯的过程中,每个节点的 dp 值都会被计算并存储起来,避免重复计算。

4. 算法流程

  1. 读入图,并使用邻接表存储。同时,计算每个节点的出度。
  2. 定义 dp 数组用于记忆化,初始化为一个特殊值(例如 -1)。
  3. 编写 dfs(u) 函数:
    • 如果 dp[u] 已经被计算过,直接返回。
    • 如果 u == N,返回 0。
    • 初始化 sum = 0
    • 遍历 u 的所有出边 ,对每一条边,sum += w + dfs(v)
    • 计算 dp[u] = sum / out_degree[u]
    • 返回 dp[u]
  4. 调用 dfs(1) 并输出结果。

代码

#include <iostream>
#include <vector>
#include <iomanip>

using namespace std;

int n, m;
vector<pair<int, int>> adj[100005];
int out_degree[100005];
double dp[100005];
bool visited[100005];

double solve(int u) {
    if (visited[u]) {
        return dp[u];
    }
    if (u == n) {
        return 0.0;
    }
    
    visited[u] = true;
    double expected_len = 0.0;
    
    for (auto const& [v, w] : adj[u]) {
        expected_len += (double)w + solve(v);
    }
    
    dp[u] = expected_len / out_degree[u];
    return dp[u];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);

    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        out_degree[u]++;
    }

    double result = solve(1);

    cout << fixed << setprecision(2) << result << endl;

    return 0;
}
import java.util.*;
import java.io.*;

public class Main {
    static int n, m;
    static List<List<int[]>> adj;
    static int[] outDegree;
    static double[] dp;
    static boolean[] visited;

    public static double solve(int u) {
        if (visited[u]) {
            return dp[u];
        }
        if (u == n) {
            return 0.0;
        }

        visited[u] = true;
        double expectedLen = 0.0;

        for (int[] edge : adj.get(u)) {
            int v = edge[0];
            int w = edge[1];
            expectedLen += (double)w + solve(v);
        }
        
        dp[u] = expectedLen / outDegree[u];
        return dp[u];
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken());
        m = Integer.parseInt(st.nextToken());

        adj = new ArrayList<>();
        for (int i = 0; i <= n; i++) {
            adj.add(new ArrayList<>());
        }
        outDegree = new int[n + 1];
        dp = new double[n + 1];
        visited = new boolean[n + 1];

        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            int u = Integer.parseInt(st.nextToken());
            int v = Integer.parseInt(st.nextToken());
            int w = Integer.parseInt(st.nextToken());
            adj.get(u).add(new int[]{v, w});
            outDegree[u]++;
        }

        double result = solve(1);
        System.out.printf("%.2f\n", result);
    }
}
import sys

# 增加递归深度限制
sys.setrecursionlimit(100005)

def solve(u):
    # 如果已经计算过,直接返回
    if visited[u]:
        return dp[u]
    
    # 到达终点,期望为0
    if u == n:
        return 0.0

    visited[u] = True
    expected_len = 0.0
    
    # 遍历所有出边
    for v, w in adj[u]:
        expected_len += w + solve(v)
    
    # 计算期望
    dp[u] = expected_len / out_degree[u]
    return dp[u]

def main():
    global n, adj, out_degree, dp, visited
    
    n, m = map(int, input().split())
    
    adj = [[] for _ in range(n + 1)]
    out_degree = [0] * (n + 1)
    dp = [0.0] * (n + 1)
    visited = [False] * (n + 1)

    for _ in range(m):
        u, v, w = map(int, input().split())
        adj[u].append((v, w))
        out_degree[u] += 1
        
    result = solve(1)
    print(f"{result:.2f}")

if __name__ == "__main__":
    main()

算法及复杂度

  • 算法:期望动态规划 / 记忆化搜索
  • 时间复杂度:每个点和每条边都只会被访问一次。因此,总时间复杂度为
  • 空间复杂度:需要邻接表来存储图,以及 DP 数组和出度数组,空间复杂度为