Recursive "hasCycle" function continues "running" even when a basecase is met - javascript

Suppose I am given an array of pairs (where pair[0] depends on pair[1]). I want to detect whether there is a cycle between any of the pair dependencies.
Cycle:
[[0,1], [1,2], [2, 1]]
Explanation: There is a cycle between at 1 -> 2 and 2 -> 1
Not a Cycle:
[[0,1], [1,2], [0, 2]]
TLDR;
The problem I am having is... once I have "detected" a loop, I cannot seem to figure out how to "return" it. The callstack coninues executing the "other" children, but I want it to stop.
You can skip to bottom (The Algorithm)
Approach:
Create a graph representation of pairs using a Map ✅
/**
* #param { array } [ [intA, intB] ] - Nested array of integers pairs
* #return { Map } (representing a Graph)
*/
function createGraph(array) {
const nodes = new Map();
// Create Verticies
array.map(pair => {
const aClass = pair[0];
const bClass = pair[1];
nodes.set(aClass, []);
nodes.set(bClass, []);
})
// Create Edges
array.map(pair => {
const aClass = pair[0];
const bClass = pair[1];
nodes.get(aClass).push(bClass);
});
return nodes;
}
And so far, it is working as expected:
// Create Graph
const array = [[0,1], [1,2], [0, 2]];
const graph = createGraph(array);
console.log(graph);
// Map(3) { 0 => [ 1, 2 ], 1 => [ 2 ], 2 => [] }
Do a DFS of all the unvisted Nodes (in the GreySet) (until we have detect a cycle). ⚠️
I used the 3 Coloured Set approach. I really liked the algorothim and truly wish to implement it. From my understanding, it goes as follows.
WhiteSet contains all nodes that have not been touched.
GreySet contains nodes that are currently being explored. Thus, in our DFS, the "parent" will remain in this set.
BlackSet will hold nodes that have already been explored (to the point where there was no cycle).
So... If we explore a child that is in the BlackSet, there is no reason for us to explore it any further (it has already been explored and it does not have any cycle anyways). However, if we come across a child that is NOT in the WhiteSet AND it exists in the GreySet, that means we have a cycle.
Here is my code, I have added the console.logs below. The problem I am having is... once I have "detected" a loop, I cannot seem to figure out how to "return" it. It continues executing as you will see.
The Algorithm:
// Create Graph
const graph = createGraph(array);
console.log(graph); // Looks Good
// Detect Cycle
// Create 3 Sets
const whtSet = new Set(graph.keys()); // Put all the Integer "node values" the set.
const grySet = new Set();
const blkSet = new Set();
const unvisitedValues = whtSet.keys(); // Iterator
while (whtSet.size > 0) {
const doesItHaveCycle = hasCycle(unvisitedValues.next().value); // Expore any unexplored nodeVal
console.log('whtSet', whtSet);
console.log('grySet', grySet);
console.log('blkSet', blkSet);
console.log('does it have a cycle', doesItHaveCycle);
}
function hasCycle(nodeVal) {
// Blackset means it has been compltely explored :)
if (blkSet.has(nodeVal)) return false;
// This means we have found a cycle
if (!whtSet.has(nodeVal) && grySet.has(nodeVal)) return true;
// Remove it from the whiteSet, into the greySet.
whtSet.delete(nodeVal);
grySet.add(nodeVal);
// Recurse Children
graph.get(nodeVal).forEach((child) => {
let doesHaveCycle = hasCycle(child);
console.log(
'doesHaveCycle result: ',
doesHaveCycle,
'when exploring nodeVal',
nodeVal,
'and child',
child
);
if (doesHaveCycle === true) return true; // RETURN THIS PLS lol
});
// If above was true, I DO NOT want it to come here, but it still does.
console.log('if above was true... shouldnt come here');
// Now, that we have explored all of the children, remove it from Grey Set...
// Add it to the Black Set
grySet.delete(nodeVal);
blkSet.add(nodeVal);
return false; // IDK
}
Results of console.log
doesHaveCycle result: true when exploring nodeVal 2 and child 1
if above was true... shouldnt come here
doesHaveCycle result: false when exploring nodeVal 1 and child 2
if above was true... shouldnt come here
doesHaveCycle result: false when exploring nodeVal 0 and child 1
if above was true... shouldnt come here
whtSet Set(0) {}
grySet Set(0) {}
blkSet Set(3) { 2, 1, 0 }
does it have a cycle false
I am 100% open revising the code, it is a mess right now. But, I still want to do it the "3 Colour Set" way.
For more context, I was trying to take a crack at this cute algorithmic problem called Course Schedule.

// Recurse Children
graph.get(nodeVal).forEach((child) => {
let doesHaveCycle = hasCycle(child);
console.log(
'doesHaveCycle result: ',
doesHaveCycle,
'when exploring nodeVal',
nodeVal,
'and child',
child
);
if (doesHaveCycle === true) return true; // RETURN THIS PLS lol
});
The command "RETURN THIS PLS lol" usually works, but this is a special case. foreach just calls the callback on each element, and ignores the return value of the callback.
The function you need is called some. It calls the given function on each element just like foreach, and returns true as soon as one of those calls returns true. (It basically tells if the callback is true for some element.) It returns false otherwise.
This is how some is used:
// Recurse Children
const cycle = graph.get(nodeVal).some((child) => {
let doesHaveCycle = hasCycle(child);
console.log(
'doesHaveCycle result: ',
doesHaveCycle,
'when exploring nodeVal',
nodeVal,
'and child',
child
);
return doesHaveCycle;
});
if (cycle) return true;
// now this obviously works
console.log('if above was true... shouldnt come here');
You also probably want to break out of the while loop once a cycle is found:
let doesItHaveCycle = false;
while (whtSet.size > 0) {
doesItHaveCycle = hasCycle(unvisitedValues.next().value); // Expore any unexplored nodeVal
if (doesItHaveCycle) break;
}
Because, we are not doing a full DFS but returning as soon as we see the cycle. This leaves the current node grey and some of its children not-visited. If we wanted to continue after detecting a cycle, we would need to visit all the children, change the current node to black, and then return if there is a cycle.

Related

Return value for function is not working correctly. How to set up correct return value for function checkIfExist()?

I am writing a script for Premiere pro where I can add markers in the timeline and export a still for each marker in one go. However, when I write a function to check if the still has been previously created, the functions tells me it finds the previously created still, but then still creates a new one.
So basically: Function returns true, but still executes the else{}
//checks if the frame that is about to be exported already exists
if(checkIfExist(app.project.rootItem, outputFile)){
alert("frame already exists");
}else{
//This is where the actual still gets created and imported
activeSequence.exportFramePNG(time, outputFileName);
//here the previously created item gets moved to the appropriate bin (This is working great ATM)
moveToBin(outputFile);
}
}
}
//This function is meant to check if an item exists in the project bin. It does this by looping though all the items in the array from the start.
function checkIfExist(currentItem, name){
for(var i = 0; i<currentItem.children.numItems; i++){
currentChild = currentItem.children[i];
if(currentChild.name.toUpperCase() === name.toUpperCase()){
alert("Found: " + currentChild.name);
return true;
}if(currentChild.type == ProjectItemType.BIN){
checkIfExist(currentChild, name);
}
}
return false;
}
I think it happens because of the recursion you do:
if(currentChild.type == ProjectItemType.BIN){
checkIfExist(currentChild, name);
}
If this one gets kicked off before you can return true, you will start to run the function for a second time.
Now the first run can return a true, while the second (or even 3th, or 4th, etc) can return false and thus creating a new one, while also finding it.
Also if possible try to use arr.find or arr.findIndex and check if the value is -1 (or not found). This will make your code shorter, cleaner and less open for errors :)
But this will not work for nested arrays. Then you need to make an other function to first make a flat copy that includes all nested array before you do the arr.find or arr.findIndex. Still think that is the better solution.
You can use this to make nested array into a flat one:
let arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
function flattenDeep(arr1) {
return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
}
flattenDeep(arr1);// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

Nested loop (FOR) with IF statement inside 2nd for print just one result

Basically I have 2 arrays, one with some code and another with codes and relative description, what I need to do is match the codes and print the description but my code (apparently) stops at the first loop of the inner FOR (I've attaches a screenshot to understand better).
If I remove the IF statement from the code it prints the counters of the 2 for as it should be.
for (x=0; x<causeoferrorlength; x++)
{
document.getElementById("mdataresult").innerHTML += "x "+causeoferrorsplit[x]+"</br>";
for(k=0; k<78; k++)
{
if ( causeoferrorsplit[x] === gbrucausesoferror[k][0] )
{
document.getElementById("mdataresult").innerHTML += "k "+gbrucausesoferror[k][0]+"</br>";
}
}
}
I have no errors from the console but it isn't printing as expected.
This is probably better handled in a declarative way versus imperative. It will be shorter and easier to reason about.
Given you're using two arrays, and that the codes in the first array will always be found somewhere in the second array:
let causes = ["001", "003", "005"];
let codes = [
["001","Earthquake"],
["002","Sunspots"],
["003","User Error"],
["004","Snakes"],
["005","Black Magic"]
];
let results = causes.map( cause => codes[ codes.findIndex( code => code[0] === cause ) ][1] );
console.log(results); // ["Earthquake", "User Error", "Black Magic"]
What's happening here? We're mapping the array of potential causes of error (the first array) to a list of descriptions taken from the second array.
Array.map takes a function that is invoked once with each array member. We'll call that member 'cause'.
Array.findIndex takes a function that is invoked once for each array member. We'll call that member 'code'.
For each 'cause' in causes we find the index in codes where the first array value is equal to the cause, then return the second array value, the description.
If you have the ability to change the second array to an object, then this gets way simpler:
let causes = ["001", "003", "005"];
let codes = {
"001":"Earthquake",
"002":"Sunspots",
"003":"User Error",
"004":"Snakes",
"005":"Black Magic"
};
let results = causes.map( cause => codes[cause] );
console.log(results); // ["Earthquake", "User Error", "Black Magic"]

Recursively loop through array of objects to build an array of all possible routes with Javascript

I'm trying to write a routing function that will return all possible routes from any two given locations (I'm calling them "spaces"), but I'm stuck on writing the recursive function.
My data will look something like this:
const allSpaces = [
{
id: 0,
name: 'Living Room',
connectedSpaces: [1,2]
},
{
id: 1,
name: 'Hallway A',
connectedSpaces: [0,4]
},
{
id: 2,
name: 'Hallway B',
connectedSpaces: [0,4]
},
{
id: 3,
name: 'Bedroom',
connectedSpaces: [1,2]
}
];
So, calling a getAllRoutes(0,3) method would walk all possible routes and return an array of arrays:
[
[0,1,3],
[0,2,3]
]
Keep in mind that this may not always be as simplistic of a dataset as my example (i.e., Hallway A could have an offshoot that provides an alternate route, or could re-intersect with previously visited spaces).
I'm pretty stumped. I've made several attempts at a recursive function but keep ending up with incomplete lists or infinite loops. Any help would be appreciated!
Visualizing your data
Whenever you find yourself stuck with a problem like this, it helps to think of an easy way to visualize what's happening. To get a feel for the graph you're working with, I wrote a few lines of code to visualize the graph.
Through the visualization, I noticed there's probably a small error in the data. I figured spaces 1 and 2 should be connected to 0 and 3 rather than 0 and 4. I adjusted this in the data, and added an additional space for testing.
If you like, you can check out the visualization by expanding the snippet below.
const allSpaces=[{id:0,name:"Living Room",connectedSpaces:[1,2]},{id:1,name:"Hallway A",connectedSpaces:[0,3,4]},{id:2,name:"Hallway B",connectedSpaces:[0,3]},{id:3,name:"Bedroom",connectedSpaces:[1,2]}, {id:4,name:"Master bedroom",connectedSpaces:[1]}];
const Edge = (() => {
// Do not support two way edges. Cache from and to internally:
const cache = new Map();
const Edge = (from, to) => {
const id = `${Math.min(from, to)}.${Math.max(from, to)}`;
const length = 1;
if (!cache.has(id)) {
cache.set(id, { from, to, id, length });
}
return cache.get(id);
}
return (from => to => Edge(from, to));
})();
const edges = uniques(allSpaces.reduce(
(edges, node) => edges.concat(
node.connectedSpaces.map(Edge(node.id))
), []
));
const Node = ({ id, name }) => ({ id, label: name });
const nodes = allSpaces.map(Node);
const network = new vis.Network(
document.getElementById("cvs"),
{ nodes, edges },
{}
);
function uniques(arr) { return Array.from(new Set(arr).values()); }
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis-network.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
<div id="cvs" style="height: 300px"></div>
Being able to see our data, makes it easier to check if our function works! Now, you've asked to find all paths to get from A to B. Note that this the number of possible paths can very quickly increase as you add more nodes. (e.g., have a look at the Traveling Salesman Problem).
Shortest path?
If you're really looking for the shortest path, you might want to adapt the code below to use Dijkstra's Shortest Path algorithm, as SLaks suggested in their comment.
Brute force approach
But, since the example set is small, and you asked for all routes, let's brute force it:
Define an empty collection of paths
Define a start node
Add its id to the current path
For every node it links to:
Check if it's our destination; if so: return the collection with the current path added
Check if it's already in our path; if so: skip it (we don't want to move in circles)
If it's not in our path nor our destination, add it to the path and move deeper
Or, in code:
const walk = (destination, paths, path, node) => {
// Walking in circles
if (path.includes(node.id)) // <-- expensive, for large paths use a Set
return paths;
// End reached
if (node.id === destination)
return paths.concat([path.concat(node.id)]);
// Take next step recursively
return node.connectedSpaces
.reduce(
(acc, id) => walk(destination, acc, path.concat(node.id), spaceMap.get(id)),
paths
);
}
Here's a running snippet you can use to step through and see what happens:
const allSpaces=[{id:0,name:"Living Room",connectedSpaces:[1,2]},{id:1,name:"Hallway A",connectedSpaces:[0,3,4]},{id:2,name:"Hallway B",connectedSpaces:[0,3]},{id:3,name:"Bedroom",connectedSpaces:[1,2]}, {id:4,name:"Master bedroom",connectedSpaces:[1]}];
const spaceMap = new Map(allSpaces.map(s => [s.id, s]));
const walk = (destination, paths, path, node) => {
// Walking in circles
if (path.includes(node.id)) // <-- expensive, for large paths use a Set
return paths;
// End reached
if (node.id === destination)
return paths.concat([path.concat(node.id)]);
// Take next step recursively
return node.connectedSpaces
.reduce(
(acc, id) => walk(destination, acc, path.concat(node.id), spaceMap.get(id)),
paths
);
}
const calcRoute = (from, to) => {
const routes = walk(to, [], [], spaceMap.get(from));
return `
Found ${routes.length} route(s) to ${spaceMap.get(to).name}
${routes.map(r => r.map(id => spaceMap.get(id).name).join(" -> ")).join("\n")}
`;
}
console.log(calcRoute(0, 3));
console.log(calcRoute(0, 4));

Ember store adding attributes incorrectly

I'm using the latest version of ember-cli, ember-data, ember-localstorage-adapter, and ember.
I have a Node object which has a parent and children. Since I had issues with creating multiple relationships with the same type of object, I decided to store the parentID in a string, and the childIDs in an array of strings. However, when I create a new Node and try to add the new Node's to the parents array of IDs, the ID ends up being added to the correct parent, but also other parents.
level 1 0
/ \
level 2 1 2
| |
level 3 3 4
In a structure like this, 0, 1, and 2 all have correct child and parent IDs. However, after adding 3 and 4, node 1 and node 2's childIDs are [3, 4], instead of [3], [4] respectively.
The Array attribute:
var ArrayTransform = DS.Transform.extend({
serialize: function(value) {
if (!value) {
return [];
}
return value;
},
deserialize: function(value) {
if (!value) {
return [];
}
return value;
}
});
The insertNode code:
insert: function(elem) {
var i,
_store = elem.node.store,
newNodeJSON = elem.node.serialize();
newNodeJSON.childIds = [];
newNodeJSON.level = getNextLevel();
_store.filter('node', function(node) {
return node.get('level') === newnodeJSON.level-1;
}).then(function(prevLevelNodes) {
// if no other nodes yet
if (prevLevelNodes.toArray().length === 0) {
makeNewNode(_store, newNodeJSON, elem.node);
}
// else, generates however many nodes that are in the previous level
else {
prevLevelNodes.toArray().forEach(function(node, idx) {
newNodeJSON.parentId = node.get('id');
makeNewNode(_store, newNodeJSON, elem.node);
});
}
});
}
var makeNewNode = function(_store, newNodeJSON, node) {
console.log(newNodeJSON.parentId); // returns correct value
var newNode = _store.createRecord('node', newNodeJSON);
newNode.save();
var newNodeId = newNode.get('id');
if (newNode.get('parentId')) {
_store.find('node', newNode.get('parentId')).then(function(n) {
var cids = n.get('childIds');
console.log(newNodeId); // returns expected value
console.log(cids); // **DOESN'T RETURN AN EMPTY ARRAY**: returns array with [3,4]
cids.push(newNodeId);
console.log(n.get('childIds')); // returns array with [3,4]
n.save();
});
}
To top this off, this error happens 90% of the time, but 10% of the time it performs as expected. This seems to suggest that there's some sort of race condition, but I'm not sure where that would even be. Some places that I feel like might be causing issues: the ember-cli compilation, passing the entire _store in when making a new node, ember-data being weird, ember-localstorage-adapter being funky... no clue.
For anyone else who may have this problem in the future: the problem lies in two things.
In ArrayTransform, typically I am returning the value sans modification.
In my insert code, I'm passing the same JSON that I defined at the top of the function to makeNewNode.
This JSON contains a reference to a single childIds array; therefore, each new node that gets created uses this same reference for its childIds. Although this doesn't quite explain why the cids array wasn't empty before the push executed (perhaps this is some sort of compiler oddity or console printing lag), it explains why these both Level 3 children were in both Level 2 parents' childIds array.
tl;dr: pass by value vs pass by reference error

rx: unfold array to multiple streams

I have a stream holding an array, each element of which has an id. I need to split this into a stream per id, which will complete when the source stream no longer carries the id.
E.g. input stream sequence with these three values
[{a:1}, {b:1}] [{a:2}, {b:2}, {c:1}] [{b:3}, {c:2}]
should return three streams
a -> 1 2 |
b -> 1 2 3
c -> 1 2
Where a has completed on the 3rd value, since its id is gone, and c has been created on the 2nd value, since its id has appeared.
I'm trying groupByUntil, a bit like
var input = foo.share();
var output = input.selectMany(function (s) {
return rx.Observable.fromArray(s);
}).groupByUntil(
function (s) { return s.keys()[0]; },
null,
function (g) { return input.filter(
function (s) { return !findkey(s, g.key); }
); }
)
So, group by the id, and dispose of the group when the input stream no longer has the id. This seems to work, but the two uses of input look odd to me, like there could a weird order dependency when using a single stream to control the input of the groupByUntil, and the disposal of the groups.
Is there a better way?
update
There is, indeed, a weird timing problem here. fromArray by default uses the currentThread scheduler, which will result in events from that array being interleaved with events from input. The dispose conditions on the group are then evaluated at the wrong time (before the groups from the previous input have been processed).
A possible workaround is to do fromArray(.., rx.Scheduler.immediate), which will keep the grouped events in sync with input.
yeah the only alternative I can think of is to manage the state yourself. I don't know that it is better though.
var d = Object.create(null);
var output = input
.flatMap(function (s) {
// end completed groups
Object
.keys(d)
.filter(function (k) { return !findKey(s, k); })
.forEach(function (k) {
d[k].onNext(1);
d[k].onCompleted();
delete d[k];
});
return Rx.Observable.fromArray(s);
})
.groupByUntil(
function (s) { return s.keys()[0]; },
null,
function (g) { return d[g.key] = new Rx.AsyncSubject(); });

Categories

Resources