From d69e94002efae122d02f2bba80a59490225383b1 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 2 Apr 2022 00:52:47 +0300 Subject: [PATCH 1/6] Add binary heap data structure --- src/utils/Heap.ts | 142 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/utils/Heap.ts diff --git a/src/utils/Heap.ts b/src/utils/Heap.ts new file mode 100644 index 000000000..9f508048a --- /dev/null +++ b/src/utils/Heap.ts @@ -0,0 +1,142 @@ +/** Binary heap. */ +abstract class BinHeap { + /** + * Heap data array consisting of [weight, payload] pairs, arranged by weight + * to satisfy heap condition. + * + * Encodes the binary tree by storing tree root at index 0 and + * left child of element i at `i * 2 + 1` and + * right child of element i at `i * 2 + 2`. + */ + protected data: [number, T][]; + + constructor() { + this.data = []; + } + + /** Get number of elements in the heap. */ + public get size() { + return this.data.length; + } + + /** + * Should element with weight `weightA` be closer to root than element with + * weight `weightB`? + */ + protected abstract heapOrderABeforeB(weightA: number, weightB: number): boolean; + + /** Restore heap condition, starting at index i and traveling towards root. */ + protected heapifyUp(i: number): void { + // Swap the new element up towards root until it reaches root position or + // settles under under a suitable parent + while(i > 0) { + const p = Math.floor((i - 1) / 2); + + // Reached heap-ordered state already? + if(this.heapOrderABeforeB(this.data[p][0], this.data[i][0])) + break; + + // Swap + const tmp = this.data[p]; + this.data[p] = this.data[i]; + this.data[i] = tmp; + + // And repeat at parent index + i = p; + } + } + + /** Restore heap condition, starting at index i and traveling away from root. */ + protected heapifyDown(i: number): void { + // Swap the shifted element down in the heap until it either reaches the + // bottom layer or is in correct order relative to it's children + while(i < this.data.length) { + const l = i * 2 + 1; + const r = i * 2 + 2; + let toSwap = i; + + // Find which one of element i and it's children should be closest to root + if(l < this.data.length && this.heapOrderABeforeB(this.data[l][0], this.data[toSwap][0])) + toSwap = l; + if(r < this.data.length && this.heapOrderABeforeB(this.data[r][0], this.data[toSwap][0])) + toSwap = r; + + // Already in order? + if(i == toSwap) + break; + + // Not in order. Swap child that should be closest to root up to 'i' and repeat + const tmp = this.data[toSwap]; + this.data[toSwap] = this.data[i]; + this.data[i] = tmp; + + i = toSwap; + } + } + + /** Add a new element to the heap. */ + public push(value: T, weight: number): void { + const i = this.data.length; + this.data[i] = [weight, value]; + this.heapifyUp(i); + } + + /** Get the value of the root-most element of the heap, without changing the heap. */ + public peek(): T | undefined { + if(this.data.length == 0) + return undefined; + + return this.data[0][1]; + } + + /** Remove the root-most element of the heap and return the removed element's value. */ + public pop(): T | undefined { + if(this.data.length == 0) + return undefined; + + const value = this.data[0][1]; + + this.data[0] = this.data[this.data.length - 1]; + this.data.length = this.data.length - 1; + + this.heapifyDown(0); + + return value; + } + + /** Change the weight of an element in the heap. */ + public changeWeight(predicate: (value: T) => boolean, weight: number): void { + // Find first element with matching value, if any + const i = this.data.findIndex(([_, v]) => predicate(v)); + if(i == -1) + return; + + // Update that element's weight + this.data[i][0] = weight; + + // And re-heapify if needed + const p = Math.floor((i - 1) / 2); + const l = i * 2 + 1; + const r = i * 2 + 2; + + if(!this.heapOrderABeforeB(this.data[p][0], this.data[i][0])) // Needs to shift root-wards? + this.heapifyUp(i); + else // Try shifting deeper + this.heapifyDown(i); + } +} + + +/** Binary max-heap. */ +export class MaxHeap extends BinHeap { + heapOrderABeforeB(weightA: number, weightB: number): boolean { + return weightA > weightB; + } +} + +/** Binary min-heap. */ +export class MinHeap extends BinHeap { + heapOrderABeforeB(weightA: number, weightB: number): boolean { + return weightA < weightB; + } +} From fb0cc157947e8f56fc4a923be7c62d9f4499546b Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 2 Apr 2022 03:17:34 +0300 Subject: [PATCH 2/6] New coding contract type: 'Shortest Path in a Grid' --- src/data/codingcontracttypes.ts | 129 ++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/data/codingcontracttypes.ts b/src/data/codingcontracttypes.ts index 23235fa9a..a3ee24632 100644 --- a/src/data/codingcontracttypes.ts +++ b/src/data/codingcontracttypes.ts @@ -1,4 +1,5 @@ import { getRandomInt } from "../utils/helpers/getRandomInt"; +import { MinHeap } from "../utils/Heap"; /* tslint:disable:completed-docs no-magic-numbers arrow-return-shorthand */ @@ -794,6 +795,134 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [ return obstacleGrid[obstacleGrid.length - 1][obstacleGrid[0].length - 1] === parseInt(ans); }, }, + { + name: "Shortest Path in a Grid", + desc: (data: number[][]): string => { + return [ + "You are located in the top-left corner of the following grid:\n\n", + `  [${data.map(line => "[" + line + "]").join(",\n   ")}]\n\n`, + "You are trying to find the shortest path to the bottom-right corner of the grid,", + "but there are obstacles on the grid that you cannot move onto.", + "These obstacles are denoted by '1', while empty spaces are denoted by 0.\n\n", + "Determine the shortest path from start to finish, if one exists.", + "The answer should be given as a string of UDLR characters, indicating the moves along the path\n\n", + "NOTE: If there are multiple equally short paths, any of them is accepted as answer.", + "If there is no path, the answer should be an empty string.\n", + "NOTE: The data returned for this contract is an 2D array of numbers representing the grid.\n\n", + "Examples:\n\n", + "    [[0,1,0,0,0],\n", + "     [0,0,0,1,0]]\n", + "\n", + "Answer: 'DRRURRD'\n\n", + "    [[0,1],\n", + "     [1,0]]\n", + "\n", + "Answer: ''\n\n", + ].join(" "); + }, + difficulty: 5, // TODO: higher, but probably not much more? + numTries: 10, // TODO: probably OK? + gen: (): number[][] => { + const height = getRandomInt(6, 12); + const width = getRandomInt(6, 12); + const dstY = height - 1; + const dstX = width - 1; + const minPathLength = dstY + dstX; // Math.abs(dstY - srcY) + Math.abs(dstX - srcX) + + const grid: number[][] = new Array(height); + for(let y = 0; y < height; y++) + grid[y] = new Array(width).fill(0); + + for(let y = 0; y < height; y++) { + for(let x = 0; x < width; x++) { + if(y == 0 && x == 0) continue; // Don't block start + if(y == dstY && x == dstX) continue; // Don't block destination + + // Generate more obstacles the farther a position is from start and destination, + // with minimum obstacle chance of 15% + const distanceFactor = Math.min(y + x, dstY - y + dstX - x) / minPathLength; + if (Math.random() < Math.max(0.15, distanceFactor)) + grid[y][x] = 1; + } + } + + return grid; + }, + solver: (data: number[][], ans: string): boolean => { + const width = data[0].length; + const height = data.length; + const dstY = height - 1; + const dstX = width - 1; + + const distance: [number][] = new Array(height); + //const prev: [[number, number] | undefined][] = new Array(height); + const queue = new MinHeap<[number, number]>(); + + for(let y = 0; y < height; y++) { + distance[y] = new Array(width).fill(Infinity) as [number]; + //prev[y] = new Array(width).fill(undefined) as [undefined]; + } + + function validPosition(y: number, x: number) { + return y >= 0 && y < height && x >= 0 && x < width && data[y][x] == 0; + } + + // List in-bounds and passable neighbors + function* neighbors(y: number, x: number) { + if(validPosition(y - 1, x)) yield [y - 1, x]; // Up + if(validPosition(y + 1, x)) yield [y + 1, x]; // Down + if(validPosition(y, x - 1)) yield [y, x - 1]; // Left + if(validPosition(y, x + 1)) yield [y, x + 1]; // Right + } + + // Prepare starting point + distance[0][0] = 0; + queue.push([0, 0], 0); + + // Take next-nearest position and expand potential paths from there + while(queue.size > 0) { + const [y, x] = queue.pop() as [number, number]; + for(const [yN, xN] of neighbors(y, x)) { + const d = distance[y][x] + 1; + if(d < distance[yN][xN]) { + if(distance[yN][xN] == Infinity) // Not reached previously + queue.push([yN, xN], d); + else // Found a shorter path + queue.changeWeight(([yQ, xQ]) => yQ == yN && xQ == xN, d); + //prev[yN][xN] = [y, x]; + distance[yN][xN] = d; + } + } + } + + // No path at all? + if(distance[dstY][dstX] == Infinity) + return ans == ""; + + // There is a solution, require that the answer path is as short as the shortest + // path we found + if(ans.length > distance[dstY][dstX]) + return false; + + // Further verify that the answer path is a valid path + let ansX = 0; + let ansY = 0; + for(const direction of ans) { + switch(direction) { + case "U": ansY -= 1; break; + case "D": ansY += 1; break; + case "L": ansX -= 1; break; + case "R": ansX += 1; break; + default: return false; // Invalid character + } + if(!validPosition(ansY, ansX)) + return false; + } + + // Path was valid, finally verify that the answer path brought us to the end coordinates + return ansY == dstY && ansX == dstX; + }, + }, { desc: (data: string): string => { return [ From 809f8f6687b81668f49f2b99dce784cea1ea4ec6 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 2 Apr 2022 04:27:38 +0300 Subject: [PATCH 3/6] Tuned Shortest Path contract generation Now generates about 4/5 puzzles where a path exists and 1/5 with the destination completely blocked off. --- src/data/codingcontracttypes.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/data/codingcontracttypes.ts b/src/data/codingcontracttypes.ts index a3ee24632..2c88b87a6 100644 --- a/src/data/codingcontracttypes.ts +++ b/src/data/codingcontracttypes.ts @@ -838,9 +838,10 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [ if(y == 0 && x == 0) continue; // Don't block start if(y == dstY && x == dstX) continue; // Don't block destination - // Generate more obstacles the farther a position is from start and destination, - // with minimum obstacle chance of 15% - const distanceFactor = Math.min(y + x, dstY - y + dstX - x) / minPathLength; + // Generate more obstacles the farther a position is from start and destination. + // Raw distance factor peaks at 50% at half-way mark. Rescale to 40% max. + // Obstacle chance range of [15%, 40%] produces ~78% solvable puzzles + const distanceFactor = Math.min(y + x, dstY - y + dstX - x) / minPathLength * 0.8; if (Math.random() < Math.max(0.15, distanceFactor)) grid[y][x] = 1; } From 197e875610ab5a11f571825957023e74e88aa335 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 2 Apr 2022 04:34:16 +0300 Subject: [PATCH 4/6] Set Shortest Path contract difficulty factor --- src/data/codingcontracttypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/codingcontracttypes.ts b/src/data/codingcontracttypes.ts index 2c88b87a6..3265302cb 100644 --- a/src/data/codingcontracttypes.ts +++ b/src/data/codingcontracttypes.ts @@ -820,8 +820,8 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [ "Answer: ''\n\n", ].join(" "); }, - difficulty: 5, // TODO: higher, but probably not much more? - numTries: 10, // TODO: probably OK? + difficulty: 7, + numTries: 10, gen: (): number[][] => { const height = getRandomInt(6, 12); const width = getRandomInt(6, 12); From dc3b083587787cfb38eefb3bbfffcac3bdefe3cf Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 2 Apr 2022 04:55:15 +0300 Subject: [PATCH 5/6] Add readthedocs documentation for Shortest Path contract type --- doc/source/basicgameplay/codingcontracts.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/source/basicgameplay/codingcontracts.rst b/doc/source/basicgameplay/codingcontracts.rst index 51f68fc68..3edb89453 100644 --- a/doc/source/basicgameplay/codingcontracts.rst +++ b/doc/source/basicgameplay/codingcontracts.rst @@ -196,6 +196,23 @@ The list contains the name of (i.e. the value returned by | | | | | | | Determine how many unique paths there are from start to finish. | +------------------------------------+------------------------------------------------------------------------------------------+ +| Shortest Path in a Grid | | You are given a 2D array of numbers (array of array of numbers) representing | +| | | a grid. The 2D array contains 1's and 0's, where 1 represents an obstacle and | +| | | 0 represents a free space. | +| | | | +| | | Assume you are initially positioned in top-left corner of that grid and that you | +| | | are trying to reach the bottom-right corner. In each step, you may move to the up, | +| | | down, left or right. Furthermore, you cannot move onto spaces which have obstacles. | +| | | | +| | | Determine if paths exist from start to destination, and find the shortest one. | +| | | | +| | | Examples: | +| | | [[0,1,0,0,0], | +| | | [0,0,0,1,0]] -> "DRRURRD" | +| | | [[0,1], | +| | | [1,0]] -> "" | +| | | | ++------------------------------------+------------------------------------------------------------------------------------------+ | Sanitize Parentheses in Expression | | Given a string with parentheses and letters, remove the minimum number of invalid | | | | parentheses in order to validate the string. If there are multiple minimal ways | | | | to validate the string, provide all of the possible results. | From 233289af56e3ae56a67bc600e0d7f4ba08cf4067 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 2 Apr 2022 05:16:48 +0300 Subject: [PATCH 6/6] Fix lint errors --- src/data/codingcontracttypes.ts | 4 +- src/utils/Heap.ts | 110 ++++++++++++++++---------------- 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/src/data/codingcontracttypes.ts b/src/data/codingcontracttypes.ts index 3265302cb..cf53a8938 100644 --- a/src/data/codingcontracttypes.ts +++ b/src/data/codingcontracttypes.ts @@ -864,12 +864,12 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [ //prev[y] = new Array(width).fill(undefined) as [undefined]; } - function validPosition(y: number, x: number) { + function validPosition(y: number, x: number): boolean { return y >= 0 && y < height && x >= 0 && x < width && data[y][x] == 0; } // List in-bounds and passable neighbors - function* neighbors(y: number, x: number) { + function* neighbors(y: number, x: number): Generator<[number, number]> { if(validPosition(y - 1, x)) yield [y - 1, x]; // Up if(validPosition(y + 1, x)) yield [y + 1, x]; // Down if(validPosition(y, x - 1)) yield [y, x - 1]; // Left diff --git a/src/utils/Heap.ts b/src/utils/Heap.ts index 9f508048a..ea31ed7bc 100644 --- a/src/utils/Heap.ts +++ b/src/utils/Heap.ts @@ -15,15 +15,58 @@ abstract class BinHeap { } /** Get number of elements in the heap. */ - public get size() { + public get size(): number { return this.data.length; } - /** - * Should element with weight `weightA` be closer to root than element with - * weight `weightB`? - */ - protected abstract heapOrderABeforeB(weightA: number, weightB: number): boolean; + /** Add a new element to the heap. */ + public push(value: T, weight: number): void { + const i = this.data.length; + this.data[i] = [weight, value]; + this.heapifyUp(i); + } + + /** Get the value of the root-most element of the heap, without changing the heap. */ + public peek(): T | undefined { + if(this.data.length == 0) + return undefined; + + return this.data[0][1]; + } + + /** Remove the root-most element of the heap and return the removed element's value. */ + public pop(): T | undefined { + if(this.data.length == 0) + return undefined; + + const value = this.data[0][1]; + + this.data[0] = this.data[this.data.length - 1]; + this.data.length = this.data.length - 1; + + this.heapifyDown(0); + + return value; + } + + /** Change the weight of an element in the heap. */ + public changeWeight(predicate: (value: T) => boolean, weight: number): void { + // Find first element with matching value, if any + const i = this.data.findIndex(e => predicate(e[1])); + if(i == -1) + return; + + // Update that element's weight + this.data[i][0] = weight; + + // And re-heapify if needed + const p = Math.floor((i - 1) / 2); + + if(!this.heapOrderABeforeB(this.data[p][0], this.data[i][0])) // Needs to shift root-wards? + this.heapifyUp(i); + else // Try shifting deeper + this.heapifyDown(i); + } /** Restore heap condition, starting at index i and traveling towards root. */ protected heapifyUp(i: number): void { @@ -74,56 +117,11 @@ abstract class BinHeap { } } - /** Add a new element to the heap. */ - public push(value: T, weight: number): void { - const i = this.data.length; - this.data[i] = [weight, value]; - this.heapifyUp(i); - } - - /** Get the value of the root-most element of the heap, without changing the heap. */ - public peek(): T | undefined { - if(this.data.length == 0) - return undefined; - - return this.data[0][1]; - } - - /** Remove the root-most element of the heap and return the removed element's value. */ - public pop(): T | undefined { - if(this.data.length == 0) - return undefined; - - const value = this.data[0][1]; - - this.data[0] = this.data[this.data.length - 1]; - this.data.length = this.data.length - 1; - - this.heapifyDown(0); - - return value; - } - - /** Change the weight of an element in the heap. */ - public changeWeight(predicate: (value: T) => boolean, weight: number): void { - // Find first element with matching value, if any - const i = this.data.findIndex(([_, v]) => predicate(v)); - if(i == -1) - return; - - // Update that element's weight - this.data[i][0] = weight; - - // And re-heapify if needed - const p = Math.floor((i - 1) / 2); - const l = i * 2 + 1; - const r = i * 2 + 2; - - if(!this.heapOrderABeforeB(this.data[p][0], this.data[i][0])) // Needs to shift root-wards? - this.heapifyUp(i); - else // Try shifting deeper - this.heapifyDown(i); - } + /** + * Should element with weight `weightA` be closer to root than element with + * weight `weightB`? + */ + protected abstract heapOrderABeforeB(weightA: number, weightB: number): boolean; }