You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
12 KiB
339 lines
12 KiB
|
4 months ago
|
import dfs from './dfs';
|
||
|
|
import getConnectedComponents, { detectStrongConnectComponents } from './connected-component';
|
||
|
|
import { getNeighbors } from './util';
|
||
|
|
var detectDirectedCycle = function detectDirectedCycle(graphData) {
|
||
|
|
var cycle = null;
|
||
|
|
var _a = graphData.nodes,
|
||
|
|
nodes = _a === void 0 ? [] : _a;
|
||
|
|
var dfsParentMap = {};
|
||
|
|
// 所有没有被访问的节点集合
|
||
|
|
var unvisitedSet = {};
|
||
|
|
// 正在被访问的节点集合
|
||
|
|
var visitingSet = {};
|
||
|
|
// 所有已经被访问过的节点集合
|
||
|
|
var visitedSet = {};
|
||
|
|
// 初始化 unvisitedSet
|
||
|
|
nodes.forEach(function (node) {
|
||
|
|
unvisitedSet[node.id] = node;
|
||
|
|
});
|
||
|
|
var callbacks = {
|
||
|
|
enter: function enter(_a) {
|
||
|
|
var currentNode = _a.current,
|
||
|
|
previousNode = _a.previous;
|
||
|
|
if (visitingSet[currentNode]) {
|
||
|
|
// 如果当前节点正在访问中,则说明检测到环路了
|
||
|
|
cycle = {};
|
||
|
|
var currentCycleNode = currentNode;
|
||
|
|
var previousCycleNode = previousNode;
|
||
|
|
while (previousCycleNode !== currentNode) {
|
||
|
|
cycle[currentCycleNode] = previousCycleNode;
|
||
|
|
currentCycleNode = previousCycleNode;
|
||
|
|
previousCycleNode = dfsParentMap[previousCycleNode];
|
||
|
|
}
|
||
|
|
cycle[currentCycleNode] = previousCycleNode;
|
||
|
|
} else {
|
||
|
|
// 如果不存在正在访问集合中,则将其放入正在访问集合,并从未访问集合中删除
|
||
|
|
visitingSet[currentNode] = currentNode;
|
||
|
|
delete unvisitedSet[currentNode];
|
||
|
|
// 更新 DSF parents 列表
|
||
|
|
dfsParentMap[currentNode] = previousNode;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
leave: function leave(_a) {
|
||
|
|
var currentNode = _a.current;
|
||
|
|
// 如果所有的节点的子节点都已经访问过了,则从正在访问集合中删除掉,并将其移入到已访问集合中,
|
||
|
|
// 同时也意味着当前节点的所有邻居节点都被访问过了
|
||
|
|
visitedSet[currentNode] = currentNode;
|
||
|
|
delete visitingSet[currentNode];
|
||
|
|
},
|
||
|
|
allowTraversal: function allowTraversal(_a) {
|
||
|
|
var nextNode = _a.next;
|
||
|
|
// 如果检测到环路则需要终止所有进一步的遍历,否则会导致无限循环遍历
|
||
|
|
if (cycle) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
// 仅允许遍历没有访问的节点,visitedSet 中的都已经访问过了
|
||
|
|
return !visitedSet[nextNode];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
// 开始遍历节点
|
||
|
|
while (Object.keys(unvisitedSet).length) {
|
||
|
|
// 从第一个节点开始进行 DFS 遍历
|
||
|
|
var firsetUnVisitedKey = Object.keys(unvisitedSet)[0];
|
||
|
|
dfs(graphData, firsetUnVisitedKey, callbacks);
|
||
|
|
}
|
||
|
|
return cycle;
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* 检测无向图中的所有Base cycles
|
||
|
|
* refer: https://www.codeproject.com/Articles/1158232/Enumerating-All-Cycles-in-an-Undirected-Graph
|
||
|
|
* @param graph
|
||
|
|
* @param nodeIds 节点 ID 的数组
|
||
|
|
* @param include 包含或排除指定的节点
|
||
|
|
* @return [{[key: string]: INode}] 返回一组base cycles
|
||
|
|
*/
|
||
|
|
export var detectAllUndirectedCycle = function detectAllUndirectedCycle(graphData, nodeIds, include) {
|
||
|
|
var _a, _b;
|
||
|
|
if (include === void 0) {
|
||
|
|
include = true;
|
||
|
|
}
|
||
|
|
var allCycles = [];
|
||
|
|
var components = getConnectedComponents(graphData, false);
|
||
|
|
// loop through all connected components
|
||
|
|
for (var _i = 0, components_1 = components; _i < components_1.length; _i++) {
|
||
|
|
var component = components_1[_i];
|
||
|
|
if (!component.length) continue;
|
||
|
|
var root = component[0];
|
||
|
|
var rootId = root.id;
|
||
|
|
var stack = [root];
|
||
|
|
var parent_1 = (_a = {}, _a[rootId] = root, _a);
|
||
|
|
var used = (_b = {}, _b[rootId] = new Set(), _b);
|
||
|
|
// walk a spanning tree to find cycles
|
||
|
|
while (stack.length > 0) {
|
||
|
|
var curNode = stack.pop();
|
||
|
|
var curNodeId = curNode.id;
|
||
|
|
var neighbors = getNeighbors(curNodeId, graphData.edges);
|
||
|
|
var _loop_1 = function _loop_1(i) {
|
||
|
|
var _c;
|
||
|
|
var neighborId = neighbors[i];
|
||
|
|
var neighbor = graphData.nodes.find(function (node) {
|
||
|
|
return node.id === neighborId;
|
||
|
|
});
|
||
|
|
// const neighborId = neighbor.get('id');
|
||
|
|
if (neighborId === curNodeId) {
|
||
|
|
// 自环
|
||
|
|
allCycles.push((_c = {}, _c[neighborId] = curNode, _c));
|
||
|
|
} else if (!(neighborId in used)) {
|
||
|
|
// visit a new node
|
||
|
|
parent_1[neighborId] = curNode;
|
||
|
|
stack.push(neighbor);
|
||
|
|
used[neighborId] = new Set([curNode]);
|
||
|
|
} else if (!used[curNodeId].has(neighbor)) {
|
||
|
|
// a cycle found
|
||
|
|
var cycleValid = true;
|
||
|
|
var cyclePath = [neighbor, curNode];
|
||
|
|
var p = parent_1[curNodeId];
|
||
|
|
while (used[neighborId].size && !used[neighborId].has(p)) {
|
||
|
|
cyclePath.push(p);
|
||
|
|
if (p === parent_1[p.id]) break;else p = parent_1[p.id];
|
||
|
|
}
|
||
|
|
cyclePath.push(p);
|
||
|
|
if (nodeIds && include) {
|
||
|
|
// 如果有指定包含的节点
|
||
|
|
cycleValid = false;
|
||
|
|
if (cyclePath.findIndex(function (node) {
|
||
|
|
return nodeIds.indexOf(node.id) > -1;
|
||
|
|
}) > -1) {
|
||
|
|
cycleValid = true;
|
||
|
|
}
|
||
|
|
} else if (nodeIds && !include) {
|
||
|
|
// 如果有指定不包含的节点
|
||
|
|
if (cyclePath.findIndex(function (node) {
|
||
|
|
return nodeIds.indexOf(node.id) > -1;
|
||
|
|
}) > -1) {
|
||
|
|
cycleValid = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 把 node list 形式转换为 cycle 的格式
|
||
|
|
if (cycleValid) {
|
||
|
|
var cycle = {};
|
||
|
|
for (var index = 1; index < cyclePath.length; index += 1) {
|
||
|
|
cycle[cyclePath[index - 1].id] = cyclePath[index];
|
||
|
|
}
|
||
|
|
if (cyclePath.length) {
|
||
|
|
cycle[cyclePath[cyclePath.length - 1].id] = cyclePath[0];
|
||
|
|
}
|
||
|
|
allCycles.push(cycle);
|
||
|
|
}
|
||
|
|
used[neighborId].add(curNode);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
for (var i = 0; i < neighbors.length; i += 1) {
|
||
|
|
_loop_1(i);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return allCycles;
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* Johnson's algorithm, 时间复杂度 O((V + E)(C + 1))$ and space bounded by O(V + E)
|
||
|
|
* refer: https://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF
|
||
|
|
* refer: https://networkx.github.io/documentation/stable/_modules/networkx/algorithms/cycles.html#simple_cycles
|
||
|
|
* @param graph
|
||
|
|
* @param nodeIds 节点 ID 的数组
|
||
|
|
* @param include 包含或排除指定的节点
|
||
|
|
* @return [{[key: string]: INode}] 返回所有的 simple cycles
|
||
|
|
*/
|
||
|
|
export var detectAllDirectedCycle = function detectAllDirectedCycle(graphData, nodeIds, include) {
|
||
|
|
if (include === void 0) {
|
||
|
|
include = true;
|
||
|
|
}
|
||
|
|
var path = []; // stack of nodes in current path
|
||
|
|
var blocked = new Set();
|
||
|
|
var B = []; // remember portions of the graph that yield no elementary circuit
|
||
|
|
var allCycles = [];
|
||
|
|
var idx2Node = {};
|
||
|
|
var node2Idx = {};
|
||
|
|
// 辅助函数: unblock all blocked nodes
|
||
|
|
var unblock = function unblock(thisNode) {
|
||
|
|
var stack = [thisNode];
|
||
|
|
while (stack.length > 0) {
|
||
|
|
var node = stack.pop();
|
||
|
|
if (blocked.has(node)) {
|
||
|
|
blocked.delete(node);
|
||
|
|
B[node.id].forEach(function (n) {
|
||
|
|
stack.push(n);
|
||
|
|
});
|
||
|
|
B[node.id].clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var circuit = function circuit(node, start, adjList) {
|
||
|
|
var closed = false; // whether a path is closed
|
||
|
|
if (nodeIds && include === false && nodeIds.indexOf(node.id) > -1) return closed;
|
||
|
|
path.push(node);
|
||
|
|
blocked.add(node);
|
||
|
|
var neighbors = adjList[node.id];
|
||
|
|
for (var i = 0; i < neighbors.length; i += 1) {
|
||
|
|
var neighbor = idx2Node[neighbors[i]];
|
||
|
|
if (neighbor === start) {
|
||
|
|
var cycle = {};
|
||
|
|
for (var index = 1; index < path.length; index += 1) {
|
||
|
|
cycle[path[index - 1].id] = path[index];
|
||
|
|
}
|
||
|
|
if (path.length) {
|
||
|
|
cycle[path[path.length - 1].id] = path[0];
|
||
|
|
}
|
||
|
|
allCycles.push(cycle);
|
||
|
|
closed = true;
|
||
|
|
} else if (!blocked.has(neighbor)) {
|
||
|
|
if (circuit(neighbor, start, adjList)) {
|
||
|
|
closed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (closed) {
|
||
|
|
unblock(node);
|
||
|
|
} else {
|
||
|
|
for (var i = 0; i < neighbors.length; i += 1) {
|
||
|
|
var neighbor = idx2Node[neighbors[i]];
|
||
|
|
if (!B[neighbor.id].has(node)) {
|
||
|
|
B[neighbor.id].add(node);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
path.pop();
|
||
|
|
return closed;
|
||
|
|
};
|
||
|
|
var _a = graphData.nodes,
|
||
|
|
nodes = _a === void 0 ? [] : _a;
|
||
|
|
// Johnson's algorithm 要求给节点赋顺序,先按节点在数组中的顺序
|
||
|
|
for (var i = 0; i < nodes.length; i += 1) {
|
||
|
|
var node = nodes[i];
|
||
|
|
var nodeId = node.id;
|
||
|
|
node2Idx[nodeId] = i;
|
||
|
|
idx2Node[i] = node;
|
||
|
|
}
|
||
|
|
// 如果有指定包含的节点,则把指定节点排序在前,以便提早结束搜索
|
||
|
|
if (nodeIds && include) {
|
||
|
|
var _loop_2 = function _loop_2(i) {
|
||
|
|
var nodeId = nodeIds[i];
|
||
|
|
node2Idx[nodes[i].id] = node2Idx[nodeId];
|
||
|
|
node2Idx[nodeId] = 0;
|
||
|
|
idx2Node[0] = nodes.find(function (node) {
|
||
|
|
return node.id === nodeId;
|
||
|
|
});
|
||
|
|
idx2Node[node2Idx[nodes[i].id]] = nodes[i];
|
||
|
|
};
|
||
|
|
for (var i = 0; i < nodeIds.length; i++) {
|
||
|
|
_loop_2(i);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 返回 节点顺序 >= nodeOrder 的强连通分量的adjList
|
||
|
|
var getMinComponentAdj = function getMinComponentAdj(components) {
|
||
|
|
var _a;
|
||
|
|
var minCompIdx;
|
||
|
|
var minIdx = Infinity;
|
||
|
|
// Find least component and the lowest node
|
||
|
|
for (var i = 0; i < components.length; i += 1) {
|
||
|
|
var comp = components[i];
|
||
|
|
for (var j = 0; j < comp.length; j++) {
|
||
|
|
var nodeIdx_1 = node2Idx[comp[j].id];
|
||
|
|
if (nodeIdx_1 < minIdx) {
|
||
|
|
minIdx = nodeIdx_1;
|
||
|
|
minCompIdx = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var component = components[minCompIdx];
|
||
|
|
var adjList = [];
|
||
|
|
for (var i = 0; i < component.length; i += 1) {
|
||
|
|
var node = component[i];
|
||
|
|
adjList[node.id] = [];
|
||
|
|
for (var _i = 0, _b = getNeighbors(node.id, graphData.edges, 'target').filter(function (n) {
|
||
|
|
return component.map(function (c) {
|
||
|
|
return c.id;
|
||
|
|
}).indexOf(n) > -1;
|
||
|
|
}); _i < _b.length; _i++) {
|
||
|
|
var neighbor = _b[_i];
|
||
|
|
// 对自环情况 (点连向自身) 特殊处理:记录自环,但不加入adjList
|
||
|
|
if (neighbor === node.id && !(include === false && nodeIds.indexOf(node.id) > -1)) {
|
||
|
|
allCycles.push((_a = {}, _a[node.id] = node, _a));
|
||
|
|
} else {
|
||
|
|
adjList[node.id].push(node2Idx[neighbor]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
component: component,
|
||
|
|
adjList: adjList,
|
||
|
|
minIdx: minIdx
|
||
|
|
};
|
||
|
|
};
|
||
|
|
var nodeIdx = 0;
|
||
|
|
while (nodeIdx < nodes.length) {
|
||
|
|
var subgraphNodes = nodes.filter(function (n) {
|
||
|
|
return node2Idx[n.id] >= nodeIdx;
|
||
|
|
});
|
||
|
|
var sccs = detectStrongConnectComponents({
|
||
|
|
nodes: subgraphNodes,
|
||
|
|
edges: graphData.edges
|
||
|
|
}).filter(function (component) {
|
||
|
|
return component.length > 1;
|
||
|
|
});
|
||
|
|
if (sccs.length === 0) break;
|
||
|
|
var scc = getMinComponentAdj(sccs);
|
||
|
|
var minIdx = scc.minIdx,
|
||
|
|
adjList = scc.adjList,
|
||
|
|
component = scc.component;
|
||
|
|
if (component.length > 1) {
|
||
|
|
component.forEach(function (node) {
|
||
|
|
B[node.id] = new Set();
|
||
|
|
});
|
||
|
|
var startNode = idx2Node[minIdx];
|
||
|
|
// startNode 不在指定要包含的节点中,提前结束搜索
|
||
|
|
if (nodeIds && include && nodeIds.indexOf(startNode.id) === -1) return allCycles;
|
||
|
|
circuit(startNode, startNode, adjList);
|
||
|
|
nodeIdx = minIdx + 1;
|
||
|
|
} else {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return allCycles;
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* 查找图中所有满足要求的圈
|
||
|
|
* @param graph
|
||
|
|
* @param directed 是否为有向图
|
||
|
|
* @param nodeIds 节点 ID 的数组,若不指定,则返回图中所有的圈
|
||
|
|
* @param include 包含或排除指定的节点
|
||
|
|
* @return [{[key: string]: Node}] 包含所有环的数组,每个环用一个Object表示,其中key为节点id,value为该节点在环中指向的下一个节点
|
||
|
|
*/
|
||
|
|
export var detectAllCycles = function detectAllCycles(graphData, directed, nodeIds, include) {
|
||
|
|
if (include === void 0) {
|
||
|
|
include = true;
|
||
|
|
}
|
||
|
|
if (directed) return detectAllDirectedCycle(graphData, nodeIds, include);
|
||
|
|
return detectAllUndirectedCycle(graphData, nodeIds, include);
|
||
|
|
};
|
||
|
|
export default detectDirectedCycle;
|