代码之家  ›  专栏  ›  技术社区  ›  jayphelps

在逐步构建有向图的同时,更有效地计算每个依赖项的传递闭包

  •  9
  • jayphelps  · 技术社区  · 7 年前

    我需要回答以下问题: 在依赖关系图中给定一个节点,将其依赖项按它们自己的可传递依赖项分组,这些依赖项将受到特定开始节点的影响。

    换句话说,给定依赖关系图中的一个节点,找到一组直接依赖项,这些直接依赖项具有从特定起始节点派生的公共依赖项,可传递地具有这些公共依赖项。

    例如,给定伪代码:

    设B=2
    设c= a+b
    设d=a+b
    让E = A
    设f=a+e
    设G=C+D
    < /代码> 
    
    

    你可以计算这个图表:

    如果我们使用a作为开始节点,我们可以看到a、bothc和d的从属节点的从属节点have a dependent ofg。和fhas a dependent ofeanda

    请注意,ab根本没有影响,因此在决定如何对a的从属项进行分组时,不应考虑到这一点。

    使用a作为开始节点,我们希望获得这组从属项:

    groups=c,d,e,f
    < /代码> 
    
    

    canddhave direct or transitive downstream relations,andeandftogether as well.但是,例如,eandfhave no dependent(downstream)relation at all withcordeither directly or indicately(transitively).并且bdoes not derived fromadirectly or indicately,so it should not have any impact on decising our grouping.

    还要记住,为了简单起见,这个图很小。可能是子图下面的传递依赖项比本例所具有的要远得多。


    我做了大量的论文研究,确实有很多解决方案,但是它们没有我想要的性能特征。随着时间的推移,图形会逐步创建,在每个阶段,我都需要能够回答这个问题,因此每次遍历整个图形都是一种释放机制。

    我认为我有一个主要的优势,在我能找到的各种方法中没有被引用:我完全控制了图的创建,并且依赖项是以逆拓扑顺序添加的,所以图是正确排序的。考虑到这一点,我考虑了递增计算答案的显而易见的解决方案(动态编程)。

    我认为位掩码是存储和查找给定节点所依赖项的快速方法。将依赖项添加到节点时,我将更新该节点的掩码以包括该依赖项的位(它本身包括其依赖项等)。

    let maskcounter=0;
    
    类节点{
    施工单位(名称){
    this.name=名称;
    this.dependents=[];
    this.mask=1<<maskCounter;
    幻灯片计数器+;
    }
    
    添加依赖项(依赖项){
    //现在我们的掩码包含表示所有
    //它的直接和可传递依赖项
    this.mask=this.mask_dependent.mask;
    
    //需要查看此依赖项是否具有可传递的
    //依赖于存在于其中一个组中的自身
    for(此.dependents的const组){
    const result=group.mask&dependent.mask;
    
    如果(结果){
    group.mask=从属.mask;
    group.values.push(依赖);
    返回;
    }
    }
    
    //如果达到,则此依赖项没有可传递的依赖项
    //与此节点的任何其他从属节点一起使用。
    很困惑,嗯?
    这个。家属。推({
    mask:dependent.mask,
    值:【依赖】
    (});
    }
    }
    < /代码> 
    
    

    然而,依赖项将需要按与图形相反的顺序添加,以便图形正确排序,并且图形顶部包含其所有依赖项的遮罩。

    const a=new node('a');
    const b=新节点(“b”);
    const c=新节点(“c”);
    const d=新节点(“d”);
    const e=新节点(“e”);
    const f=新节点(“f”);
    const g=新节点(“g”);
    
    b.附加依赖项(c);
    b.附加依赖项(d);
    c.加依赖(g);
    d.加依赖(g);
    e.添加依赖项(f);
    a.附加依赖项(c);
    a.添加依赖项(d);
    a.附加依赖项(e);
    a.添加依赖项(f);
    < /代码> 
    
    

    位掩码将递增如下所示:

    b=b 000000 10_c 00000100
    B=B 00000110_D 000001000
    C=C 00000100 G 01000000
    D=D 00001000_G 01000000
    
    
    
    
    
    
    

    这是可行的,CPU方面是可以接受的,因为我经常需要知道分组的从属项,但从属项更改的频率要少得多。

    上面的示例使用javascript简化演示,它使用32位有符号整数进行位操作,因此我们只能创建31个唯一的节点。我们可以使用一个任意精度整数(例如bigint)来创建“unlimited”数量的节点,但是问题是内存使用。

    因为每个节点都需要自己独特的翻转位,所以ithinkmemory usage is:。

    n*(n+1)/2=bits(其中n=number of nodes)
    
    例如,10000个节点约为6.25兆字节!
    < /代码> 
    
    

    这不包括任何使用任意精度整数(或类似的自定义数据结构)的平台开销。

    在我的用例中,通常会有10000+的内存,事实上,在某些情况下,100K+是可能的(625 MB!)!!)而且,使用无限量的内存也可以无限期地创建新节点,因此该解决方案不实用,因为“垃圾收集”不再使用节点的屏蔽位,而这些屏蔽位会从图形中删除——这当然是可能的,但这是我希望避免的传统GC问题(如果可能)。

    旁注:根据图表的大小和深度,这实际上也可能表现不好。即使按位运算本身速度相对较快,但对图顶部的每个节点执行100000位的bigint操作也不是很快。因此,完全重新考虑我的方法也是受欢迎的。


    最终,将内存换成CPU是一种常见的权宜之计,但我想知道,我是否缺少另一种平衡更好的方法,或者需要更少的内存?

    可能还有其他一些我没有想到的独特考虑因素可以使用。

    教训我![答:]会受到特定开始节点的影响。

    换句话说,给定依赖关系图中的一个节点,找到一组直接依赖项,这些直接依赖项具有从特定起始节点派生的公共依赖项。

    例如,给出了伪代码:

    let a = 1
    let b = 2
    let c = a + b
    let d = a + b
    let e = a
    let f = a + e
    let g = c + d
    

    你可以计算这个图表:

    Graph diagram

    如果我们使用a作为起始节点,我们可以看到,两者cd依赖于g. 和f依赖于e.

    注意到对…没有影响b因此,在决定如何对其家属进行分组时,不应考虑.

    使用作为开始节点,我们希望得到这些分组的从属项集:

    groups = {{c, d}, {e, f}}
    

    CD具有直接或可传递的下游关系,以及ef一起。但是,例如,ef与…完全没有依赖关系CD直接或间接(传递)。和B不是从直接或间接地,所以它不应该对决定我们的分组有任何影响。

    还要记住,为了简单起见,这个图很小。它可能是可传递依赖项在子图中发生得比本例发生得更远。


    我做了大量的论文研究,确实有很多解决方案,但是它们没有我想要的性能特征。这个图是随着时间的推移逐步创建的,在每个阶段,我都需要能够回答这个问题,所以每次遍历整个图都是一个释放器。

    我认为我有一个主要的优势,在我能找到的各种方法中没有被引用:我完全控制了图的创建,并且依赖项是以逆拓扑顺序添加的,所以图是正确排序的。考虑到这一点,我考虑了递增计算答案的显而易见的解决方案(动态编程)。

    我认为位掩码是存储和查找给定节点所依赖项的快速方法。将依赖项添加到节点时,我将更新该节点的掩码以包括该依赖项的位(它本身包括其依赖项等)

    let maskCounter = 0;
    
    class Node {
      constructor(name) {
        this.name = name;
        this.dependents = [];
        this.mask = 1 << maskCounter;
        maskCounter++;
      }
    
      addDependent(dependent) {
        // Now our mask contains the bits representing all of
        // its direct and transitive dependents
        this.mask = this.mask | dependent.mask;
    
        // Need to see if this dependent has a transitive
        // dependent of its own that exists in one of the groups
        for (const group of this.dependents) {
          const result = group.mask & dependent.mask;
    
          if (result) {
            group.mask |= dependent.mask;
            group.values.push(dependent);
            return;
          }
        }
    
        // If reached, this dependent has no transitive dependents
        // of its own with any of this node's other dependents.
        // That's confusing, huh?
        this.dependents.push({
          mask: dependent.mask,
          values: [dependent]
        });
      }
    }
    

    然而,依赖项将需要按与图形相反的顺序添加,以便图形正确排序,并且图形顶部包含其所有依赖项的遮罩。

    const a = new Node('a');
    const b = new Node('b');
    const c = new Node('c');
    const d = new Node('d');
    const e = new Node('e');
    const f = new Node('f');
    const g = new Node('g');
    
    b.addDependent(c);
    b.addDependent(d);
    c.addDependent(g);
    d.addDependent(g);
    e.addDependent(f);
    a.addDependent(c);
    a.addDependent(d);
    a.addDependent(e);
    a.addDependent(f);
    

    位掩码将逐渐如下所示:

    b = b 00000010 | c 00000100
    b = b 00000110 | d 00001000
    c = c 00000100 | g 01000000
    d = d 00001000 | g 01000000
    e = e 00010000 | f 00100000
    a = a 00000001 | c 01000100
    a = a 01000101 | d 01001000
    a = a 01001101 | e 00110000
    a = a 01111101 | f 00100000
    ===========================
    a = 01111101
    

    最后有面具01111101,每个位表示其下游传递依赖项。注意,第二个到最后一个位没有翻转,这是位不依赖于完全。

    如果我们看看a.dependents我们看到:

    [
      { values: [c, d], mask: 0b00110000 },
      { values: [e, f], mask: 0b01001100 }
    ]
    

    它提供了我们正在寻找的答案,最终是一组集合。a.dependents.map(group => group.values)--这是一个数组,也叫列表,但为了简单起见,它被用作一个集合。

    这里有一个JSBin:https://jsbin.com/jexofip/edit?js,console

    这是可行的,CPU方面是可以接受的,因为我经常需要知道分组的依赖项,但依赖项更改的频率要少得多。

    上面的示例使用javascript简化演示,它使用32位有符号整数进行位操作,因此我们只能创建31个唯一的节点。我们可以使用任意精度整数(例如BigInt)创建“无限”数量的节点,但是问题是内存使用。

    因为每个节点都需要自己独特的翻转位,认为内存使用量为:

    N * (N + 1) / 2 = bits      (where N = number of nodes)
    
    e.g. 10,000 nodes is about 6.25 megabytes!
    

    这不包括使用任意精度整数(或类似的自定义数据结构)的任何平台开销。

    在我的用例中,通常会有10000+的内存,事实上,在某些情况下,100K+是可能的(625 MB!)!!!)而且,使用无限量的内存也可以无限期地创建新节点,所以这个解决方案是不实际的,因为“垃圾收集”不再是简单的方法,不再使用从图中删除的节点的屏蔽位——这当然是可能的,但这是我希望尽可能避免的传统GC问题。

    旁注:根据图表的大小和深度,这实际上也可能表现不好。即使按位运算本身速度相对较快,但对图顶部的每个节点执行100000位的bigint操作也不是很快。因此,完全重新考虑我的方法也是受欢迎的。


    最终,将内存换成CPU是一种常见的权宜之计,但我想知道,我是否缺少另一种平衡更好的方法,或者需要更少的内存?

    可能还有其他一些我没有想到的独特的考虑可以使用。

    学我吧!

    3 回复  |  直到 7 年前
        1
  •  1
  •   Ashley    7 年前

    将每个节点的“可到达”节点存储为一个位掩码,并执行一个按位的、当然听起来很难在计算上击败的操作。如果这方面的主要问题是高内存使用率,那么这可能被视为内存压缩问题。

    如果位掩码非常稀疏(很多是零),它们有可能压缩到更小的大小。

    我想您会想要找到一个压缩库,它可以将位掩码作为流进行反压缩。这样您就可以执行按位和按位解压缩,从而避免存储完全解压缩的位掩码。

        2
  •  3
  •   abacabadabacaba    7 年前

    您要分组依据的关系不是等价关系。例如,考虑这个依赖关系图:

    这里, b and c have a common dependent,and so do c and d ,but there are no common dependent between b and d 。在这种情况下,您可能希望在同一个组中具有 b、 c和 d。然而,这件事变得更加棘手:

    这里, a 不依赖于 c ,因此您可能希望在单独的组中拥有 b d ,现在您不需要关心 c 。但是,有一类算法将分组 b和 d together在这种情况下:algorithms which maintain grouping of all node s,and use this a s a b a s is for grouping new node's direct descendants.

    其中一种算法使用a disjoint-set-structure 有效跟踪连接的节点。在我的示例中,在处理 A 之前,算法将具有节点 B , C , D , E ,and F all in the same set,so they will b e grouped together.

    下面是一个实现:

    函数查找(节点){
    返回node.parent==null?node:(node.parent=查找(node.parent));
    }
    
    功能合并(A,B){
    发现(a);
    b =查找(b);
    如果(A.rank<B.rank){
    A=B=B;
    }否则{
    B.父=A;
    如果(a.rank==b.rank){
    ++A级;
    }
    }
    }
    
    类节点{
    构造函数(名称、从属项){
    this.name=名称;
    this.parent=空;
    这个等级=0;
    让depmap=new map();
    为(让D的家属){
    设dset=find(d);
    如果(!depmap.has(dset))。{
    depmap.set(dset,[]);
    }
    获取(dset).push(d);
    }
    output+=name+':'+array.from(depmap.values()).map(a=>''+a.join(',')+'').join(',')+'\n';
    for(让d of depmap.keys()){
    //或:for(让d of dependents){
    合并(这个,D);
    }
    }
    
    toSTRIN(){
    返回this.name;
    }
    }
    
    让输出='';
    const f=新节点(“f”,[]);
    const e=新节点(“e”,[f]);
    const d=新节点(“d”,[]);
    const c=新节点(“c”,[d]);
    const b=新节点(“b”,[d]);
    const a=新节点(“a”,[b,c,e,f]);
    document.getElementByID('output').textContent=output;>
    
    . 例如,考虑这个依赖关系图:

    a->bcd, bc->e, cd->f

    在这里, C 有一个共同的依赖者,也是 C D ,但两者之间没有共同的依赖关系 D . 在这种情况下,你可能想 B , C D 在同一组中。但是,这种情况会更加棘手:

    a->bd, bc->e, cd->f

    在这里, 不依赖 C ,所以你可能想要 B D 在不同的群体中,既然你不需要关心 C . 然而,有一类算法将 D 在本例中,一起使用:保持分组的算法 全部的 节点,并将其用作分组新节点的直接后代的基础。

    其中一种算法使用 disjoint-set structure 有效跟踪连接的节点。在我的示例中,在处理之前 ,该算法将具有节点 , C , D , e ,和 f 全部在同一组中,因此它们将被分组在一起。

    以下是一个实现:

    function find(node) {
      return node.parent == null ? node : (node.parent = find(node.parent));
    }
    
    function merge(a, b) {
      a = find(a);
      b = find(b);
      if (a.rank < b.rank) {
        a.parent = b;
      } else {
        b.parent = a;
        if (a.rank == b.rank) {
          ++a.rank;
        }
      }
    }
    
    class Node {
      constructor(name, dependents) {
        this.name = name;
        this.parent = null;
        this.rank = 0;
        let depMap = new Map();
        for (let d of dependents) {
          let dset = find(d);
          if (!depMap.has(dset)) {
            depMap.set(dset, []);
          }
          depMap.get(dset).push(d);
        }
        output += name + ': ' + Array.from(depMap.values()).map(a => '{' + a.join(', ') + '}').join(', ') + '\n';
        for (let d of depMap.keys()) {
        // or: for (let d of dependents) {
          merge(this, d);
        }
      }
    
      toString() {
        return this.name;
      }
    }
    
    let output = '';
    const f = new Node('f', []);
    const e = new Node('e', [f]);
    const d = new Node('d', []);
    const c = new Node('c', [d]);
    const b = new Node('b', [d]);
    const a = new Node('a', [b, c, e, f]);
    document.getElementById('output').textContent = output;
    <pre id=output></pre>
        3
  •  1
  •   Robert Monfera    7 年前

    如果是有向非循环图,可以执行 topological sorting 对于节点,这似乎是后续步骤的良好基础。拓扑排序本身可以有效地完成。在FRP启发的库中有一些实现,例如 crosslink 或帕德平德的 flyd

    此外,检查 this answer .

    推荐文章