Inital attempt and Commit
This commit is contained in:
commit
b2c5dec784
5 changed files with 343 additions and 0 deletions
22
LICENSE.txt
Normal file
22
LICENSE.txt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) 2021 Kyle Simpson <getify@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
33
css/style.css
Normal file
33
css/style.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#dialpad {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: min-content;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
|
||||
#dialpad > button {
|
||||
font-size: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#dialpad > button:hover {
|
||||
border-color: orange;
|
||||
}
|
||||
|
||||
#dialpad > button.highlighted {
|
||||
border-color: blue;
|
||||
}
|
||||
|
||||
#results.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#starting-key-1,
|
||||
#starting-key-2,
|
||||
#hop-count,
|
||||
#path-count {
|
||||
font-weight: bold;
|
||||
}
|
||||
42
index.html
Normal file
42
index.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Knight's Dialer</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>
|
||||
Hops to take: <input type="number" id="enter-hop-count" size="2" maxlength="2" min="1" max="99" value="3">
|
||||
</p>
|
||||
|
||||
<div id="dialpad">
|
||||
<button type="button" value="1">1</button>
|
||||
<button type="button" value="2">2</button>
|
||||
<button type="button" value="3">3</button>
|
||||
<button type="button" value="4">4</button>
|
||||
<button type="button" value="5">5</button>
|
||||
<button type="button" value="6">6</button>
|
||||
<button type="button" value="7">7</button>
|
||||
<button type="button" value="8">8</button>
|
||||
<button type="button" value="9">9</button>
|
||||
<span><!-- spacer element for grid layout shift on bottom row --></span>
|
||||
<button type="button" value="0">0</button>
|
||||
</div>
|
||||
|
||||
<div id="results" class="hidden">
|
||||
<p>
|
||||
Count of distinct paths starting from <span id="starting-key-1">?</span> and taking <span id="hop-count">?</span> hop(s): <span id="path-count">?</span> <span id="count-timing">?</span>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Distinct acyclic paths starting from <span id="starting-key-2">?</span> <span id="paths-timing">?</span>:</h3>
|
||||
|
||||
<div id="acyclic-paths"></div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
97
js/app.js
Normal file
97
js/app.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import Dialer from "./dialer.js";
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded",function ready(){
|
||||
var enterHopsEl = document.getElementById("enter-hop-count");
|
||||
var dialpadEl = document.getElementById("dialpad");
|
||||
var dialpadKeyEls = dialpadEl.querySelectorAll("button");
|
||||
var resultsEl = document.getElementById("results");
|
||||
var startingKey1El = document.getElementById("starting-key-1");
|
||||
var startingKey2El = document.getElementById("starting-key-2");
|
||||
var hopCountEl = document.getElementById("hop-count");
|
||||
var pathCountEl = document.getElementById("path-count");
|
||||
var countTimingEl = document.getElementById("count-timing");
|
||||
var acyclicPathsEl = document.getElementById("acyclic-paths");
|
||||
var pathsTimingEl = document.getElementById("paths-timing");
|
||||
|
||||
dialpadEl.addEventListener("mouseover",onHoverKey,false);
|
||||
dialpadEl.addEventListener("mouseout",onHoverKey,false);
|
||||
dialpadEl.addEventListener("click",onClickKey,false);
|
||||
|
||||
|
||||
// ********************************
|
||||
|
||||
function onHoverKey(evt) {
|
||||
var el = evt.target;
|
||||
|
||||
// discover which keys (if any) can be reached from
|
||||
// a hovered key
|
||||
var reachable = (
|
||||
el.matches("button:hover") ?
|
||||
Dialer.reachableKeys(Number(el.value)) :
|
||||
[]
|
||||
);
|
||||
|
||||
for (let keyEl of dialpadKeyEls) {
|
||||
if (reachable.includes(Number(keyEl.value))) {
|
||||
keyEl.classList.add("highlighted");
|
||||
}
|
||||
else {
|
||||
keyEl.classList.remove("highlighted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onClickKey(evt) {
|
||||
var clickedEl = evt.target;
|
||||
|
||||
// clicked on a dialpad key?
|
||||
if (clickedEl.matches("#dialpad > button")) {
|
||||
let startingKey = Number(clickedEl.value);
|
||||
startingKey1El.innerText = startingKey;
|
||||
startingKey2El.innerText = startingKey;
|
||||
|
||||
let hopCount = Number(enterHopsEl.value) || 1;
|
||||
enterHopsEl.value = hopCount;
|
||||
hopCountEl.innerText = hopCount;
|
||||
|
||||
|
||||
let startTiming = performance.now();
|
||||
// count the distinct paths (including cyclic paths)
|
||||
let pathCount = Dialer.countPaths(startingKey,hopCount);
|
||||
let endTiming = performance.now();
|
||||
printTiming(countTimingEl,Number(endTiming - startTiming) || 0);
|
||||
pathCountEl.innerText = pathCount;
|
||||
|
||||
startTiming = performance.now();
|
||||
let acyclicPaths = Dialer.listAcyclicPaths(startingKey);
|
||||
endTiming = performance.now();
|
||||
printTiming(pathsTimingEl,Number(endTiming - startTiming) || 0);
|
||||
if (acyclicPaths.length > 0) {
|
||||
acyclicPathsEl.innerHTML = "";
|
||||
for (let path of acyclicPaths) {
|
||||
let pathEl = document.createElement("div");
|
||||
pathEl.innerHTML = path.join(" ➡ ");
|
||||
acyclicPathsEl.appendChild(pathEl);
|
||||
}
|
||||
}
|
||||
else {
|
||||
acyclicPathsEl.innerHTML = "-- none --";
|
||||
}
|
||||
|
||||
resultsEl.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function printTiming(timingEl,ms) {
|
||||
ms = Number(ms.toFixed(1));
|
||||
|
||||
if (ms >= 50) {
|
||||
timingEl.innerHTML = `(<strong>${(ms / 1000).toFixed(2)}</strong> sec)`;
|
||||
}
|
||||
else {
|
||||
timingEl.innerHTML = `(<strong>${ms.toFixed(1)}</strong> ms)`
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
149
js/dialer.js
Normal file
149
js/dialer.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
export default {
|
||||
reachableKeys,
|
||||
countPaths,
|
||||
listAcyclicPaths
|
||||
};
|
||||
//-------------------//
|
||||
// Constants //
|
||||
//-------------------//
|
||||
|
||||
const reachableKeysMap = new Map([
|
||||
[0, [4, 6]], // From key 0, you can reach keys 4 and 6
|
||||
[1, [6, 8]], // From key 1, you can reach keys 6 and 8
|
||||
[2, [9, 7]], // From key 2, you can reach keys 9 and 7
|
||||
[3, [8, 4]], // From key 3, you can reach keys 8 and 4
|
||||
[4, [3, 9, 0]], // From key 4, you can reach keys 3, 9, and 0
|
||||
[6, [1, 7, 0]], // From key 6, you can reach keys 1, 7, and 0
|
||||
[7, [2, 6]], // From key 7, you can reach keys 2 and 6
|
||||
[8, [1, 3]], // From key 8, you can reach keys 1 and 3
|
||||
[9, [2, 4]], // From key 9, you can reach keys 2 and 4
|
||||
]);
|
||||
|
||||
|
||||
//-------------------//
|
||||
// Utility Functions //
|
||||
//-------------------//
|
||||
|
||||
//TODO: P:1 Combine cyclic/acyclic DFS :ODOT//
|
||||
|
||||
// Helper function to traverse nodes
|
||||
function traverseDFS(currentDigit, processNode, endCondition, visited = new Set(), path = []) {
|
||||
if (endCondition(currentDigit)) {
|
||||
return processNode();
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
let reachable = reachableKeys(currentDigit);
|
||||
|
||||
for (let nextDigit of reachable) {
|
||||
if (visited.has(nextDigit)) continue;
|
||||
|
||||
// Mark the node as visited for acyclic paths
|
||||
if (visited !== null) visited.add(nextDigit);
|
||||
|
||||
// Add node to path for acyclic paths
|
||||
if (path !== null) path.push(nextDigit);
|
||||
|
||||
// Process node and/or count results
|
||||
count += traverseDFS(nextDigit, processNode, endCondition, visited, path);
|
||||
|
||||
// Backtrack for acyclic paths
|
||||
if (path !== null) path.pop();
|
||||
if (visited !== null) visited.delete(nextDigit);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// Recursive DFS function for cyclic paths
|
||||
function cyclicDFS(currentDigit, remainingHops) {
|
||||
return traverseDFS(
|
||||
currentDigit,
|
||||
() => remainingHops === 0 ? 1 : 0,
|
||||
() => remainingHops === 0,
|
||||
null, // No visited nodes needed
|
||||
null // No path needed
|
||||
);
|
||||
}
|
||||
|
||||
// Recursive DFS function for acyclic paths
|
||||
function asyclicDFS(currentDigit, visited, path) {
|
||||
return traverseDFS(
|
||||
currentDigit,
|
||||
() => {
|
||||
if (path.length > 1) {
|
||||
allPaths.push([...path]);
|
||||
}
|
||||
return 0; // Count not needed for acyclic paths
|
||||
},
|
||||
() => false, // No end condition, continue until all paths are explored
|
||||
visited,
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
// Quick Sort implementation to sort paths for acyclic path listing
|
||||
function quickSort(arr, compareFn) {
|
||||
if (arr.length <= 1) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const pivot = arr[Math.floor(arr.length / 2)];
|
||||
const left = [];
|
||||
const right = [];
|
||||
const middle = [];
|
||||
|
||||
for (const element of arr) {
|
||||
if (compareFn(element, pivot) < 0) {
|
||||
left.push(element);
|
||||
} else if (compareFn(element, pivot) > 0) {
|
||||
right.push(element);
|
||||
} else {
|
||||
middle.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return [...quickSort(left, compareFn), ...middle, ...quickSort(right, compareFn)];
|
||||
}
|
||||
|
||||
// Comparator function to compare path lengths
|
||||
function comparePathLength(pathA, pathB) {
|
||||
return pathA.length - pathB.length;
|
||||
}
|
||||
|
||||
//-------------------//
|
||||
// Export Functions //
|
||||
//-------------------//
|
||||
|
||||
// Function to validate starting digit input and retrive the reachable values from the reachableKeysMap
|
||||
function reachableKeys(startingDigit) {
|
||||
//Check if startingDigit is an invalid digit or 5
|
||||
if (!reachableKeysMap.has(startingDigit)){
|
||||
return [];
|
||||
}
|
||||
//return the mapped values for startingDigit
|
||||
return reachableKeysMap.get(startingDigit);
|
||||
}
|
||||
|
||||
// Function to build a list of all acyclic paths
|
||||
function listAcyclicPaths(startingDigit) {
|
||||
// Handle invalid cases
|
||||
if (!reachableKeysMap.has(startingDigit)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let allPaths = [];
|
||||
asyclicDFS(startingDigit, new Set(), []);
|
||||
|
||||
// Sort paths using Quick Sort based on their lengths
|
||||
return quickSort(allPaths, comparePathLength);
|
||||
}
|
||||
|
||||
// Function to count number of total paths given a startingDigit and a hopCount
|
||||
function countPaths(startingDigit, hopCount) {
|
||||
// Handle invalid cases
|
||||
if (!reachableKeysMap.has(startingDigit) || hopCount < 0) {
|
||||
return 0;
|
||||
}
|
||||
return cyclicDFS(startingDigit, hopCount);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue