From 14ddf66f2dab5f92153f75e485b5c7be8a39acab Mon Sep 17 00:00:00 2001 From: Qualtagh Date: Mon, 5 Dec 2022 01:27:48 +0200 Subject: [PATCH 1/3] asymptotically faster solution for 17.23 --- .../QuestionLookup.java | 198 ++++++++++++++++++ .../Q17_23_Max_Black_Square/QuestionLookup.md | 86 ++++++++ .../Q17_23_Max_Black_Square/Subsquare.java | 9 + .../Q17_23_Max_Black_Square/Tester.java | 122 +++++++++++ 4 files changed, 415 insertions(+) create mode 100644 Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.java create mode 100644 Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md create mode 100644 Java/Ch 17. Hard/Q17_23_Max_Black_Square/Tester.java diff --git a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.java b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.java new file mode 100644 index 000000000..277a52730 --- /dev/null +++ b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.java @@ -0,0 +1,198 @@ +package Q17_23_Max_Black_Square; + +import CtCILibrary.AssortedMethods; +import java.util.ArrayList; +import java.util.List; +import java.util.NavigableSet; +import java.util.TreeSet; + +public class QuestionLookup { + private static class Cell { + public int row; + public int column; + public boolean isZero; + public int zerosRight; + public int zerosBelow; + public int zerosLeft; + public int zerosAbove; + public int minRightAndBelow; + public int minLeftAndAbove; + } + + /* 1D interval + * (needed for a subtask of finding longest intersecting intervals) */ + private static class Interval { + public int start; + public int end; + public int length; + + public Interval(int start, int end) { + this.start = start; + this.end = end; + this.length = end - start + 1; + } + } + + private static Subsquare maxSquare(Subsquare a, Subsquare b) { + return a == null ? b : b == null ? a : a.size < b.size ? b : a; + } + + private static Interval maxInterval(Interval a, Interval b) { + return a == null ? b : b == null ? a : a.length < b.length ? b : a; + } + + public static Subsquare findSquare(int[][] matrix) { + Cell[][] cells = createCells(matrix); + countZerosRightAndBelow(cells); + countZerosLeftAndAbove(cells); + return findSquareAmongDiagonals(cells); + } + + private static Cell[][] createCells(int[][] matrix) { + int size = matrix.length; + Cell[][] cells = new Cell[size][size]; + for (int r = 0; r < size; r++) { + for (int c = 0; c < size; c++) { + Cell cell = cells[r][c] = new Cell(); + cell.row = r; + cell.column = c; + cell.isZero = matrix[r][c] == 0; + } + } + return cells; + } + + private static void countZerosRightAndBelow(Cell[][] cells) { + int size = cells.length; + for (int r = size - 1; r >= 0; r--) { + for (int c = size - 1; c >= 0; c--) { + Cell cell = cells[r][c]; + if (!cell.isZero) { + continue; + } + cell.zerosRight = (c + 1 < size ? cells[r][c + 1].zerosRight : 0) + 1; + cell.zerosBelow = (r + 1 < size ? cells[r + 1][c].zerosBelow : 0) + 1; + cell.minRightAndBelow = Math.min(cell.zerosRight, cell.zerosBelow); + } + } + } + + private static void countZerosLeftAndAbove(Cell[][] cells) { + int size = cells.length; + for (int r = 0; r < size; r++) { + for (int c = 0; c < size; c++) { + Cell cell = cells[r][c]; + if (!cell.isZero) { + continue; + } + cell.zerosLeft = (c > 0 ? cells[r][c - 1].zerosLeft : 0) + 1; + cell.zerosAbove = (r > 0 ? cells[r - 1][c].zerosAbove : 0) + 1; + cell.minLeftAndAbove = Math.min(cell.zerosLeft, cell.zerosAbove); + } + } + } + + private static Subsquare findSquareAmongDiagonals(Cell[][] cells) { + int size = cells.length; + Subsquare best = null; + /* Loop through the diagonals of the matrix. + * The diagonal 0 goes from top left corner to the bottom right corner. + * It's the longest diagonal. Its length equals to "size". + * Diagonals with negative numbers are above, positive below. + * E.g., the last diagonal with maximum number is located at the + * bottom left corner. Its length is 1. */ + for (int diagonal = -size + 1; diagonal < size; diagonal++) { + Cell[] diagonalCells = extractDiagonal(cells, diagonal); + Subsquare subsquare = findSquareOnDiagonal(diagonalCells); + best = maxSquare(best, subsquare); + } + return best; + } + + /* Copy a diagonal of a square to a separate one-dimensional array */ + private static Cell[] extractDiagonal(Cell[][] cells, int diagonal) { + int size = cells.length; + int diagonalLength = size - Math.abs(diagonal); + Cell[] diagonalCells = new Cell[diagonalLength]; + for (int position = 0; position < diagonalLength; position++) { + int r = diagonal < 0 ? position : position + diagonal; + int c = diagonal < 0 ? position - diagonal : position; + Cell cell = cells[r][c]; + diagonalCells[position] = cell; + } + return diagonalCells; + } + + private static Subsquare findSquareOnDiagonal(Cell[] diagonalCells) { + Interval[] leftStarts = getDiagonalLeftIntervals(diagonalCells); + Interval[] rightEnds = getDiagonalRightIntervals(diagonalCells); + Interval intersection = findLongestIntersection(leftStarts, rightEnds); + if (intersection == null) { + return null; + } + Cell topLeftCorner = diagonalCells[intersection.start]; + return new Subsquare(topLeftCorner.row, topLeftCorner.column, intersection.length); + } + + private static Interval[] getDiagonalLeftIntervals(Cell[] diagonalCells) { + Interval[] intervals = new Interval[diagonalCells.length]; + for (int start = 0; start < intervals.length; start++) { + int length = diagonalCells[start].minRightAndBelow; + int end = start + length - 1; + intervals[start] = new Interval(start, end); + } + return intervals; + } + + private static Interval[] getDiagonalRightIntervals(Cell[] diagonalCells) { + Interval[] intervals = new Interval[diagonalCells.length]; + for (int end = 0; end < intervals.length; end++) { + int length = diagonalCells[end].minLeftAndAbove; + int start = end - length + 1; + intervals[end] = new Interval(start, end); + } + return intervals; + } + + /* Turn array where index is the end of an interval + * into array where index is the start of intervals and value is a list of their ends. + * A list is needed because multiple intervals can start at the same position. */ + private static List[] getStartToEndMap(Interval[] intervalEnds) { + List[] intervalStarts = new List[intervalEnds.length]; + for (Interval interval : intervalEnds) { + intervalStarts[interval.end] = new ArrayList<>(); + if (interval.length != 0) { + intervalStarts[interval.start].add(interval.end); + } + } + return intervalStarts; + } + + /* Find longest intersection {start, end} of left and right intervals such that + * end - rightEnds[end] + 1 <= start <= end <= start + leftStarts[start] - 1 + * Equivalently: right.start <= left.start <= right.end <= left.end */ + private static Interval findLongestIntersection(Interval[] leftStarts, Interval[] rightEnds) { + List[] rightStartToEndMap = getStartToEndMap(rightEnds); + Interval longest = null; + NavigableSet openedRightIntervals = new TreeSet<>(); + for (Interval left : leftStarts) { + openedRightIntervals.addAll(rightStartToEndMap[left.start]); + Integer rightEnd = left.length == 0 ? null : openedRightIntervals.floor(left.end); + Interval intersection = rightEnd == null ? null : new Interval(left.start, rightEnd); + longest = maxInterval(longest, intersection); + openedRightIntervals.remove(left.start); + } + return longest; + } + + public static void main(String[] args) { + int[][] matrix = AssortedMethods.randomMatrix(7, 7, 0, 1); + AssortedMethods.printMatrix(matrix); + Subsquare square = findSquare(matrix); + if (square == null) { + System.out.println("no squares found"); + } else { + square.print(); + } + } +} \ No newline at end of file diff --git a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md new file mode 100644 index 000000000..753f9c324 --- /dev/null +++ b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md @@ -0,0 +1,86 @@ +17.23 - Max Square Matrix +--- +> Imagine you have a square matrix, where each cell (pixel) is either black or white. Design an algorithm to find the maximum subsquare such that all four borders are filled with black pixels. + +--- + +In [the previous solution](https://github.com/careercup/CtCI-6th-Edition/blob/master/Java/Ch%2017.%20Hard/Q17_23_Max_Black_Square/QuestionEff.java), we counted zeros below and to the right of each cell. We managed to do it using dynamic programming with quadratic time complexity - O(N2) where N is the length of the square matrix side. + +Knowing how many zeros are located below and to the right shows us how big a potential square could be if its top left corner started at the given cell. See a picture below - we have several potential corners here: + + + +If some cell has 5 zeros to the right and only 4 below, it means that the square starting at this cell can be of size 1-4. Thus, `min(zerosBelow, zerosRight)` shows us maximum size of a square candidate which top left corner is located at the current cell. + +Similarly, we can make these calculations for zeros above and zeros to the left as well. This will provide information on potential bottom right corners of seeked squares. + +When two opposite corners meet each other (i.e. their sides are long enough to reach the opposite sides), a square is detected. So, how to check if corners meet? In order to do that, let's switch to diagonals. A diagonal representation turns 2D problem into 1D. + + + +We need to extract cells of each diagonal in a loop and treat them as a one-dimensional array. There are 2 * N - 1 diagonals. The size of the first one (in the top right corner) is 1. The one in the middle is the longest (of length N). And the bottom left corner also has length 1. Let's give them indexes from -(N - 1) to (N - 1). The middle one will have index 0. + + + +Now, let's concentrate on one of diagonals. For each diagonal, we've collected 2 arrays of corner sizes: one for top-left corners and another one - for bottom-right. We need to find intersection points and choose the biggest intersection (i.e. square). Once we have the biggest square for each diagonal, we'll simply take the maximum square of all diagonals. + + + +This task can be formulated as: +> Given 2 arrays of non-negative integers `A` and `B` of equal length, find positions `i` and `j` such that `j - B[j] < i <= j < i + A[i]`. Return `max(j - i)` of all such `i` and `j`, or `null` if there are no such indicies. + +In other words, if top left corner starts at `i` then it can reach the opposite corner at index `j` between `i` (incl.) and `i + A[i]` (excl.). And vice versa, if bottom right corner starts at `j`, the opposite corner (at index `i`) should be between `j - B[j]` (excl.) and `j` (incl.). If both conditions are met, a valid square is found. Its size is `j - i + 1`. + +So, this is a task about intersecting intervals. + +Let's take a look at the example: +```text +Matrix diagonal before extraction: + +Top left corners (A): | Bottom right corners (B): +(count to the right) | (count above) +4 x x x | 1 x x x + 5 x x x x | 2 x x x x + 0 | 0 + 2 x | 1 x + 3 x x | 2 x x + 0 x | 0 x + 2 x | 3 x + 0 | 0 + +1D subtask: + i: 0 1 2 3 4 5 6 7 + A[i]: 4 5 0 2 3 0 2 0 + i + A[i] - 1: 3 5 1 4 6 4 7 6 +i <= j <= i + A[i] - 1: 0-3 1-5 --- 3-4 4-6 --- 6-7 --- + + j: 0 1 2 3 4 5 6 7 + B[j]: 1 2 0 1 2 0 3 0 + j - B[j] + 1: 0 0 3 3 3 6 4 8 +j - B[j] + 1 <= i <= j: 0-0 0-1 --- 3-3 3-4 --- 4-6 --- + +Intersections: +i = 0: 0-3 includes j = 0 (0-0), 1 (0-1), 3 (3-3) +Filter out j = 3 (3-3) because lower bound 3 > i = 0 +So, only j = 0 and j = 1 suit + +i = 1: 1-5 includes j = 1 (0-1), 3 (3-3), 4 (3-4) +Filter out j = 3 (3-3) because lower bound 3 > i = 1 +Filter out j = 4 (3-4) because lower bound 3 > i = 1 +Only j = 1 suits + +i = 3: 3-4 includes j = 3 (3-3), 4 (3-4), both suit + +i = 4: 4-6 includes j = 4 (3-4), 6 (4-6), both suit + +i = 6: 6-7 includes j = 6 (4-6) - it suits + +Resulting pairs of [i, j]: +{0, 0}, {0, 1}, {1, 1}, {3, 3}, {3, 4}, {4, 4}, {4, 6}, {6, 6} +Note that all these pairs form valid sqares of sizes: + 1, 2, 1, 1, 2, 1, 3, 1 +Maximim size is 3 of square formed by +top left corner at index 4 and bottom right corner on index 6 +``` + +This is basically a naive brute force approach to solve this subtask. Its time complexity is O(N2). Can it be improved? diff --git a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/Subsquare.java b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/Subsquare.java index 98cfee1d2..87b1d4676 100644 --- a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/Subsquare.java +++ b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/Subsquare.java @@ -11,4 +11,13 @@ public Subsquare(int r, int c, int sz) { public void print() { System.out.println("(" + row + ", " + column + ", " + size + ")"); } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Subsquare other = (Subsquare) obj; + return row == other.row && column == other.column && size == other.size; + } } \ No newline at end of file diff --git a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/Tester.java b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/Tester.java new file mode 100644 index 000000000..066ba0e66 --- /dev/null +++ b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/Tester.java @@ -0,0 +1,122 @@ +package Q17_23_Max_Black_Square; + +import CtCILibrary.AssortedMethods; +import java.util.ArrayList; +import java.util.List; + +public class Tester { + private static void throwSquaresDiffer(int[][] matrix, Subsquare expectedMaxSquare, Subsquare actualMaxSquare) { + System.out.println("wrong size"); + AssortedMethods.printMatrix(matrix); + System.out.println("expected square:"); + expectedMaxSquare.print(); + System.out.println("actual square:"); + actualMaxSquare.print(); + throw new AssertionError("wrong size"); + } + + private static List findSquareAllMethods(int[][] matrix) { + List maxSquares = new ArrayList(); + // comment out slow versions to speed up the test + maxSquares.add(Question.findSquare(matrix)); + maxSquares.add(QuestionEff.findSquare(matrix)); + maxSquares.add(QuestionLookup.findSquare(matrix)); + return maxSquares; + } + + private static void checkMatrix(int[][] matrix, Subsquare expected) { + List maxSquares = findSquareAllMethods(matrix); + for (Subsquare square : maxSquares) { + if (expected == null ? square == null : expected.equals(square)) { + continue; + } + throwSquaresDiffer(matrix, expected, square); + } + } + + private static void testSquareInsideCorner() { + int[][] matrix = { + {0, 0, 0, 0, 0}, + {0, 1, 1, 1, 1}, + {0, 1, 0, 0, 0}, + {0, 1, 0, 1, 0}, + {0, 1, 0, 0, 0}, + }; + Subsquare expected = new Subsquare(2, 2, 3); + checkMatrix(matrix, expected); + } + + private static void testSquare() { + int[][] matrix = { + {0, 0, 0, 0, 1, 1, 1, 1}, + {0, 0, 0, 0, 0, 0, 1, 1}, + {0, 0, 1, 1, 1, 1, 1, 1}, + {0, 0, 1, 0, 0, 1, 1, 1}, + {1, 0, 1, 0, 0, 0, 0, 1}, + {1, 0, 1, 1, 0, 1, 0, 1}, + {1, 1, 1, 1, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 1, 0, 1}, + }; + Subsquare expected = new Subsquare(4, 4, 3); + checkMatrix(matrix, expected); + } + + private static void testLongerCornerInsideShorterOne() { + int[][] matrix = { + {0, 0, 0, 0, 1, 1, 1, 1}, + {0, 0, 0, 0, 0, 0, 1, 1}, + {0, 0, 1, 0, 1, 1, 1, 1}, + {0, 0, 0, 0, 0, 1, 1, 1}, + {1, 0, 1, 0, 0, 0, 0, 1}, + {1, 0, 1, 1, 0, 1, 0, 1}, + {1, 1, 1, 1, 0, 0, 0, 0}, + {1, 1, 1, 1, 1, 1, 0, 1}, + }; + Subsquare expected = new Subsquare(0, 0, 4); + checkMatrix(matrix, expected); + } + + private static void testAllBlack() { + int[][] matrix = AssortedMethods.randomMatrix(10, 10, 0, 0); + Subsquare expected = new Subsquare(0, 0, 10); + checkMatrix(matrix, expected); + } + + private static void testAllWhite() { + int[][] matrix = AssortedMethods.randomMatrix(10, 10, 1, 1); + Subsquare expected = null; + checkMatrix(matrix, expected); + } + + private static void testComparison() { + for (int size = 0; size <= 1000; size += 10) { + int[][] matrix = AssortedMethods.randomMatrix(size, size, 0, 1); + List maxSquares = findSquareAllMethods(matrix); + Subsquare expectedMaxSquare = maxSquares.get(0); + int expectedMaxSize = expectedMaxSquare == null ? 0 : expectedMaxSquare.size; + System.out.println("size = "+size); + if (expectedMaxSquare == null) { + System.out.println("no square found"); + } else { + expectedMaxSquare.print(); + } + for (int i = 1; i < maxSquares.size(); i++) { + Subsquare actualMaxSquare = maxSquares.get(i); + int actualMaxSize = actualMaxSquare == null ? 0 : actualMaxSquare.size; + if (actualMaxSize == expectedMaxSize) { + continue; + } + throwSquaresDiffer(matrix, expectedMaxSquare, actualMaxSquare); + } + } + } + + public static void main(String[] args) { + testAllBlack(); + testAllWhite(); + testLongerCornerInsideShorterOne(); + testSquare(); + testSquareInsideCorner(); + testComparison(); + } +} \ No newline at end of file From 7527d8a3cbd2d2af187da4f92c07c340048b3fbd Mon Sep 17 00:00:00 2001 From: Qualtagh Date: Mon, 5 Dec 2022 05:05:13 +0200 Subject: [PATCH 2/3] Add algorithm explanation --- .../Q17_23_Max_Black_Square/QuestionLookup.md | 223 ++++++++++++++---- 1 file changed, 177 insertions(+), 46 deletions(-) diff --git a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md index 753f9c324..9e212811c 100644 --- a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md +++ b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md @@ -4,6 +4,8 @@ --- +### Preparing cells data + In [the previous solution](https://github.com/careercup/CtCI-6th-Edition/blob/master/Java/Ch%2017.%20Hard/Q17_23_Max_Black_Square/QuestionEff.java), we counted zeros below and to the right of each cell. We managed to do it using dynamic programming with quadratic time complexity - O(N2) where N is the length of the square matrix side. Knowing how many zeros are located below and to the right shows us how big a potential square could be if its top left corner started at the given cell. See a picture below - we have several potential corners here: @@ -18,7 +20,7 @@ When two opposite corners meet each other (i.e. their sides are long enough to r -We need to extract cells of each diagonal in a loop and treat them as a one-dimensional array. There are 2 * N - 1 diagonals. The size of the first one (in the top right corner) is 1. The one in the middle is the longest (of length N). And the bottom left corner also has length 1. Let's give them indexes from -(N - 1) to (N - 1). The middle one will have index 0. +We need to extract cells of each diagonal in a loop and treat them as a one-dimensional array. There are `2 * N - 1` diagonals. The size of the first one (in the top right corner) is 1. The one in the middle is the longest (of length N). And the bottom left corner also has length 1. Let's give them indexes from `-(N - 1)` to `(N - 1)`. The middle one will have index 0. @@ -26,61 +28,190 @@ Now, let's concentrate on one of diagonals. For each diagonal, we've collected 2 -This task can be formulated as: -> Given 2 arrays of non-negative integers `A` and `B` of equal length, find positions `i` and `j` such that `j - B[j] < i <= j < i + A[i]`. Return `max(j - i)` of all such `i` and `j`, or `null` if there are no such indicies. +--- -In other words, if top left corner starts at `i` then it can reach the opposite corner at index `j` between `i` (incl.) and `i + A[i]` (excl.). And vice versa, if bottom right corner starts at `j`, the opposite corner (at index `i`) should be between `j - B[j]` (excl.) and `j` (incl.). If both conditions are met, a valid square is found. Its size is `j - i + 1`. +### A subtask: the longest intersection of intervals -So, this is a task about intersecting intervals. +This task can be formulated as: +> `A` and `B` are 2 equally sized arrays of integers. Index `start` of array `A` denotes the beginning of the interval `left`. The value `A[start]` is the length of the interval `left`. Index `end` of array `B` is the end of the interval `right`. The value `B[end]` is the length of the interval `right`. Find the longest intersection `[left.start, right.end]` among all possible intervals `left` and `right` such that +> `right.start <= left.start <= right.end <= left.end` -Let's take a look at the example: +Check the following example on how this subtask relates to the initial problem of subsquares: ```text -Matrix diagonal before extraction: - -Top left corners (A): | Bottom right corners (B): -(count to the right) | (count above) -4 x x x | 1 x x x - 5 x x x x | 2 x x x x - 0 | 0 - 2 x | 1 x - 3 x x | 2 x x - 0 x | 0 x - 2 x | 3 x - 0 | 0 - -1D subtask: - i: 0 1 2 3 4 5 6 7 - A[i]: 4 5 0 2 3 0 2 0 - i + A[i] - 1: 3 5 1 4 6 4 7 6 -i <= j <= i + A[i] - 1: 0-3 1-5 --- 3-4 4-6 --- 6-7 --- - - j: 0 1 2 3 4 5 6 7 - B[j]: 1 2 0 1 2 0 3 0 - j - B[j] + 1: 0 0 3 3 3 6 4 8 -j - B[j] + 1 <= i <= j: 0-0 0-1 --- 3-3 3-4 --- 4-6 --- +Original matrix: +0 x + 0 x + x x x x + x 0 x +x x x x x x + x x 0 + +Squares on main diagonal: +{row=2, col=2, size=1} +{row=2, col=2, size=3} +{row=4, col=4, size=1} + +Equivalent arrays of zeros to the right (A) and to the left (B): +A: 0 0 4 0 2 0 +B: 0 0 1 0 5 0 + +Intervals: +i: 0 1 2 3 4 5 # +A: x x x x (1) + x x (2) +B: x x x x x (3) + x (4) Intersections: -i = 0: 0-3 includes j = 0 (0-0), 1 (0-1), 3 (3-3) -Filter out j = 3 (3-3) because lower bound 3 > i = 0 -So, only j = 0 and j = 1 suit +(1)x(3) - {start=2, end=4, length=3} +(1)x(4) - {start=2, end=2, length=1} +(2)x(3) - {start=4, end=4, length=1} +(2)x(4) - no overlapping +^^ this is equivalent to the squares above +``` -i = 1: 1-5 includes j = 1 (0-1), 3 (3-3), 4 (3-4) -Filter out j = 3 (3-3) because lower bound 3 > i = 1 -Filter out j = 4 (3-4) because lower bound 3 > i = 1 -Only j = 1 suits +As usual, we can start from a naive brute-force approach: compare each `left` interval with every `right` interval and return the longest intersection: +```java +private static Interval findLongestIntersection(Interval[] a, Interval[] b) { + Interval longest = null; + for (Interval left : a) { + for (Interval right : b) { + if (right.start <= left.start && left.start <= right.end && right.end <= left.end) { + Interval intersection = new Interval(left.start, right.end); + longest = maxInterval(longest, intersection); + } + } + } + return longest; +} +``` +This method takes O(N2) time. Can it be improved? -i = 3: 3-4 includes j = 3 (3-3), 4 (3-4), both suit +--- -i = 4: 4-6 includes j = 4 (3-4), 6 (4-6), both suit +### Optimization: track opened intervals -i = 6: 6-7 includes j = 6 (4-6) - it suits +We can notice that once we obtained `left`, there's no need to traverse the whole `B` array searching for overlapping `right` intervals. In the example above, intervals (2) and (4) don't intersect. So, checking (2), we could omit any intervals from `B` that had already ended before we reached the start of the interval (2). Let `openedRightIntervals` be a set of `right` intervals that have started by the moment we handle current index but haven't ended yet. Take a look at the example: +```text + i: 0 1 2 3 4 5 # + B: x x x x x (3) + x (4) +openedRightIntervals: [3] [3] [3, 4] [3] [3] [] +``` +While iterating over array `A`, we can keep track of `openedRightIntervals` in linear time by adding all `right` intervals that start at some position and removing an interval ending at that position. Note that exactly one `right` interval ends at every index. But several `right` intervals can start at the same position. See: +```text +Matrix: +0 x x + 0 x x +x x x x + 0 x +x x x x x + +B: 0 0 3 0 5 + +Intervals: +x x x (1) - {start=0, end=2, length=3} +x x x x x (2) - {start=0, end=4, length=5} +``` +Despite that some indexes will contain a list of `right` intervals starting there, the total quantity of `right` intervals is N anyway, so the time and space remain linear. + +--- + +### Optimization: take last interval + +Another optimization is based on the fact that we don't need to list all intersections, we just need the longest one. Thus, finding the longest intersection for each `left` interval is enough. Eventually, we'll be able to take the longest one among them. -Resulting pairs of [i, j]: -{0, 0}, {0, 1}, {1, 1}, {3, 3}, {3, 4}, {4, 4}, {4, 6}, {6, 6} -Note that all these pairs form valid sqares of sizes: - 1, 2, 1, 1, 2, 1, 3, 1 -Maximim size is 3 of square formed by -top left corner at index 4 and bottom right corner on index 6 +Which `right` interval produces the longest intersection for a given `left` interval? See the following examples of intervals and compare with the original matrix: +```text +Matrix: +0 x + 0 x x this is (left) + x(1) x | + 0 x | + 0 x x v x +x x x x x(2)x x x x + x x(3) x + x 0 x x + (left)-> x x(4) x + 0 x + x x x x x x(5) + +Intervals: +i: 0 1 2 3 4 5 6 7 8 9 10 +A: x x x x (left) +B: x x (1) + x x x x x x (2) + x x x (3) + x x (4) + x x x x x x x (5) + +(1) - no overlap at all +(2) - long interval (L=6), but overlap is short (L=1) +(3) - short interval (L=3), but it overlaps 50% (L=2) +(4) - it looks like an overlap, but check the original matrix - corners don't meet + right.start = 7 > left.start = 5, but right.start should be <= left.start +(5) - long interval (L=7), covers "left" entirely, but check the original matrix + corners don't touch each other because right.end = 10 > left.end = 8 (should be <=) +``` +Summing it up, the longest overlap is produced by the last `right` interval met before the index `left.end` (among intervals which were opened at `left.start`, i.e. among those contained in `openedRightIntervals` set). So, we need to query the last `right.end <= left.end`. It can be done using a binary search tree, e.g., the Java implementation - TreeMap. Time complexity of this action is O(log N). We can also combine `openedRightIntervals` with this index thus turning it into a TreeSet: +```java +private static Interval findLongestIntersection(Interval[] a, Interval[] b) { + /* Turn B intervals from {index = end, value = length} format + * to {index = start, value = List of ends of B intervals starting at this index} */ + List[] rightStartPoints = endsToStarts(b); + + Interval longest = null; + NavigableSet openedRightIntervals = new TreeSet<>(); + for (Interval left : a) { + /* Track B intervals opened up to left.start point */ + openedRightIntervals.addAll(rightStartPoints[left.start]); + + /* Find the last interval up to left.end point. + * This interval gives the longest intersection for the interval "left". */ + Integer rightEnd = left.length == 0 ? null : openedRightIntervals.floor(left.end); + + /* Check if new intersection is the longest one among all intervals */ + Interval intersection = rightEnd == null ? null : new Interval(left.start, rightEnd); + longest = maxInterval(longest, intersection); + + /* Remove B interval ending at the current index */ + openedRightIntervals.remove(left.start); + } + return longest; +} ``` -This is basically a naive brute force approach to solve this subtask. Its time complexity is O(N2). Can it be improved? +--- + +### Full algorithm + +**Taking all these steps together:** +- for each cell, count zeros to the left, to the right, above and below this cell +- find the length a potential corner may have starting in this cell + - top left corner's length = min(zeros right, zeros below) + - bottom right corner's length = min(zeros left, zeros above) +- loop through diagonals + - extract diagonal as one-dimensional array + - prepare 2 arrays A and B + - A contains lengths of top left corners + - B contains lengths of bottom right corners + - find longest intersection of intervals from A and B + - turn it into subsquare +- find the biggest subsquare among all diagonals + +**Find longest intersection of intervals from A and B:** +- rearrange array B to be indexed by the start of intervals instead of their end +- start tracking opened intervals of B +- loop through intervals from A + - mark all intervals from B starting at this index as opened + - find the last interval from B which ends before A interval end + - calculate the intersection if such an interval is found + - remove the interval of B ending at this index from the set of opened intervals +- find the longest intersection among all found intersections + +Preparation steps take O(N2). Looping through diagonals is O(N) per se. For each diagonal, extraction and array preparations take O(N). The longest intersection subtask is O(N * log N) totally: looping is O(N), tracking of opened intervals is O(N) in sum, last interval querying takes O(log N) per each iteration. **Total time:** O(N2 * log N). **Total space:** O(N2) - we count zeros using copies of the matrix. Extra memory is spent on each iteration of diagonals loop and is allocated temporarily for that iteration only. It takes O(N), so it can be neglected. + +Comparing to previous solutions, we managed to achieve an improvement: +- [Simple solution](https://github.com/careercup/CtCI-6th-Edition/blob/master/Java/Ch%2017.%20Hard/Q17_23_Max_Black_Square/Question.java): O(N4), space: O(1) +- [Pre-processing solution](https://github.com/careercup/CtCI-6th-Edition/blob/master/Java/Ch%2017.%20Hard/Q17_23_Max_Black_Square/QuestionEff.java): O(N3), space: O(N2) +- This (lookup) solution: O(N2 * log N), space: O(N2) From 618b165555807ffefb5f71dd4215d282b561662b Mon Sep 17 00:00:00 2001 From: Qualtagh Date: Mon, 5 Dec 2022 14:18:35 +0200 Subject: [PATCH 3/3] fix time complexity calculations --- Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md index 9e212811c..8d0e14f03 100644 --- a/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md +++ b/Java/Ch 17. Hard/Q17_23_Max_Black_Square/QuestionLookup.md @@ -113,7 +113,7 @@ Intervals: x x x (1) - {start=0, end=2, length=3} x x x x x (2) - {start=0, end=4, length=5} ``` -Despite that some indexes will contain a list of `right` intervals starting there, the total quantity of `right` intervals is N anyway, so the time and space remain linear. +Despite that some indexes will contain a list of `right` intervals starting there, the total quantity of `right` intervals is N anyway, so the number of insertions and additional space remain linear. --- @@ -180,6 +180,7 @@ private static Interval findLongestIntersection(Interval[] a, Interval[] b) { return longest; } ``` +Note that since `openedRightIntervals` set is a TreeSet now, insertion and removal will take O(log N) time. Total time of tracking `openedRightIntervals` becomes O(N * log N). Space remains O(N). --- @@ -209,7 +210,7 @@ private static Interval findLongestIntersection(Interval[] a, Interval[] b) { - remove the interval of B ending at this index from the set of opened intervals - find the longest intersection among all found intersections -Preparation steps take O(N2). Looping through diagonals is O(N) per se. For each diagonal, extraction and array preparations take O(N). The longest intersection subtask is O(N * log N) totally: looping is O(N), tracking of opened intervals is O(N) in sum, last interval querying takes O(log N) per each iteration. **Total time:** O(N2 * log N). **Total space:** O(N2) - we count zeros using copies of the matrix. Extra memory is spent on each iteration of diagonals loop and is allocated temporarily for that iteration only. It takes O(N), so it can be neglected. +Preparation steps take O(N2). Looping through diagonals is O(N) per se. For each diagonal, extraction and array preparations take O(N). The longest intersection subtask is O(N * log N) totally: looping is O(N), tracking of opened intervals is O(N * log N) in sum, last interval querying takes O(log N) per each iteration. **Total time:** O(N2 * log N). **Total space:** O(N2) - we count zeros using copies of the matrix. Extra memory is spent on each iteration of diagonals loop and is allocated temporarily for that iteration only. It takes O(N), so it can be neglected. Comparing to previous solutions, we managed to achieve an improvement: - [Simple solution](https://github.com/careercup/CtCI-6th-Edition/blob/master/Java/Ch%2017.%20Hard/Q17_23_Max_Black_Square/Question.java): O(N4), space: O(1)