简体   繁体   English

检测有向图中的循环

[英]Detect cycle in directed Graph

Recently had to detect recursions in a directed graph in code logic.最近不得不在代码逻辑中检测有向图中的递归。 My nodejs implementation feels complex and I am now wondering:我的 nodejs 实现感觉很复杂,我现在想知道:

  • Are there any problems with the code?代码有问题吗?
  • Can we simplify it / make it more readable?我们可以简化它/使其更具可读性吗?
const checkCyclic = (graph) => {
  const nodes = new Set(Object.keys(graph));
  const searchCycle = (trace, node) => {
    const cycleStartIdx = trace.indexOf(node);
    if (cycleStartIdx !== -1) {
      throw new Error(`Cycle detected: ${trace
        .slice(cycleStartIdx).concat(node).join(' <- ')}`);
    }
    if (nodes.delete(node) === true) {
      const nextTrace = trace.concat(node);
      graph[node].forEach((nextNode) => searchCycle(nextTrace, nextNode));
    }
  };
  while (nodes.size !== 0) {
    searchCycle([], nodes.values().next().value);
  }
};

checkCyclic({
  p1: ['p3'],
  p2: ['p1'],
  p3: ['p2']
});
// => Recursion detected: p1 <- p3 <- p2 <- p1

checkCyclic({
  p0: ['p1'],
  p1: ['p3'],
  p2: ['p1'],
  p3: ['p2']
});
// => Recursion detected: p1 <- p3 <- p2 <- p1

checkCyclic({
  p0: ['p0']
});
// => Cycle detected: p0 <- p0

For the curious, this is being used in promise-pool-ext , which also contains tests.出于好奇,这被用于promise-pool-ext ,它也包含测试。

Thank you very much for your feedback!非常感谢您的反馈!


Edit: Played around and did the iterative implementation (looks even uglier!)编辑:玩弄并进行了迭代实现(看起来更丑!)

module.exports = (G) => {
  const pending = new Set(Object.keys(G));
  while (pending.size !== 0) {
    const trace = [pending.values().next().value];
    const parentIdx = [0];
    pending.delete(trace[0]);

    while (trace.length !== 0) {
      const c = trace.length - 1;
      const parent = G[trace[c]][parentIdx[c]];
      if (parent !== undefined) {
        if (trace.includes(parent)) {
          throw new Error(`Cycle detected: ${trace
            .slice(trace.indexOf(parent)).concat(parent).join(' <- ')}`);
        }

        parentIdx[c] += 1;
        if (pending.delete(parent)) {
          trace.push(parent);
          parentIdx.push(0);
        }
      } else {
        trace.pop();
        parentIdx.pop();
      }
    }
  }
};

I usually prefer iterative to recursive, but in this case it might not worth the readability trade-off.我通常更喜欢迭代而不是递归,但在这种情况下,可能不值得进行可读性权衡。 Any idea how to improve this implementation?知道如何改进这个实现吗?

We may shorten it a bit:我们可以稍微缩短一下:

 function getCycle (G, n, path) { if (path.includes(n)) { throw `cycle ${path.slice(path.indexOf(n)).concat(n).join('<-')}` } path.push(n) return G[n].forEach(next => getCycle(G, next, path.slice(0))) } function validate (G) { Object.keys(G).forEach(n => getCycle(G, n, [])) } validate({ p1:['p2','p3','p4'], p2:['p3'], p3:['p0'], p0:[], p4:[] }) console.log('ok') validate({ p1:['p2','p3','p4'], p2:['p3'], p3:['p0'], p0:[], p4:['p1'] })

Now this is not the most efficient since we:现在这不是最有效的,因为我们:

  • find on path which is an array and not a set (idem O(k) instead of O(1))在是数组而不是集合的路径上查找(同上 O(k) 而不是 O(1))
  • do revisit vertices even though they have already been visited重新访问顶点,即使它们已经被访问过

Below a slightly more optimized version at the sake of readability?为了可读性,低于稍微优化的版本?

 function getCycle (G, n, path, visited) { if (path.has(n)) { const v = [...path] throw `cycle ${v.slice(v.indexOf(n)).concat(n).join('<-')}` } visited.add(n) path.add(n) return G[n].forEach(next => getCycle(G, next, new Set(path), visited)) } function validate (G) { const visited = new Set() Object.keys(G).forEach(n => { if (visited.has(n)) return getCycle(G, n, new Set(), visited) }) } validate({ p1:['p2','p3','p4'], p2:['p3'], p3:['p0'], p0:[], p4:[] }) console.log('ok') validate({ p1:['p2','p3','p4'], p2:['p3'], p3:['p0'], p0:[], p4:['p1'] })


Regarding perfs, I have (cheaply) tried to reproduce and compare algos on the same graph G (generated by random-dag) with 50 nodes.关于性能,我(廉价地)尝试在具有50 个节点的同一图 G(由随机 dag 生成)上重现和比较算法。

They seem to be equivalent.他们似乎是等价的。

 function checkCyclic (G) { const pending = new Set(Object.keys(G)); while (pending.size !== 0) { const trace = [pending.values().next().value]; const parentIdx = [0]; pending.delete(trace[0]); while (trace.length !== 0) { const lastIdx = trace.length - 1; const parent = G[trace[lastIdx]][parentIdx[lastIdx]]; if (parent === undefined) { trace.pop(); parentIdx.pop(); } else { if (trace.includes(parent)) { throw new Error(`cycle ${trace .slice(trace.indexOf(parent)).concat(parent).join('<-')}`); } parentIdx[lastIdx] += 1; if (pending.delete(parent)) { trace.push(parent); parentIdx.push(0); } } } } }; function grodzi1(G) { function getCycle (G, n, path) { if (path.includes(n)) { throw `cycle ${path.slice(path.indexOf(n)).concat(n).join('<-')}` } path.push(n) return G[n].forEach(next => getCycle(G, next, path.slice(0))) } Object.keys(G).forEach(n => getCycle(G, n, [])) } function grodzi2(G) { function getCycle (G, n, path, visited) { if (path.has(n)) { const v = [...path] throw `cycle ${v.slice(v.indexOf(n)).concat(n).join('<-')}` } visited.add(n) path.add(n) return G[n].forEach(next => getCycle(G, next, new Set(path), visited)) } const visited = new Set() Object.keys(G).forEach(n => { if (visited.has(n)) return getCycle(G, n, new Set(), visited) }) } // avoid copying the set function grodziNoCopy(G) { function getCycle (G, n, path, visited) { if (path.has(n)) { const v = [...path] throw `cycle ${v.slice(v.indexOf(n)).concat(n).join('<-')}` } visited.add(n) path.add(n) return G[n].forEach(next => { getCycle(G, next, path, visited) path.delete(next) }) } const visited = new Set() Object.keys(G).forEach(n => { if (visited.has(n)) return getCycle(G, n, new Set(), visited) }) } // avoid visiting the already visited set of nodes function grodziStopVisit(G) { function getCycle (G, n, path, visited) { if (path.has(n)) { const v = [...path] throw `cycle ${v.slice(v.indexOf(n)).concat(n).join('<-')}` } if (visited.has(n)) return visited.add(n) path.add(n) return G[n].forEach(next => { getCycle(G, next, path, visited) path.delete(next) }) } const visited = new Set() Object.keys(G).forEach(n => { if (visited.has(n)) return getCycle(G, n, new Set(), visited) }) } // same but iterative function grodziIter(G) { function dfs (G, n, visited) { let stack = [{ path: [], n }] let x while (x = stack.pop()) { const {n, path} = x if (path.includes(n)) { const v = [...path] throw `cycle ${v.slice(v.indexOf(n)).concat(n).join('<-')}` } if (visited.has(n)) continue visited.add(n) path.push(n) G[n].forEach(next => stack.push({ path: path.slice(0), n: next })) } } const visited = new Set() Object.keys(G).forEach(n => visited.has(n) || dfs(G, n, visited)) } const G = {"0":["5","6","12","15","18","30","31","32","33","35","39","41","52","54"],"1":["12","17","29","30","34","35","38","39","40","43","53"],"2":["5","7","12","13","14","15","16","19","21","31","35","36","37","40","41","53"],"3":["14","16","15","30","32","40","52","55"],"4":["5","6","13","15","17","18","32","35","40","41","42","51"],"5":["16","15","30","33","52","53","55"],"6":["11","16","18","33","36","37","42","51","53"],"7":["14","15","16","22","30","33","35","36","39","41","43","49","53","54","55"],"8":["31","36","41","51"],"9":["18","30","36","37","39","40","50","52"],"10":["15","17","18","19","31","32","33","35","37","40","41","48","54","55"],"11":["15","17","19","31","32","35","38","41","40","43","48","52"],"12":["17","21","32","33","35","52","54","55"],"13":["18","19","20","29","33","35","36","38","41","43","52"],"14":["16","17","19","35","39","55"],"15":["20","22","30","33","35","38","39","41","42","43","49","50","54"],"16":["20","32","34","36","37","39","40","42","44","53"],"17":["28","31","36","35","38","41","43","44","48"],"18":["19","31","34","36","35","38","41","49","52","53","55"],"19":["29","36","48","51"],"20":["29","32","33","36","37","49"],"21":["30","31","33","34","35","36","39","48"],"22":["30","31","32","34","36","37","41","43","48"],"23":["33","34","35","36","37","40","44","50"],"24":["28","34","35","36","38","41","42","48","52"],"25":["28","29","31","32","36","41","43","53"],"26":["29","35","37","38","39","41","43","50"],"27":["31","35","36","37","41","42","48","51","53"],"28":["35","37","38","40","41","50","55"],"29":["38","39","40","42","44","51","54"],"30":["37","38","40","41","42","43","49","50","53"],"31":["36","39","40","50","52","54"],"32":["37","38","39","41","44","48","49","52","55"],"33":["41","40","42","44","52","53"],"34":["35","36","41","42","49","52","54"],"35":["44","55"],"36":["41","50","52","53","54"],"37":["52","55"],"38":["55"],"39":["40","41","51"],"40":["48","49","52"],"41":["49","52","53"],"42":["53"],"43":["48","50","52","55"],"44":["48","52","54"],"45":["49","53","54"],"46":["49","50","52"],"47":["48","50","52","53","55"],"48":[],"49":[],"50":[],"51":[],"52":[],"53":[],"54":[],"55":[]} function bench (fn, label) { console.time(label) for (let idx = 0; idx < 50; idx += 1) { fn(G) } console.timeEnd(label) } function shouldThrow (...fns) { const cyc = {"p1":["p2","p3","p4"],"p2":["p3"],"p3":["p0"],"p0":[],"p4":["p1"]} fns.forEach(fn => { let ok = false try { fn(cyc) } catch (e) { ok = e.toString().includes('cycle p1<-p4<-p1') if(!ok){ throw new Error('failzed ', e) } } if (!ok){ throw 'should have thrown' } }) } shouldThrow(checkCyclic, grodzi1, grodzi2, grodziNoCopy, grodziStopVisit, grodziIter) for(let i = 0; i < 3; ++i) { bench(checkCyclic, 'cyclic') bench(grodzi1, 'grodzi1') bench(grodzi2, 'grodzi2') bench(grodziNoCopy, 'grodziNoCopy') bench(grodziStopVisit, 'grodziStopVisit') bench(grodziIter, 'grodziIter') console.log('next') }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM