Merge pull request #3331 from Ornedan/shortest-path-cct

New coding contract: Shortest Path in a Grid
This commit is contained in:
hydroflame 2022-04-08 00:38:19 -04:00 committed by GitHub
commit d956e47246
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 287 additions and 0 deletions

@ -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. |

@ -1,4 +1,5 @@
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { MinHeap } from "../utils/Heap";
import { HammingEncode, HammingDecode } from "../utils/HammingCodeTools";
/* tslint:disable:completed-docs no-magic-numbers arrow-return-shorthand */
@ -795,6 +796,135 @@ 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: 7,
numTries: 10,
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.
// 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;
}
}
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): 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): 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
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 [

140
src/utils/Heap.ts Normal file

@ -0,0 +1,140 @@
/** Binary heap. */
abstract class BinHeap<T> {
/**
* 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(): number {
return this.data.length;
}
/** 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 {
// 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;
}
}
/**
* Should element with weight `weightA` be closer to root than element with
* weight `weightB`?
*/
protected abstract heapOrderABeforeB(weightA: number, weightB: number): boolean;
}
/** Binary max-heap. */
export class MaxHeap<T> extends BinHeap<T> {
heapOrderABeforeB(weightA: number, weightB: number): boolean {
return weightA > weightB;
}
}
/** Binary min-heap. */
export class MinHeap<T> extends BinHeap<T> {
heapOrderABeforeB(weightA: number, weightB: number): boolean {
return weightA < weightB;
}
}