diff --git a/markdown/bitburner.codingcontractnameenumtype.md b/markdown/bitburner.codingcontractnameenumtype.md index 6ad23115a..5c94fa446 100644 --- a/markdown/bitburner.codingcontractnameenumtype.md +++ b/markdown/bitburner.codingcontractnameenumtype.md @@ -38,5 +38,6 @@ type CodingContractNameEnumType = { EncryptionIIVigenereCipher: "Encryption II: Vigenère Cipher"; SquareRoot: "Square Root"; TotalPrimesInRange: "Total Number of Primes"; + LargestRectangleInAMatrix: "Largest Rectangle in a Matrix"; }; ``` diff --git a/markdown/bitburner.codingcontractsignatures.md b/markdown/bitburner.codingcontractsignatures.md index 7b08b1b9d..78e0a2e2b 100644 --- a/markdown/bitburner.codingcontractsignatures.md +++ b/markdown/bitburner.codingcontractsignatures.md @@ -38,5 +38,6 @@ export type CodingContractSignatures = { "Encryption II: Vigenère Cipher": [[string, string], string]; "Square Root": [bigint, bigint, [string, string]]; "Total Number of Primes": [number[], number]; + "Largest Rectangle in a Matrix": [(1 | 0)[][], [[number, number], [number, number]]]; }; ``` diff --git a/src/CodingContract/ContractTypes.ts b/src/CodingContract/ContractTypes.ts index 443eeeb07..a15816dc3 100644 --- a/src/CodingContract/ContractTypes.ts +++ b/src/CodingContract/ContractTypes.ts @@ -20,6 +20,7 @@ import { subarrayWithMaximumSum } from "./contracts/SubarrayWithMaximumSum"; import { totalPrimesInRange } from "./contracts/TotalPrimesInRange"; import { totalWaysToSum } from "./contracts/TotalWaysToSum"; import { uniquePathsInAGrid } from "./contracts/UniquePathsInAGrid"; +import { largestRectangle } from "./contracts/LargestRectangle"; // This is the base interface, but should not be used for // typechecking individual entries. Use the two types below for that. @@ -114,6 +115,7 @@ export const CodingContractDefinitions: CodingContractTypes = { ...findLargestPrimeFactor, ...generateIPAddresses, ...hammingCode, + ...largestRectangle, ...mergeOverlappingIntervals, ...minimumPathSumInATriangle, ...proper2ColoringOfAGraph, diff --git a/src/CodingContract/Enums.ts b/src/CodingContract/Enums.ts index 237d4344a..8d54ed729 100644 --- a/src/CodingContract/Enums.ts +++ b/src/CodingContract/Enums.ts @@ -28,4 +28,5 @@ export enum CodingContractName { EncryptionIIVigenereCipher = "Encryption II: Vigenère Cipher", SquareRoot = "Square Root", TotalPrimesInRange = "Total Number of Primes", + LargestRectangleInAMatrix = "Largest Rectangle in a Matrix", } diff --git a/src/CodingContract/contracts/LargestRectangle.ts b/src/CodingContract/contracts/LargestRectangle.ts new file mode 100644 index 000000000..1c0add486 --- /dev/null +++ b/src/CodingContract/contracts/LargestRectangle.ts @@ -0,0 +1,172 @@ +import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; +import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; +import { CodingContractTypes } from "../ContractTypes"; +import { CodingContractName } from "@enums"; + +export const largestRectangle: Pick = { + [CodingContractName.LargestRectangleInAMatrix]: { + desc: (data): string => { + let gridString = ""; + for (let i = 0; i < data.length; i++) { + gridString += ` [${data[i]}]${i !== data.length - 1 ? ",\n" : ""}`; + } + return `You are given a binary matrix consisting only of 0s and 1s: + +[ +${gridString} +] + +Your task is to find the two corners of the largest rectangle ([[r1,c1],[r2,c2]]) that does not contain any 1s. + +Example 1: +Data: +[ + [1,0,0], + [0,0,0] +] + +Answer:[[0,1],[1,2]] + +Example 2: +Data: +[ + [0,0,0,1], + [0,0,0,0], + [0,0,1,0], + [0,0,0,1] +] + +Answer: [[0,0],[3,1]] +`; + }, + difficulty: 6, + generate: () => { + const numRows = getRandomIntInclusive(4, 15); + const numColumns = getRandomIntInclusive(4, 15); + + const grid: (1 | 0)[][] = []; + grid.length = numRows; + let allOnes: boolean; + do { + allOnes = true; + for (let i = 0; i < numRows; ++i) { + grid[i] = []; + grid[i].length = numColumns; + grid[i].fill(0); + } + + for (let r = 0; r < numRows; ++r) { + for (let c = 0; c < numColumns; ++c) { + // 15% chance of an element being an obstacle + if (Math.random() < 0.15) { + grid[r][c] = 1; + } else { + allOnes = false; + } + } + } + } while (allOnes); + + return grid; + }, + getAnswer: (data) => { + const histograms = Array.from({ length: data.length }, () => Array(data[0].length).fill(0)); + for (let i = 0; i < data[0].length; i++) { + let count = 0; + for (let j = 0; j < data.length; j++) { + if (data[j][i] == 0) { + count++; + } else { + count = 0; + } + histograms[j][i] = count; + } + } + let maxArea = 0; + let maxL = 0; + let maxR = 0; + let maxU = 0; + let maxD = 0; + for (let i = 0; i < histograms.length; i++) { + const row = histograms[i]; + for (let j = 0; j < row.length; j++) { + if (row[j] == 0) continue; + let left = j; + let right = j; + // If the index is -1/row.length (out of bounds), it will return undefined. That's when comparing to a number + // also returns false. + while (row[left - 1] >= row[j]) { + left--; + } + while (row[right + 1] >= row[j]) { + right++; + } + if ((right - left + 1) * row[j] > maxArea) { + maxArea = (right - left + 1) * row[j]; + maxL = left; + maxR = right; + maxU = i - row[j] + 1; + maxD = i; + } + } + } + return [ + [maxU, maxL], + [maxD, maxR], + ]; + }, + solver: (state, answer): boolean => { + if ( + answer[0][0] < 0 || + answer[0][0] > state.length - 1 || + answer[0][1] < 0 || + answer[0][1] > state[0].length - 1 || + answer[1][0] < 0 || + answer[1][0] > state.length - 1 || + answer[1][1] < 0 || + answer[1][1] > state[0].length - 1 + ) + return false; + + const minR = Math.min(answer[0][0], answer[1][0]); + const maxR = Math.max(answer[0][0], answer[1][0]); + const minC = Math.min(answer[0][1], answer[1][1]); + const maxC = Math.max(answer[0][1], answer[1][1]); + for (let i = minR; i <= maxR; i++) { + if (state[i].slice(minC, maxC + 1).includes(1)) { + return false; + } + } + + const solution = largestRectangle[CodingContractName.LargestRectangleInAMatrix].getAnswer(state); + if (solution === null) { + exceptionAlert( + new Error( + `Unexpected null when calculating the answer for ${CodingContractName.LargestRectangleInAMatrix} contract. Data: ${state}`, + ), + ); + return false; + } + + const userArea = (maxR - minR + 1) * (maxC - minC + 1); + return userArea === (solution[1][0] - solution[0][0] + 1) * (solution[1][1] - solution[0][1] + 1); + }, + convertAnswer: (ans) => { + let parsedAnswer: unknown; + try { + parsedAnswer = JSON.parse(ans); + } catch (error) { + console.error("Invalid answer:", error); + return null; + } + if (!largestRectangle[CodingContractName.LargestRectangleInAMatrix].validateAnswer(parsedAnswer)) { + return null; + } + return parsedAnswer; + }, + validateAnswer: (ans): ans is [[number, number], [number, number]] => + Array.isArray(ans) && + ans.length === 2 && + ans.every((a) => Array.isArray(a) && a.length === 2 && a.every((n) => typeof n === "number")), + }, +}; diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index 483fa5de2..26f9afd2f 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -9439,6 +9439,7 @@ type CodingContractNameEnumType = { EncryptionIIVigenereCipher: "Encryption II: Vigenère Cipher"; SquareRoot: "Square Root"; TotalPrimesInRange: "Total Number of Primes"; + LargestRectangleInAMatrix: "Largest Rectangle in a Matrix"; }; /** @public */ @@ -9475,6 +9476,7 @@ export type CodingContractSignatures = { "Encryption II: Vigenère Cipher": [[string, string], string]; "Square Root": [bigint, bigint, [string, string]]; "Total Number of Primes": [number[], number]; + "Largest Rectangle in a Matrix": [(1 | 0)[][], [[number, number], [number, number]]]; }; /** @public */ diff --git a/test/jest/CodingContract/LargestRectangle.test.ts b/test/jest/CodingContract/LargestRectangle.test.ts new file mode 100644 index 000000000..e9edb5dbf --- /dev/null +++ b/test/jest/CodingContract/LargestRectangle.test.ts @@ -0,0 +1,184 @@ +import { CodingContractName } from "../../../src/Enums"; +import { largestRectangle } from "../../../src/CodingContract/contracts/LargestRectangle"; + +const contract = largestRectangle[CodingContractName.LargestRectangleInAMatrix]; + +describe("LargestRectangle", () => { + test("empty matrix", () => { + const data = Array.from({ length: 5 }, () => Array<0>(3).fill(0)); + expect(contract.desc(data)).toContain(` +[ + [0,0,0], + [0,0,0], + [0,0,0], + [0,0,0], + [0,0,0] +] +`); + expect(contract.getAnswer(data)).toEqual([ + [0, 0], + [4, 2], + ]); + expect( + contract.solver(data, [ + [0, 0], + [4, 2], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [4, 2], + [0, 0], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [4, 0], + [0, 2], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [0, 2], + [4, 0], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [0, 0], + [2, 4], + ]), + ).toBe(false); + expect( + contract.solver(data, [ + [0, 0], + [1, 1], + ]), + ).toBe(false); + }); + + test("single one", () => { + const data = Array.from({ length: 5 }, () => Array<0 | 1>(5).fill(0)); + data[1][1] = 1; + expect(contract.desc(data)).toContain(` +[ + [0,0,0,0,0], + [0,1,0,0,0], + [0,0,0,0,0], + [0,0,0,0,0], + [0,0,0,0,0] +] +`); + expect(contract.getAnswer(data)).toEqual([ + [2, 0], + [4, 4], + ]); + expect( + contract.solver(data, [ + [0, 2], + [4, 4], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [4, 4], + [0, 2], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [4, 2], + [0, 4], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [0, 4], + [4, 2], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [2, 0], + [4, 4], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [1, 0], + [4, 4], + ]), + ).toBe(false); + expect( + contract.solver(data, [ + [0, 0], + [4, 4], + ]), + ).toBe(false); + expect( + contract.solver(data, [ + [-1, 2], + [4, 4], + ]), + ).toBe(false); + }); + test("single zero", () => { + const data = Array.from({ length: 1 }, () => Array<0 | 1>(8).fill(1)); + data[0][3] = 0; + expect(contract.desc(data)).toContain(` +[ + [1,1,1,0,1,1,1,1] +] +`); + expect(contract.getAnswer(data)).toEqual([ + [0, 3], + [0, 3], + ]); + expect( + contract.solver(data, [ + [0, 3], + [0, 3], + ]), + ).toBe(true); + expect( + contract.solver(data, [ + [0, 3], + [0, 4], + ]), + ).toBe(false); + expect( + contract.solver(data, [ + [0, 2], + [0, 3], + ]), + ).toBe(false); + expect( + contract.solver(data, [ + [0, 3], + [1, 3], + ]), + ).toBe(false); + expect( + contract.solver(data, [ + [0, 0], + [0, 7], + ]), + ).toBe(false); + }); + test("generate doesn't return all ones", () => { + const origRandom = Math.random; + let calls = 0; + const mockRandom = () => { + if (calls++ < 100) { + return 0; + } + return origRandom(); + }; + try { + Math.random = mockRandom; + expect(contract.generate().flat()).toContain(0); + } finally { + Math.random = origRandom; + } + }); +});