mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-18 20:25:45 +01:00
Merge pull request #3331 from Ornedan/shortest-path-cct
New coding contract: Shortest Path in a Grid
This commit is contained in:
commit
d956e47246
@ -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
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user