Мы можем немного его укоротить:
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']
})
Теперь это не самое эффективное, так как мы:
- находим по пути, который является массивом, а не множеством (то же самое O (k ) вместо O (1))
- пересмотреть вершины, даже если они уже были посещены
Ниже немного более оптимизированная версия для удобства чтения?
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']
})
Что касается перфектов, я (дешево) пытался воспроизвести и сравнить алгоритмы на одном и том же графе G (сгенерированном методом random-dag) с 50 узлов.
Они кажутся эквивалентными.
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')
}