【问题标题】:Directed probability graph - algorithm to reduce cycles?有向概率图 - 减少周期的算法?
【发布时间】:2017-02-08 22:18:56
【问题描述】:

考虑一个有向图,它从第一个节点 1 遍历到一些最终节点(没有更多的出边)。图中的每条边都有一个与之相关的概率。将每条可能路径通向所有可能的最终节点的概率相加返回1。 (这意味着,我们保证最终到达最终节点之一。)

如果图中不存在循环,问题就会很简单。不幸的是,图中可能会出现相当复杂的循环,它可以被无限次遍历(显然,概率随着每次循环遍历而成倍减小)。

是否有一种通用算法可以找到到达每个最终节点的概率?

一个特别讨厌的例子:

我们可以将边表示为矩阵(从行(节点)x 到行(节点)y 的概率在条目(x,y) 中)

{{0, 1/2, 0, 1/14, 1/14, 0, 5/14}, 
 {0, 0, 1/9, 1/2, 0, 7/18, 0}, 
 {1/8, 7/16, 0, 3/16, 1/8, 0, 1/8}, 
 {0, 1, 0, 0, 0, 0, 0}, 
 {0, 0, 0, 0, 0, 0, 0}, 
 {0, 0, 0, 0, 0, 0, 0}, 
 {0, 0, 0, 0, 0, 0, 0}}

或者作为有向图:

起始节点1 为蓝色,最终节点5,6,7 为绿色。所有边都用从它们起源的节点开始遍历它们的概率来标记。

从起始节点 1 到最终节点有 8 条不同的路径:

{{1/14, {1, 5}}, {5/14, {1, 7}}, {7/36, {1, 2, 6}}, 
 {1/144, {1, 2, 3, 5}}, {1/144, {1, 2, 3, 7}}, 
 {1/36, {1, 4, 2, 6}}, {1/1008, {1, 4, 2, 3, 5}}, {1/1008, {1, 4, 2, 3, 7}}}

(每个路径的符号是{概率,访问的节点序列})

并且有五个不同的循环:

{{1/144, {2, 3, 1}}, {7/144, {3, 2}}, {1/2, {4, 2}}, 
{1/48, {3, 4, 2}}, {1/1008, {4, 2, 3, 1}}}

(循环的符号是{遍历循环一次的概率,访问的节点序列})。

如果只有这些循环可以解决以获得有效的树状图,问题就会解决。

关于如何解决这个问题的任何提示?

我熟悉 Java、C++ 和 C,因此建议使用这些语言的建议。

【问题讨论】:

  • 您可以通过阻止已经访问过的节点来避免无限循环。无论您使用的是递归解决方案还是 Dijkstra 算法之类的东西,这都没有什么不好的地方。
  • @WeatherVane 你是说我列出的8条不同的路径已经是最终结果了吗?我相信实际上允许无限循环,因为它最终成为循环遍历概率中的几何级数(在最简单的情况下为 1 个循环),它收敛到一个有限数。我的麻烦是当有很多循环和几个相交时。然后我看不到如何概括几何级数。
  • 您询问如何删除周期。
  • 他不想要简单的循环去除,他想要评估到达每个末端节点的渐近概率,假设有无限组通过图的循环路径。
  • 您正在描述某种Markov chains 的图表。我为此添加了 SO 的标签,这可能会引起比我更适合回答的人的注意。

标签: java c++ c directed-graph markov-chains


【解决方案1】:

我不是马尔可夫链领域的专家,虽然我认为算法很可能因你提出的问题而闻名,但我很难找到它们。

如果这个方向没有帮助,那么您可以考虑自己动手。我在这里看到至少两种不同的方法:

  1. 模拟。

通过以 100% 的概率从处于状态 1 的系统开始,并执行多次迭代,在其中应用转移概率来计算迈出一步后获得的状态概率,从而检查系统的状态如何随时间演变。如果可以从每个节点(以非零概率)到达至少一个最终(“吸收”)节点,那么经过足够多的步骤,系统处于最终状态以外的任何状态的概率将逐渐接近零。您可以将系统以最终状态 S 结束的概率估计为在 n 步后处于状态 S 的概率,该估计中的误差上限由系统的概率给出在 n 个步骤之后处于非最终状态。

实际上,计算 Trn 也是如此,其中 Tr 是您的过渡概率矩阵,以 100% 的概率对所有最终状态添加自边缘。

  1. 精确计算。

考虑一个图形 G,如您所描述的。给定两个顶点if,使得从if至少有一条路径,并且f 除了自边外没有出边,我们可以将路径从 if 划分为以它们重访次数为特征的类i 在到达 f 之前。这样的类可能有无数个,我将它们指定为 Cif(n),其中 n 表示Cif(n)中的路径重访节点的次数。特别是,Cii(0) 包含 G 中所有包含 i ( 澄清:以及其他路径)。

假设系统从节点i开始遍历图G,在节点f结束的总概率由下式给出

Pr(f|i, G) = Pr(Cif(0)|G) + Pr(C如果(1)|G) + Pr(C如果(2)|G) ...

现在观察如果 n > 0 那么 Cif(n) 具有 ct 两条路径并集的形式,其中 c 属于 Cii(n-1) 和 t 属于 C如果(0)。即c是一条从节点i开始到节点i结束的路径,经过in-1次之间,t是从if的不经过的路径我再次。我们可以用它来重写我们的概率公式:

Pr(f|i,G) = Pr(Cif(0)|G) + Pr(Cii(0)|G) * Pr(C如果(0)|G) + Pr(Cii(1) |G) * Pr(C如果(0)|G) + ...

但请注意,Cii(n) 中的每条路径都是 n 的组合+1 个路径属于 Cii(0)。 Pr(Cii(n)|G) = Pr( Cii(0)|G)n+1,所以我们得到

Pr(f|i) = Pr(Cif( 0)|G) + Pr(Cii(0)|G) * Pr(C如果(0)|G) + Pr(Cii(0)|G )2 * Pr(Cif(0)|G) + ...

现在,一个小代数给了我们

Pr(f|i,G) - Pr(Cif(0)|G) = Pr(Cii(0)|G) * Pr(f |,G)

,我们可以解出 Pr(f|i,G) 得到

Pr(f|i,G) = Pr(Cif(0)|G) / (1 - Pr(Cii(0)|G))

因此,就不返回起始节点的路径而言,我们已将问题简化为一个问题,除非可能作为其结束节点。这些并不排除具有不包括起始节点的循环的路径,但我们仍然可以根据原始问题的几个实例重写这个问题,在原始图的子图上计算。

特别地,令 S(i, G) 为图 G 中顶点 i 的后继集合——即,顶点集合 s 使得 G 中有一条从 is 的边,令 X(G,i) 是 G 的子图,它是通过删除从 i 开始的所有边而形成的。此外,令 pis 为 G 中与边 (i, s) 相关的概率。

Pr(Cif(0)|G) = S 中 s 的总和pis(i, G) * Pr(f|s,X(G,i))

换句话说,从 i 到 G 到达 f 而中间不重新访问 i 的概率是 i 的所有后继者的总和em>i 是从 i 一步到达 s 的概率与从 f 到达的概率的乘积em>s 到 G,而不遍历任何从 i 出站的边。这适用于 G 中的所有 f,包括 i

现在观察 S(i, G) 和所有 pis 都是已知的,并且计算 Pr(f|s,X(G,i)) 的问题是原始问题的一个新的、严格更小的实例。因此,可以递归地执行该计算,并且保证这样的递归终止。但是,如果您的图表很复杂,它可能需要很长时间,而且看起来这种递归方法的简单实现会在节点数量上呈指数级扩展。有一些方法可以加快计算速度,以换取更高的内存使用率(即记忆化)。


可能还有其他可能性。例如,我怀疑可能存在一种自下而上的动态编程方法来解决问题,但我无法说服自己图中的循环不会出现无法解决的问题。

【讨论】:

  • 我相信记忆会让它几乎是线性的(肯定是多项式的)。你太棒了!谢谢。
  • @FauChristian,问题要求的是“一种通用算法来找到到达每个最终节点的概率”(强调添加),这个答案提供二。如果您不认为这些是实施提示,那么实施提示确实不是问题所要求的。当然,OP似乎完全满意这个答案回答了这个问题。
  • 这个答案完全分解了问题。没有什么可补充的了。只需接受它并按字面意思实施它,它就会起作用。
  • @FauChristian,无论您是否接受所提出的结论满足“算法”的定义,以我的方式开发它的优点之一是结论的逻辑过程都达到了,一清二楚。我断言该方法可以很好地适应循环重叠。如果您不同意,那么您当然可以显示对非重叠的依赖进入推导的位置。
  • @FauChristian,(1) 推导中没有删除项需要任何“更正”。通过使用其中固有的递推关系,无限级数被简化为两项之和。 (2) S(i,G) 中是否存在顶点 i 无关紧要,因为此时我们正在计算子图 X(G,i) 中的路径。该图包含 G 的所有顶点,但没有从(或通过)i 到任何其他顶点的路径。我描述了从 i 中删除出站边缘,但如果您愿意,可以通过将它们全部替换为一个自身边缘来保留马尔可夫属性。
【解决方案2】:

问题说明

输入数据是一组 m 行 n 列概率,本质上是一个 m × n 矩阵,其中 m = n = 有向图上的顶点数。行是边缘起点,列是边缘目的地。根据问题中提到的循环,我们将认为图是循环的,图中至少存在一个循环。

让我们将起始顶点定义为 s。让我们还将终端顶点定义为没有退出边的顶点,并将它们的集合定义为大小为 z 的集合 T。因此,我们有 z 组从 s 到 T 中一个顶点的路线,并且由于循环1,这些路线的大小可能是无限的。在这种情况下,我们不能断定将在任意多的步骤中到达终端顶点。

在输入数据中,与不在 T 中的顶点对应的行的概率被归一化为总计为 1.0。我们将假设马尔可夫性质,即每个顶点的概率不随时间变化。这排除了在图形搜索中使用概率来确定路线优先级2

有限的数学课本有时将类似于此问题的示例问题命名为 Drunken Random Walks,以强调步行者忘记过去的事实, 指的是马尔可夫链的无内存特性。

将概率应用于路线

到达终端顶点的概率可以表示为乘积的无穷级数和。

Pt = lim s -> ∞ Σ ∏ Pi, j,

其中 s 是步长索引,t 是终端顶点索引,i ∈ [1 .. m] 和 j ∈ [1 .. n]

减少

当两个或多个循环相交(共享一个或多个顶点)时,分析会因涉及它们的无限模式集而变得复杂。看来,在对relevant academic work 进行一些分析和审查后,使用当今的数学工具得出一组准确的终端顶点到达概率可能最好使用收敛算法来完成。

一些初始减少是可能的。

  1. 首先要考虑的是枚举目标顶点,这很容易,因为相应行的概率为零。

  2. 接下来的考虑是将任何进一步的约简与学术文献所称的不可约子图区分开来。下面的深度优先算法会记住在构建潜在路线时已经访问过哪些顶点,因此可以轻松地对其进行改造以识别哪些顶点参与循环。但是,建议使用现有的经过良好测试、同行评审的图形库来识别子图并将其表征为不可约。

图的不可约部分的数学简化可能是合理的,也可能是不合理的。考虑图中的起始顶点 A 和唯一终止顶点 B 表示为 {A->C, C->A, A->D, D->A, C->D, D->C, C->B, D->B}。

虽然可以将图简化为不存在通过顶点 A 的循环的概率关系,但如果不修改退出 C 和 D 的顶点的概率或允许退出 C 和 D 的边的概率的总和,则不能删除顶点 A 以进一步简化小于 1.0。

收敛广度优先遍历

忽略重访并允许循环的广度优先遍历可以迭代步长索引 s,不是到某个固定的 smax,而是到某个收敛趋势中足够稳定和准确的点。如果循环重叠在由单个循环引起的更简单的周期性中产生分叉,则尤其需要这种方法。

Σ PsΔs.

为了在 s 增加时建立合理的收敛,必须确定所需的精度作为完成收敛算法的标准和衡量精度的指标,方法是查看所有终端顶点结果的长期趋势。结合趋势收敛度量提供一个标准,其中终端顶点概率的总和接近于一,作为健全性检查和准确性标准,这可能很重要。实际上,可能需要四个收敛标准3

  1. 每终端顶点概率趋势收敛增量
  2. 平均概率趋势收敛增量
  3. 总概率在单位上的收敛
  4. 总步数(出于实际计算原因限制深度)

即使超出这四个条件,程序也可能需要包含一个中断陷阱,允许在不满足上述所有四个条件的情况下在长时间等待后写入和随后检查输出。

抗周期深度优先算法示例

有比以下算法更有效的算法,但它相当容易理解,它使用 C++ -Wall 编译时不会发出警告,它为所有有限和合法的有向图以及可能的起始和目标顶点生成所需的输出 4。使用 addEdge 方法 5 可以轻松加载问题中给出的形式的矩阵。

#include <iostream>
#include <list>

class DirectedGraph {

    private:
        int miNodes;
        std::list<int> * mnpEdges;
        bool * mpVisitedFlags;

    private:
        void initAlreadyVisited() {
            for (int i = 0; i < miNodes; ++ i)
                mpVisitedFlags[i] = false;
        }

        void recurse(int iCurrent, int iDestination,
               int route[], int index,
               std::list<std::list<int> *> * pnai) {

            mpVisitedFlags[iCurrent] = true;
            route[index ++] = iCurrent;

            if (iCurrent == iDestination) {
                auto pni = new std::list<int>;
                for (int i = 0; i < index; ++ i)
                    pni->push_back(route[i]);
                pnai->push_back(pni);

            } else {
                auto it = mnpEdges[iCurrent].begin();
                auto itBeyond = mnpEdges[iCurrent].end();
                while (it != itBeyond) {
                    if (! mpVisitedFlags[* it])
                        recurse(* it, iDestination,
                                route, index, pnai);
                    ++ it;
                }
            }

            -- index;
            mpVisitedFlags[iCurrent] = false;
        } 

    public:
        DirectedGraph(int iNodes) {
            miNodes = iNodes;
            mnpEdges = new std::list<int>[iNodes];
            mpVisitedFlags = new bool[iNodes];
        }

        ~DirectedGraph() {
            delete mpVisitedFlags;
        }

        void addEdge(int u, int v) {
            mnpEdges[u].push_back(v);
        }

        std::list<std::list<int> *> * findRoutes(int iStart,
                int iDestination) {
            initAlreadyVisited();
            auto route = new int[miNodes];
            auto pnpi = new std::list<std::list<int> *>();
            recurse(iStart, iDestination, route, 0, pnpi);
            delete route;
            return pnpi;
        }
};

int main() {

    DirectedGraph dg(5);

    dg.addEdge(0, 1);
    dg.addEdge(0, 2);
    dg.addEdge(0, 3);
    dg.addEdge(1, 3);
    dg.addEdge(1, 4);
    dg.addEdge(2, 0);
    dg.addEdge(2, 1);
    dg.addEdge(4, 1);
    dg.addEdge(4, 3);

    int startingNode = 2;
    int destinationNode = 3;

    auto pnai = dg.findRoutes(startingNode, destinationNode);

    std::cout
            << "Unique routes from "
            << startingNode
            << " to "
            << destinationNode
            << std::endl
            << std::endl;

    bool bFirst;
    std::list<int> * pi;
    auto it = pnai->begin();
    auto itBeyond = pnai->end();
    std::list<int>::iterator itInner;
    std::list<int>::iterator itInnerBeyond;
    while (it != itBeyond) {
        bFirst = true;
        pi = * it ++;
        itInner = pi->begin();
        itInnerBeyond = pi->end();
        while (itInner != itInnerBeyond) {
            if (bFirst)
                bFirst = false;
            else
                std::cout << ' ';
            std::cout << (* itInner ++);
        }
        std::cout << std::endl;
        delete pi;
    }

    delete pnai;

    return 0;
}

备注

[1] 有向图算法中处理不当的循环将陷入无限循环。 (请注意,有向图从 A 到 B 的路径数表示为 {A->B, B->A} 的简单情况是无穷大。)

[2] 概率有时用于降低搜索的 CPU 周期成本。在该策略中,概率是优先级队列中元规则的输入值,以减少计算挑战非常繁琐的搜索(即使对于计算机也是如此)。生产系统中的早期文献将无引导的大型搜索组合爆炸的指数特征称为指数特征。

[3] 实际上可能需要检测每个顶点的广度优先概率趋势,并根据四个标准指定令人满意的收敛

  1. Δ(Σ∏P)tmax ∀ t
  2. Σt=0T Δ(Σ∏P)t / T ave
  3. |ΣΣ∏P - 1| max,其中 u 是最终概率总和的最大允许偏差
  4. s 最大

[4] 只要有足够的可用计算资源来支持数据结构和足够的时间来得出给定计算系统速度的答案。

[5] 您可以使用两个嵌套循环来加载 DirectedGraph dg(7) 和输入数据,以遍历问题中列举的行和列。内部循环的主体只是一个条件边添加。

if (prob != 0) dg.addEdge(i, j);

变量概率是 P m,n。路由存在只关心零/非零状态。

【讨论】:

  • 我最终对循环遍历进行了精确的恢复。具有记忆的递归算法对于大小为 10 个状态系统的运行速度非常快,即使在资源非常少的情况下也是如此。您是否在我的问题中的示例上测试了您的代码?也许我们可以比较我们得到的最终状态概率是否一致。
  • / 是除法的启发式。基本上,该矩阵的条目是有理数。每行加起来为 1(因此进入其他状态(或保持原状态)的概率为 100%)。全零的行是最终状态(离开它们的概率是 0%)。节点 id 是行号。 (第一行:id=0,第二行:id=1 等)相应的,列号为目的节点id。当列中存在非零概率时,可以从相应行(起始节点)过渡到该节点。
  • 谢谢!我会仔细看看。
【解决方案3】:

我在研究有向循环图时发现了这个问题。可以使用吸收马尔可夫链计算到达每个最终节点的概率。

视频Markov Chains - Part 7(+ 第 8 部分和第 9 部分)解释了马尔可夫链中的吸收状态及其背后的数学原理。

【讨论】:

    【解决方案4】:

    我理解为以下问题:

    给定每个节点上的初始分布作为向量 b 和一个矩阵 A,它存储在每个时间步从节点 i 跳转到节点 j 的概率,有点类似于邻接矩阵。

    那么经过一个时间步的分布 b_1 是 A x b。两个时间步后的分布 b_2 为 A x b_1。同样,分布 b_n 是 A^n x b。

    对于 b_infinite 的近似值,我们可以执行以下操作:

    Vector final_probability(Matrix A, Vector b,
        Function Vector x Vector -> Scalar distance, Scalar threshold){
        b_old = b
        b_current = A x b
        while(distance(b_old,b_current) < threshold){
            b_old = b_current
            b_current = A x b_current
        }
        return b_current
    }
    

    (为了方便,我使用了数学变量名)

    换句话说,我们假设分布序列在给定阈值之后很好地收敛。可能不成立,但通常会起作用。

    您可能希望为此添加最大数量的迭代。

    欧几里得距离应该和距离一样好。

    (这使用了Markov Chain 的概念,但更实用的解决方案)

    【讨论】:

      猜你喜欢
      • 2021-05-30
      • 2022-01-11
      • 2020-12-09
      • 1970-01-01
      • 2021-03-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多