From 152d9dc3ce48e7ccc0e6f38545b4bd0c3705e701 Mon Sep 17 00:00:00 2001 From: maddiebaka Date: Thu, 28 Mar 2024 17:00:25 -0400 Subject: [PATCH] Nearest Neighbor solver, basic caching Add a nearest neighbor solver (WriteAwareBruteSolver) and some basic caching to the Java API adapter. Fix a comment typo with an off-by-one error --- build.gradle.kts | 1 + .../interview/IntegerLevenshtein.java | 62 +++++++++++++++++++ .../cleverthis/interview/PadlockAdapter.java | 2 +- .../interview/PadlockJavaAdapter.java | 30 ++++++++- .../interview/WriteAwareBruteSolver.java | 55 ++++++++++++++++ .../interview/DumbBruteSolverTest.java | 11 +++- .../interview/PerformanceAnalyze.java | 12 +++- .../cleverthis/interview/SolutionTest.java | 13 ---- .../interview/SolutionTestBase.java | 4 ++ .../interview/WriteAwareBruteSolverTest.java | 55 ++++++++++++++++ 10 files changed, 225 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/cleverthis/interview/IntegerLevenshtein.java create mode 100644 src/main/java/com/cleverthis/interview/WriteAwareBruteSolver.java delete mode 100644 src/test/java/com/cleverthis/interview/SolutionTest.java create mode 100644 src/test/java/com/cleverthis/interview/WriteAwareBruteSolverTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 89a6138..b7b7574 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ repositories { dependencies { implementation(project(":padlock-impl")) + implementation("org.apache.commons", "commons-text", "1.11.0") // if you ever need import more dependencies, following this format: // implementation("group-id:project-id:version") // just like the logback classic diff --git a/src/main/java/com/cleverthis/interview/IntegerLevenshtein.java b/src/main/java/com/cleverthis/interview/IntegerLevenshtein.java new file mode 100644 index 0000000..73c856b --- /dev/null +++ b/src/main/java/com/cleverthis/interview/IntegerLevenshtein.java @@ -0,0 +1,62 @@ +package com.cleverthis.interview; + +import org.apache.commons.text.similarity.LevenshteinDistance; + +/** + * A wrapper class that holds possible padlock passcode permutations in an integer array. This class + * overloads the toString() method and is comparable, using its string value to calculate the levenshtein distance and + * use those distance values for sorting into a data structure. + */ +public class IntegerLevenshtein implements Comparable { + private Integer[] integerData; + private Integer size; + + /** + * Creates a new IntegerLevenshtein + * @param size The number of integers (keypad size) of the passcode + */ + public IntegerLevenshtein(int size) { + this.integerData = new Integer[size]; + this.size = size; + } + + /** + * Sets the integer data array + * @param integerData The new integer data + */ + public void setIntegerData(Integer[] integerData) { + this.integerData = integerData.clone(); + } + + /** + * Gets the integer data array + * @return the integer data + */ + public Integer[] getIntegerData() { + return this.integerData; + } + + /** + * Casts each integer to a string and concatenates them together into a single string. + * For example, an internal integer array of [1,2,3,4] will be returned as the string "1234". + * @return A string representation of the integer array. + */ + public String toString() { + String temp = new String(); + for(int i = 0; i < size; i++) { + temp = temp.concat(integerData[i].toString()); + } + return temp; + } + + /** + * Overriden compareTo method that compares the calculated levenshtein distance between two IntegerLevenshtein objects + * @param otherInteger the object to be compared. + * @return The levenshtein distance between the two objects. + */ + @Override + public int compareTo(IntegerLevenshtein otherInteger) { + LevenshteinDistance levenshtein = LevenshteinDistance.getDefaultInstance(); + return levenshtein.apply(this.toString(), otherInteger.toString()); + } +} diff --git a/src/main/java/com/cleverthis/interview/PadlockAdapter.java b/src/main/java/com/cleverthis/interview/PadlockAdapter.java index 04594cf..b7981f1 100644 --- a/src/main/java/com/cleverthis/interview/PadlockAdapter.java +++ b/src/main/java/com/cleverthis/interview/PadlockAdapter.java @@ -15,7 +15,7 @@ public interface PadlockAdapter { /** * Write key presses to the input buffer of the padlock - * @param address The position / index of the button that is pressed. For example, address 1 is the first button pressed. + * @param address The position / index of the button that is pressed. For example, address 0 is the first button pressed. * @param keyIndex The value of the button that is pressed. Cannot be greater than the numpad size, as the buttons increment * sequentially * @return The old value of keyIndex at the inputted address diff --git a/src/main/java/com/cleverthis/interview/PadlockJavaAdapter.java b/src/main/java/com/cleverthis/interview/PadlockJavaAdapter.java index 6fb2e76..6602b67 100644 --- a/src/main/java/com/cleverthis/interview/PadlockJavaAdapter.java +++ b/src/main/java/com/cleverthis/interview/PadlockJavaAdapter.java @@ -4,9 +4,16 @@ import com.cleverthis.interview.padlock.PadlockImpl; /** * The concrete implementation of PadlockAdapter that communicates with the padlock directly through a Java - * class + * class. This implementation also contains a cache that it keeps in sync with the underlying API, as a front-line + * optimization against unnecessary write operations. */ public class PadlockJavaAdapter extends PadlockImpl implements PadlockAdapter { + /** + * Intermediate cache between the underlying padlock API and any code that interfaces with the PadlockAdapter + * API. + */ + private final Integer[] inputBufferState; + /** * Create a padlock instance. * @@ -15,9 +22,28 @@ public class PadlockJavaAdapter extends PadlockImpl implements PadlockAdapter { public PadlockJavaAdapter(int numpadSize) { super(numpadSize); + inputBufferState = new Integer[numpadSize]; + for(int i = 0; i < this.getNumpadSize(); i++) { - this.writeInputBuffer(i, i); + inputBufferState[i] = i; + super.writeInputBuffer(i, i); } } + /** + * Writes to the underlying input buffer, but only if the cache shows that the write is necessary + * @param address The position / index of the button that is pressed. For example, address 0 is the first button pressed. + * @param keyIndex The value of the button that is pressed. Cannot be greater than the numpad size, as the buttons increment + * sequentially + * @return The old value that was replaced, which is the same as keyIndex if a write operation doesn't actually occur. + */ + @Override + public Integer writeInputBuffer(int address, int keyIndex) { + if(inputBufferState[address] != keyIndex) { + inputBufferState[address] = keyIndex; + return super.writeInputBuffer(address, keyIndex); + } else { + return keyIndex; + } + } } diff --git a/src/main/java/com/cleverthis/interview/WriteAwareBruteSolver.java b/src/main/java/com/cleverthis/interview/WriteAwareBruteSolver.java new file mode 100644 index 0000000..eef0e53 --- /dev/null +++ b/src/main/java/com/cleverthis/interview/WriteAwareBruteSolver.java @@ -0,0 +1,55 @@ +package com.cleverthis.interview; + +import java.util.Iterator; +import java.util.TreeSet; +import org.apache.commons.text.similarity.LevenshteinDistance; + +/** + * This is a write-aware brute solver implementation that uses the levenshtein distance to sort passcode permutations + * into a tree. Walking the tree in-order (Likely a depth-first traversal internally) results in a naive nearest-neighbor + * optimization that generally performs well, but may perform poorly in some cases. + * + * This permutation optimization problem is NP-hard and a perfect brute-force solution has a worst-case running time that + * is super-polynomial. Heuristic solutions exist that have acceptable performance, and this is a more naive heuristic + * implementation. + */ +public class WriteAwareBruteSolver extends DumbBruteSolver { + + private final TreeSet orderedTree; + private final Integer numpadSize; + + public WriteAwareBruteSolver(int numpadSize) { + orderedTree = new TreeSet(); + this.numpadSize = numpadSize; + + Integer[] currentPermutation = new Integer[numpadSize]; + for(int i = 0; i < numpadSize; i++) { + currentPermutation[i] = i; + } + + boolean morePermutationsExist = true; + + do { + IntegerLevenshtein levenshteinPermutation = new IntegerLevenshtein(numpadSize); + levenshteinPermutation.setIntegerData(currentPermutation); + orderedTree.add(levenshteinPermutation); + morePermutationsExist = this.calculateNextPermutation(currentPermutation, numpadSize); + } while(morePermutationsExist); + } + + public void solve(PadlockAdapter padlockAdapter) { + int numpadSize = padlockAdapter.getNumpadSize(); + + Iterator iterator = orderedTree.iterator(); + + while(iterator.hasNext()) { + if(this.checkPermutation(iterator.next().getIntegerData(), padlockAdapter)) { + return; + } + } + } + + public Integer getTreeSize() { + return this.orderedTree.size(); + } +} diff --git a/src/test/java/com/cleverthis/interview/DumbBruteSolverTest.java b/src/test/java/com/cleverthis/interview/DumbBruteSolverTest.java index 8ce7f50..1dc715f 100644 --- a/src/test/java/com/cleverthis/interview/DumbBruteSolverTest.java +++ b/src/test/java/com/cleverthis/interview/DumbBruteSolverTest.java @@ -9,8 +9,17 @@ import static org.junit.jupiter.api.Assertions.*; * * Tests that lexicographically ordered permutations are properly calculated, and that the solver works */ -public class DumbBruteSolverTest { +public class DumbBruteSolverTest extends SolutionTestBase { + /** + * Overridden method that tests in SolutionTestBase.class expect defined and calls. + * Creates a DumbBruteSolver and calls the solve() method. + * @param padlock The padlock to solve + */ + @Override + protected void solve(PadlockAdapter padlock) { + new DumbBruteSolver().solve(padlock); + } /** * Check whether a 7-button padlock can be solved */ diff --git a/src/test/java/com/cleverthis/interview/PerformanceAnalyze.java b/src/test/java/com/cleverthis/interview/PerformanceAnalyze.java index d75a520..6c74c8c 100644 --- a/src/test/java/com/cleverthis/interview/PerformanceAnalyze.java +++ b/src/test/java/com/cleverthis/interview/PerformanceAnalyze.java @@ -6,12 +6,18 @@ import com.cleverthis.interview.padlock.PadlockImpl; * Performance test but not mean to run in unit test. */ public class PerformanceAnalyze { - private static void solve(PadlockAdapter padlock) { - new DumbBruteSolver().solve(padlock); - } private static final int TOTAL_RUN = 500; private static final int NUMPAD_SIZE = 9; + /** + * The solver object reference is held between tests, because the data structure used for optimization does not + * change between runs and keeping it in memory greatly increases performance. + */ + static WriteAwareBruteSolver solver = new WriteAwareBruteSolver(NUMPAD_SIZE); + + private static void solve(PadlockAdapter padlock) { + solver.solve(padlock); + } static { System.out.println("Total run: " + TOTAL_RUN); diff --git a/src/test/java/com/cleverthis/interview/SolutionTest.java b/src/test/java/com/cleverthis/interview/SolutionTest.java deleted file mode 100644 index ca37214..0000000 --- a/src/test/java/com/cleverthis/interview/SolutionTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.cleverthis.interview; - -import com.cleverthis.interview.padlock.PadlockImpl; - -/** - * This is a simple placeholder to show how unit test works. - */ -class SolutionTest extends SolutionTestBase { - @Override - protected void solve(PadlockAdapter padlock) { - new DumbBruteSolver().solve(padlock); - } -} \ No newline at end of file diff --git a/src/test/java/com/cleverthis/interview/SolutionTestBase.java b/src/test/java/com/cleverthis/interview/SolutionTestBase.java index 2920a85..ed715e3 100644 --- a/src/test/java/com/cleverthis/interview/SolutionTestBase.java +++ b/src/test/java/com/cleverthis/interview/SolutionTestBase.java @@ -21,6 +21,10 @@ public abstract class SolutionTestBase { assertTrue(padlock.isPasscodeCorrect()); } + /** + * Tests padlocks with numpad sizes of 1 to 7. This test runs for every class that extends this SolutionTestBase + * abstract class. + */ @Test void verify1to7() { for (int i = 1; i <= 7; i++) { diff --git a/src/test/java/com/cleverthis/interview/WriteAwareBruteSolverTest.java b/src/test/java/com/cleverthis/interview/WriteAwareBruteSolverTest.java new file mode 100644 index 0000000..15eecaf --- /dev/null +++ b/src/test/java/com/cleverthis/interview/WriteAwareBruteSolverTest.java @@ -0,0 +1,55 @@ +package com.cleverthis.interview; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for WriteAwareBruteSolver class + * + * These tests do not keep a long-lived reference to WriteAwareBruteSolver, instead opting + * to reconstruct the permutation tree for every test. This is a trade-off between performance + * and avoiding any inadvertent behavior changes from state persisting between tests. + */ +public class WriteAwareBruteSolverTest extends SolutionTestBase { + + /** + * Overridden method that tests in SolutionTestBase.class expect defined and calls. + * Creates a WriteAwareBruteSolver and calls the solve() method. + * @param padlock The padlock to solve + */ + @Override + protected void solve(PadlockAdapter padlock) { + new WriteAwareBruteSolver(padlock.getNumpadSize()).solve(padlock); + } + + /** + * Tests whether the solver can brute-force a 7-numpad padlock. + */ + @Test + protected void testSolver() { + Integer numpadSize = 7; + WriteAwareBruteSolver writeAwareSolver = new WriteAwareBruteSolver(numpadSize); + PadlockJavaAdapter padlock = new PadlockJavaAdapter(numpadSize); + + writeAwareSolver.solve(padlock); + + assertTrue(padlock.isPasscodeCorrect()); + } + + /** + * Tests whether a numpad-size of 4 is correctly expanded to 24 possible permutations in the underlying + * tree + */ + @Test + protected void testTreeSize() { + Integer numpadSize = 4; + WriteAwareBruteSolver writeAwareSolver = new WriteAwareBruteSolver(numpadSize); + PadlockJavaAdapter padlock = new PadlockJavaAdapter(4); + + writeAwareSolver.solve(padlock); + + assertEquals(writeAwareSolver.getTreeSize(), 24); + } +}