CCT: Add "Find Largest Rectangle in a Matrix" coding contract (#2519)

This commit is contained in:
Misha279-UA
2026-02-24 22:10:42 +02:00
committed by GitHub
parent 6626f0d5d1
commit c85d9cbe8c
7 changed files with 363 additions and 0 deletions

View File

@@ -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";
};
```

View File

@@ -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]]];
};
```

View File

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

View File

@@ -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",
}

View File

@@ -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<CodingContractTypes, CodingContractName.LargestRectangleInAMatrix> = {
[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<number>(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")),
},
};

View File

@@ -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 */

View File

@@ -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;
}
});
});