📜  门| GATE-CS-2016(Set 2)|问题23(1)

📅  最后修改于: 2023-12-03 15:12:43.220000             🧑  作者: Mango

门 | GATE-CS-2016(Set 2) | 问题23

这是一道关于有向图的问题,具体要求如下:

给定一个有向无环图(DAG),从源节点(source vertex)S到终点节点(destination vertex)D有多少条路径可以通过恰好K条边(K=1,2,3...)到达?

例如,下面这个DAG上,从节点1(S)到节点5(D),通过恰好1条边到达的路径有1-3-5和1-2-5,共2条;通过恰好2条边到达的路径有1-2-3-5和1-3-4-5,共2条;通过恰好3条边到达的路径只有1-3-4-2-5,共1条。

DAG

解法

这个问题可以通过动态规划来解决。具体地,我们可以以节点为单位,考虑每个节点可以通过恰好K条边到达的路径数目。假设有K步可以到达节点v,那么到达该节点有两种可能:

  • 通过有向边的形式到达。由分析我们知道,从每个祖先节点通过一条出度边能够到达该节点,那么我们可以在从其余的祖先节点到当前节点的路径上,消耗掉一步步长,即在K-1步的路径中到达祖先节点,再通过一条出度边来到当前节点。考虑到每个节点到达的路径数目只和它的祖先节点有关,而祖先节点已经被我们预处理完了,因此,这部分计算可以在使用到节点v的时候一起完成。
  • 通过节点v自身到达。在这种情况下,我们需要考虑节点v的所有入度边,即从每个入度祖先节点到节点v的路径。为什么需要考虑所有的边呢?因为节点v本身可能有入度边,也就是说,这个问题并不是只取决于节点v的祖先,同时还要考虑该节点本身到达的路径。所以,我们还需要考虑所有的入度祖先节点i。如此一来,我们可以在路径长度为K-1的路径数目上,加上从入度祖先节点i到节点v的边。

直接应用动态规划的话,时间复杂度是O(K*V^2),其中V是有向图的节点数目。可以发现,计算到一个节点的中间可能涉及到每个节点的祖先,造成了重复计算。因此,我们子问题之间有重叠的特性,可以使用备忘录法来避免重复计算。

def count_paths(G, s, d, K):
    V = len(G)
  
    # Calculate ancestors of each node
    ancestors = [[] for _ in range(V)]
    for u in range(V):
        for v in range(V):
            if G[u][v] == 1:
                ancestors[v].append(u)

    # Initialize the memo table
    memo = [[[-1]*(K+1) for _ in range(V)] for _ in range(V)]

    # Base cases:
    # 1) If K = 0 and i = j, there exists only 1 path from a vertex to itself of
    # length 0 (i.e., the empty path).
    # 2) If K = 1 and an edge exist from i–>j, there exists a path of length 1 from i to j.
    for i in range(V):
        for j in range(V):
            if K == 0 and i == j:
                memo[i][j][0] = 1
            if K == 1 and G[i][j] == 1:
                memo[i][j][1] = 1

    # Fill the remaining tables using recursive rules
    for count in range(2, K+1):
        for dest in range(V):
            for src in ancestors[dest]:
                for w in range(V):
                    if memo[src][w][count-1] != -1:
                        if memo[src][w][count] == -1:
                            memo[src][w][count] = 0
                        memo[src][w][count] += memo[src][dest][count-1]

    # Count the total number of paths
    total_paths = 0
    for i in range(K+1):
        total_paths += memo[s][d][i] if memo[s][d][i] != -1 else 0

    return total_paths

该函数接受一个邻接矩阵G,源节点s,终点节点d和一个步长K作为输入。函数通过动态规划求解从源节点s到终点节点d恰好经过K条边的路径数目。输出是一个整数,表示查询到的路径数目。

结论

我们可以使用备忘录优化的动态规划求解从一个节点到另一个节点经过K条边的路径数目。该算法的时间复杂度是O(V^2*K),其中V是有向图的节点数目,K是路径长度。

参考文献

[1] Sartaj Sahni, "Data Structures, Algorithms, and Applications in C++," 2nd edition, 2004, pp. 595-596.

[2] Python implementation of the solution described in Reference [1]: https://ideone.com/NAznef.