diff --git a/3-Solving-Problems-By-Searching/bi-directional.js b/3-Solving-Problems-By-Searching/bi-directional.js new file mode 100644 index 0000000..bc09b16 --- /dev/null +++ b/3-Solving-Problems-By-Searching/bi-directional.js @@ -0,0 +1,126 @@ +//Function to clone a object +function cloneObject(obj) { + return JSON.parse(JSON.stringify(obj)); +} +//Function to calculate euclidean distance +function distance(point1, point2) { + return Math.sqrt(Math.pow(point1[0] - point2[0], 2) + Math.pow(point1[1] - point2[1], 2)); +} + +class Node { + constructor(id, x, y, adjacent) { + this.id = id; + this.x = x; + this.y = y; + this.adjacent = (adjacent != undefined) ? adjacent : []; + } +} +//Generates a random planar graph +function randomPlanarGraph(height, width, totalNodes) { + let nodes = [], + edges = []; + let grid = gitteredGrid(height, width, totalNodes); + let rowSize = grid.length; + let columnSize = grid[0].length; + //Extract the nodes from the grid + for (let i = 0; i < rowSize; i++) { + for (let j = 0; j < columnSize; j++) { + nodes.push(new Node(i * columnSize + j, grid[i][j][0], grid[i][j][1])); + } + } + //Randomly generate edges between nodes. + let minDistance = 1.6 * Math.sqrt(height * width / totalNodes) + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + if (distance([nodes[i].x, nodes[i].y], [nodes[j].x, nodes[j].y]) < minDistance) { + nodes[i].adjacent.push(j); + nodes[j].adjacent.push(i); + edges.push([i, j]); + } + } + } + return [nodes, edges]; +} + +class Graph { + constructor(height, width, totalNodes) { + this.totalNodes = 20; + this.nodes = []; + this.height = height; + this.width = width; + this.totalNodes = totalNodes; + [this.nodes, this.edges] = randomPlanarGraph(this.height, this.width, this.totalNodes); + } + + getAdjacent(id) { + return this.nodes[id].adjacent; + } +} + +class BreadthFirstSearch { + constructor(graph, initial) { + this.graph = graph; + this.initial = initial; + this.frontier = [this.initial]; + //State is true if the node is unexplored and false if explored or in frontier + this.state = new Array(this.graph.nodes.length).fill(true); + this.frontierIndex = 0; + } + //Expands a node from the frontier and returns that node + step() { + if (this.frontier.length <= this.frontierIndex) { + return undefined; + } else { + let nextNode = this.frontier[this.frontierIndex]; + //Remove nextNode from frontier + this.frontierIndex++; + //Add to explored + this.state[nextNode] = false; + //Get adjacent nodes which are unexplored + let adjacentNodes = this.graph.getAdjacent(nextNode).filter(x => this.state[x]); + //Mark each adjacent node + adjacentNodes.forEach(x => this.state[x] = false); + //Push to frontier + adjacentNodes.forEach(x => this.frontier.push(x)); + return nextNode; + } + } +} + +class BidirectionalProblem { + constructor(graph) { + this.graph = graph; + this.initial = 0; + //Force the initial node to be around the middle of the canvas. + for (let i = 0; i < this.graph.nodes.length; i++) { + if (distance([this.graph.width / 2, this.graph.height / 2], [this.graph.nodes[i].x, this.graph.nodes[i].y]) < 50) { + this.initial = i; + break; + } + } + //Final node is chosen randomly + this.final = Math.floor(Math.random() * this.graph.nodes.length); + this.sourceBFS = new BreadthFirstSearch(this.graph, this.initial); + this.destBFS = new BreadthFirstSearch(this.graph, this.final); + } + + iterate() { + let obj = { + done: false + } + //Iterate Source side BFS + let nextNode = this.sourceBFS.step(); + obj.source = nextNode; + if (!this.destBFS.state[nextNode]) { + obj.done = true; + } + + //Iterate Destination side BFS + nextNode = this.destBFS.step(); + obj.dest = nextNode; + if (!this.sourceBFS.state[nextNode]) { + obj.done = true; + } + return obj; + } +} diff --git a/3-Solving-Problems-By-Searching/c_bi-directional.js b/3-Solving-Problems-By-Searching/c_bi-directional.js new file mode 100644 index 0000000..2bb18c3 --- /dev/null +++ b/3-Solving-Problems-By-Searching/c_bi-directional.js @@ -0,0 +1,132 @@ +class BidirectionalDiagram { + constructor(selector, h, w) { + this.selector = selector; + this.h = h; + this.w = w; + this.selector.select('canvas').remove(); + this.root = this.selector + .append('canvas') + .attr('height', this.h) + .attr('width', this.w); + this.context = this.root.node().getContext("2d"); + this.context.clearRect(0, 0, this.w, this.h); + this.delay = 4; + } + + init(problem, textElement) { + this.problem = problem; + this.nodes = this.problem.graph.nodes; + this.edges = this.problem.graph.edges; + this.initial = this.problem.initial; + this.final = this.problem.final; + this.textElement = textElement; + + this.initialColor = 'hsl(0, 20%, 80%)'; + this.edgeColor = 'hsl(0, 2%, 80%)'; + this.sourceBFSColor = 'hsl(209, 100%, 50%)'; + this.destBFSColor = 'hsl(209, 30%, 50%)'; + this.sourceColor = 'hsl(209, 100%, 20%)'; + this.destColor = 'hsl(209, 100%, 20%)'; + this.nodeSize = 3.5; + this.textColorScale = d3.scaleLinear().domain([0, this.nodes.length / 2]) + .interpolate(d3.interpolateRgb) + .range([d3.hsl('hsla(102, 100%, 50%, 1)'), d3.hsl('hsla(0, 100%, 50%, 1)')]); + + //Draw all nodes + for (let i = 0; i < this.nodes.length; i++) { + this.colorNode(i, this.initialColor); + } + //Draw all edges + for (let i = 0; i < this.edges.length; i++) { + let d = this.edges[i]; + this.context.beginPath(); + this.context.lineWidth = 1; + this.context.strokeStyle = this.edgeColor; + this.context.moveTo(this.nodes[d[0]].x, this.nodes[d[0]].y); + this.context.lineTo(this.nodes[d[1]].x, this.nodes[d[1]].y); + this.context.stroke(); + this.context.closePath(); + } + + //Initial Node + this.context.fillStyle = this.sourceColor; + this.context.beginPath(); + this.context.arc(this.nodes[this.initial].x, this.nodes[this.initial].y, 1.2 * this.nodeSize, 0, 2 * Math.PI, true); + this.context.fill(); + this.context.closePath(); + this.steps++; + this.textElement.text(this.steps); + //Final Node + this.context.fillStyle = this.destColor; + this.context.beginPath(); + this.context.arc(this.nodes[this.final].x, this.nodes[this.final].y, 1.2 * this.nodeSize, 0, 2 * Math.PI, true); + this.context.fill(); + this.context.closePath(); + this.steps++; + this.textElement.text(this.steps); + this.textElement.style('color', this.textColorScale(this.steps)); + this.steps = 0; + this.bfs(); + } + + colorNode(node, color) { + //If the given node is not an initial node or final node + if (node != this.initial && node != this.final) { + this.context.fillStyle = color; + this.context.beginPath(); + this.context.arc(this.nodes[node].x, this.nodes[node].y, this.nodeSize, 0, 2 * Math.PI, true); + this.context.fill(); + this.context.closePath(); + this.steps++; + //Update steps in the page + this.textElement.style('color', this.textColorScale(this.steps)); + this.textElement.text(this.steps); + } + } + + bfs() { + this.intervalFunction = setInterval(() => { + let next = this.problem.iterate(); + + if (next.source) { + this.colorNode(next.source, this.sourceBFSColor) + } + if (next.dest) { + this.colorNode(next.dest, this.destBFSColor) + } + if (next.done) { + clearInterval(this.intervalFunction) + } + }, this.delay); + } +} + +class BFSDiagram extends BidirectionalDiagram { + constructor(selector, h, w) { + super(selector, h, w); + } + + bfs() { + this.bfsAgent = new BreadthFirstSearch(this.problem.graph, this.initial); + this.intervalFunction = setInterval(() => { + let node = this.bfsAgent.step(); + this.colorNode(node, this.sourceBFSColor); + if (node == this.final) { + clearInterval(this.intervalFunction) + } + }, this.delay); + } +} + +$(document).ready(function() { + function init() { + let bidirectionalDiagram = new BidirectionalDiagram(d3.select('#backtracking').select('#biCanvas'), 500, 550); + let bfsDiagram = new BFSDiagram(d3.select('#backtracking').select('#bfsCanvas'), 500, 550) + let graph = new Graph(500, 530, 1500); + let problem = new BidirectionalProblem(graph); + bidirectionalDiagram.init(problem, d3.select('#backtracking').select('#biStepCount')); + bfsDiagram.init(problem, d3.select('#backtracking').select('#bfsStepCount')); + } + init(); + $('#backtracking .restart-button').click(init); +}); diff --git a/3-Solving-Problems-By-Searching/gitter-grid.js b/3-Solving-Problems-By-Searching/gitter-grid.js new file mode 100644 index 0000000..05b5268 --- /dev/null +++ b/3-Solving-Problems-By-Searching/gitter-grid.js @@ -0,0 +1,17 @@ +function gitteredGrid(height, width, totalNodes) { + let cellSize = Math.sqrt(height * width / totalNodes); + let columnSize = Math.floor(width / cellSize); + let rowSize = Math.floor(height / cellSize); + let grid = []; + + for (let i = 0; i < rowSize; i++) { + grid.push(new Array(columnSize)); + } + + for (let i = 0; i < rowSize; i++) { + for (let j = 0; j < columnSize; j++) { + grid[i][j] = [cellSize * j + cellSize / 6 + Math.random() * cellSize * 2 / 3, cellSize * i + cellSize / 6 + Math.random() * cellSize * 2 / 3]; + } + } + return grid; +} diff --git a/3-Solving-Problems-By-Searching/index.html b/3-Solving-Problems-By-Searching/index.html index 8cb5869..c02d2cb 100644 --- a/3-Solving-Problems-By-Searching/index.html +++ b/3-Solving-Problems-By-Searching/index.html @@ -6,10 +6,12 @@ - + - + + + @@ -18,6 +20,7 @@ + @@ -28,6 +31,8 @@ + + @@ -360,6 +365,29 @@

Depth Limit :

+

Bi-directional BFS

+

In bi-directional BFS, we run two simultaneous searches, one from the initial state and one from the goal state. We stop when these searches meet in the middle (ie, a node is explored by both the BFS)

+

The motivation behind this is that bd/2 + bd/2 is smaller than bd

+

In the diagram below, we compare the performance between bi-directional BFS and standard BFS side by side. The total number of nodes generated for each strategy is given below the diagram.

+

Notice that the further apart the final node is from the initial node, the better bi-directional BFS performs (as compared to standard BFS). Use the restart button to restart the simulation.

+
+
+
Restart
+
+
+
+

Bi-directional BFS

+
+

+
+
+

Standard BFS

+
+

+
+
+
+

A Star Search

Click on the canvas to restart the simulation