Skip to content

Commit

Permalink
Patch buildComponent in eles.htbc()
Browse files Browse the repository at this point in the history
- For certain graph structures, it is possible for a single edge (or multiple, counting loops) to be omitted from a component during it's construction. This ensures that components are reconstructed in their entirety.
- Add test case which fails without newest modification
- Additional clean up
- Rename `eles.hopcroftTarjan()` to `eles.htbc()` in documentation

Ref :  #2622
  • Loading branch information
r-ba authored and maxkfranz committed Feb 7, 2020
1 parent e8062ec commit ec05963
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 51 deletions.
2 changes: 1 addition & 1 deletion documentation/md/collection/hopcroftTarjanBiconnected.md
Expand Up @@ -14,7 +14,7 @@ This function returns an object of the following form:
## Examples

```js
var ht = cy.elements().hopcroftTarjan();
var ht = cy.elements().htbc();

ht.components[0].select();
```
70 changes: 30 additions & 40 deletions src/collection/algorithms/hopcroft-tarjan-biconnected.js
@@ -1,19 +1,16 @@
let hopcroftTarjanBiconnected = function() {

let eles = this;
let nodes = {};
let id = 0;
let edgeCount = 0;
let components = [];
let stack = [];
let visitedEdges = {};
let loops = {};

const buildComponent = (x, y) => {
let i = stack.length-1;
let cutset = [];
let visitedNodes = {};
let component = [];
let component = eles.spawn();

while (stack[i].x != x || stack[i].y != y) {
cutset.push(stack.pop().edge);
Expand All @@ -22,88 +19,82 @@ let hopcroftTarjanBiconnected = function() {
cutset.push(stack.pop().edge);

cutset.forEach(edge => {
component.push(edge);
let connectedNodes = edge.connectedNodes().intersection(eles);

let connectedNodes = edge.connectedNodes()
.intersection(eles);
component.merge(edge);
connectedNodes.forEach(node => {
let nodeId = node.id();

if (!(nodeId in visitedNodes)) {
visitedNodes[nodeId] = true;

if (nodeId in loops) {
loops[nodeId].forEach(loop => component.push(loop));
}
component.push(node);
const nodeId = node.id();
const connectedEdges = node.connectedEdges()
.intersection(eles);
component.merge(node);
if (!nodes[nodeId].cutVertex) {
component.merge(connectedEdges);
} else {
component.merge(connectedEdges.filter(edge => edge.isLoop()));
}
});
});

components.push(eles.spawn(component));
components.push(component);
};

const biconnectedSearch = (root, currentNode, parent) => {
if (root == parent) edgeCount += 1;
if (root === parent) edgeCount += 1;
nodes[currentNode] = {
id : id,
low : id++,
cutVertex : false
};
let edges = eles.getElementById(currentNode).connectedEdges().intersection(eles);
let edges = eles.getElementById(currentNode)
.connectedEdges()
.intersection(eles);

if (edges.size() === 0) {
components.push(eles.spawn(eles.getElementById(currentNode)));
} else {
let sourceId, targetId, otherNodeId, edgeId, isEdgeLoop;
let sourceId, targetId, otherNodeId, edgeId;

edges.forEach(edge => {
sourceId = edge.source().id();
targetId = edge.target().id();
otherNodeId = (sourceId == currentNode) ? targetId : sourceId;
isEdgeLoop = edge.isLoop();
otherNodeId = (sourceId === currentNode) ? targetId : sourceId;

if (isEdgeLoop) {
if (sourceId in loops) {
loops.push(edge);
} else {
loops[sourceId] = [edge];
}
} else if (otherNodeId != parent) {
if (otherNodeId !== parent) {
edgeId = edge.id();

if (!visitedEdges[edgeId]) {
visitedEdges[edgeId] = true;
stack.push({
x : currentNode,
y : otherNodeId,
edge : eles.getElementById(edgeId)
edge
});
}

if (!(otherNodeId in nodes)) {
biconnectedSearch(root, otherNodeId, currentNode);
nodes[currentNode].low = Math.min(nodes[currentNode].low, nodes[otherNodeId].low);
nodes[currentNode].low = Math.min(nodes[currentNode].low,
nodes[otherNodeId].low);

if (nodes[currentNode].id <= nodes[otherNodeId].low) {
nodes[currentNode].cutVertex = true;
buildComponent(currentNode, otherNodeId);
}
} else {
nodes[currentNode].low = Math.min(nodes[currentNode].low, nodes[otherNodeId].id);
nodes[currentNode].low = Math.min(nodes[currentNode].low,
nodes[otherNodeId].id);
}
}
});
}
};

eles.forEach(ele => {

if (ele.isNode()) {
let nodeId = ele.id();

if (!(nodeId in nodes)) {
edgeCount = 0;
biconnectedSearch(nodeId, nodeId, "");
biconnectedSearch(nodeId, nodeId);
nodes[nodeId].cutVertex = (edgeCount > 1);
}
}
Expand All @@ -115,14 +106,13 @@ let hopcroftTarjanBiconnected = function() {

return {
cut: eles.spawn(cutVertices),
components: components
components
};

};

export default {
hopcroftTarjanBiconnected,
htbc: hopcroftTarjanBiconnected,
export default {
hopcroftTarjanBiconnected,
htbc: hopcroftTarjanBiconnected,
htb: hopcroftTarjanBiconnected,
hopcroftTarjanBiconnectedComponents: hopcroftTarjanBiconnected
};
40 changes: 30 additions & 10 deletions test/collection-hopcroft-tarjan-biconnected.js
Expand Up @@ -4,7 +4,7 @@ var cytoscape = require('../src/test.js', cytoscape);
describe('Algorithms', function(){
describe('eles.hopcroftTarjanBiconnected()', function(){

var cy0, cy1;
var cy0, cy1, cy2;

beforeEach(function(done) {
cytoscape({
Expand Down Expand Up @@ -34,7 +34,11 @@ describe('Algorithms', function(){
{ data: { id: '1-15' } },
{ data: { id: '1-16' } },
{ data: { id: '1-17' } },
{ data: { id: '1-18' } }
{ data: { id: '1-18' } },

{ data: { id: '2-0' } },
{ data: { id: '2-1' } },
{ data: { id: '2-2' } },
],

edges: [
Expand Down Expand Up @@ -71,13 +75,21 @@ describe('Algorithms', function(){
{ data: { source: '1-12', target: '1-13', id: '1-41' } },
{ data: { source: '1-13', target: '1-14', id: '1-42' } },
{ data: { source: '1-13', target: '1-15', id: '1-43' } },
{ data: { source: '1-17', target: '1-18', id: '1-44' } }
{ data: { source: '1-17', target: '1-18', id: '1-44' } },

{ data: { id: '2-3', source: '2-2', target: '2-1' } },
{ data: { id: '2-4', source: '2-1', target: '2-0' } },
{ data: { id: '2-5', source: '2-1', target: '2-1' } },
{ data: { id: '2-6', source: '2-0', target: '2-1' } },
{ data: { id: '2-7', source: '2-0', target: '2-2' } },
{ data: { id: '2-8', source: '2-0', target: '2-0' } }
]
},

ready: function(){
cy0 = this.filter(ele => ele.id()[0] == "0");
cy1 = this.filter(ele => ele.id()[0] == "1");
cy2 = this.filter(ele => ele.id()[0] == "2");
done();
}
});
Expand All @@ -88,10 +100,18 @@ describe('Algorithms', function(){
}

it('eles.htbc(): no cut vertices, one biconnected component', function(){
var res = cy0.htbc();
expect( res.cut.map( ele2id ) ).to.deep.equal( [] );
expect( res.components.length ).to.equal( 1 );
expect( res.components[0].map( ele2id ) ).to.deep.equal( [ "0-9", "0-11", "0-4", "0-3", "0-10", "0-0", "0-8", "0-7", "0-2", "0-6", "0-1", "0-5" ] );
var res0 = cy0.htbc();
var res1 = cy2.htbc();
expect( res0.cut.map( ele2id ) ).to.deep.equal( [] );
expect( res0.components.length ).to.equal( 1 );
expect( res0.components[0].length ).to.equal( cy0.length );
expect( res0.components[0].map( ele2id ) ).to.deep.equal( [ "0-9", "0-4", "0-8", "0-10", "0-11", "0-3", "0-7", "0-0", "0-2", "0-6", "0-1", "0-5"] );

expect( res1.cut.map( ele2id ) ).to.deep.equal( [] );
expect( res1.components.length ).to.equal( 1 );
expect( res1.components[0].length ).to.equal( cy2.length );
expect( res1.components[0].map( ele2id ) ).to.deep.equal( [ "2-5", "2-1", "2-3", "2-4", "2-6", "2-7", "2-0", "2-8", "2-2" ] );

});

it('eles.htbc(): multiple biconnected components', function(){
Expand All @@ -102,9 +122,9 @@ describe('Algorithms', function(){
expect( res.components[1].map( ele2id ) ).to.deep.equal( [ "1-21", "1-2", "1-4", "1-24", "1-3", "1-20" ] );
expect( res.components[2].map( ele2id ) ).to.deep.equal( [ "1-38", "1-10", "1-16" ] );
expect( res.components[3].map( ele2id ) ).to.deep.equal( [ "1-40", "1-10", "1-18", "1-44", "1-17", "1-39" ] );
expect( res.components[4].map( ele2id ) ).to.deep.equal( [ "1-34", "1-8", "1-15", "1-43", "1-13", "1-33", "1-14", "1-42", "1-41", "1-12", "1-32" ] );
expect( res.components[5].map( ele2id ) ).to.deep.equal( [ "1-36", "1-9", "1-11", "1-31", "1-8", "1-29", "1-7", "1-37", "1-10", "1-35", "1-30", "1-28" ] );
expect( res.components[6].map( ele2id ) ).to.deep.equal( [ "1-26", "1-5", "1-7", "1-27", "1-6", "1-23", "1-2", "1-25", "1-22" ] );
expect( res.components[4].map( ele2id ) ).to.deep.equal( [ "1-34", "1-8", "1-15", "1-43", "1-13", "1-41", "1-42", "1-33", "1-14", "1-12", "1-32" ] );
expect( res.components[5].map( ele2id ) ).to.deep.equal( [ "1-36", "1-9", "1-30", "1-35", "1-11", "1-29", "1-31", "1-37", "1-8", "1-7", "1-10", "1-28" ] );
expect( res.components[6].map( ele2id ) ).to.deep.equal( [ "1-26", "1-5", "1-22", "1-25", "1-7", "1-27", "1-6", "1-23", "1-2"] );
expect( res.components[7].map( ele2id ) ).to.deep.equal( [ "1-19", "1-1", "1-2" ] );
});

Expand Down

0 comments on commit ec05963

Please sign in to comment.