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
This commit is contained in:
maddiebaka 2024-03-28 17:00:25 -04:00
parent 8386ace107
commit 152d9dc3ce
10 changed files with 225 additions and 20 deletions

View File

@ -13,6 +13,7 @@ repositories {
dependencies { dependencies {
implementation(project(":padlock-impl")) implementation(project(":padlock-impl"))
implementation("org.apache.commons", "commons-text", "1.11.0")
// if you ever need import more dependencies, following this format: // if you ever need import more dependencies, following this format:
// implementation("group-id:project-id:version") // implementation("group-id:project-id:version")
// just like the logback classic // just like the logback classic

View File

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

View File

@ -15,7 +15,7 @@ public interface PadlockAdapter {
/** /**
* Write key presses to the input buffer of the padlock * 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 * @param keyIndex The value of the button that is pressed. Cannot be greater than the numpad size, as the buttons increment
* sequentially * sequentially
* @return The old value of keyIndex at the inputted address * @return The old value of keyIndex at the inputted address

View File

@ -4,9 +4,16 @@ import com.cleverthis.interview.padlock.PadlockImpl;
/** /**
* The concrete implementation of PadlockAdapter that communicates with the padlock directly through a Java * 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 { 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. * Create a padlock instance.
* *
@ -15,9 +22,28 @@ public class PadlockJavaAdapter extends PadlockImpl implements PadlockAdapter {
public PadlockJavaAdapter(int numpadSize) { public PadlockJavaAdapter(int numpadSize) {
super(numpadSize); super(numpadSize);
inputBufferState = new Integer[numpadSize];
for(int i = 0; i < this.getNumpadSize(); i++) { 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;
}
}
} }

View File

@ -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<IntegerLevenshtein> orderedTree;
private final Integer numpadSize;
public WriteAwareBruteSolver(int numpadSize) {
orderedTree = new TreeSet<IntegerLevenshtein>();
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<IntegerLevenshtein> iterator = orderedTree.iterator();
while(iterator.hasNext()) {
if(this.checkPermutation(iterator.next().getIntegerData(), padlockAdapter)) {
return;
}
}
}
public Integer getTreeSize() {
return this.orderedTree.size();
}
}

View File

@ -9,8 +9,17 @@ import static org.junit.jupiter.api.Assertions.*;
* *
* Tests that lexicographically ordered permutations are properly calculated, and that the solver works * 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 * Check whether a 7-button padlock can be solved
*/ */

View File

@ -6,12 +6,18 @@ import com.cleverthis.interview.padlock.PadlockImpl;
* Performance test but not mean to run in unit test. * Performance test but not mean to run in unit test.
*/ */
public class PerformanceAnalyze { public class PerformanceAnalyze {
private static void solve(PadlockAdapter padlock) {
new DumbBruteSolver().solve(padlock);
}
private static final int TOTAL_RUN = 500; private static final int TOTAL_RUN = 500;
private static final int NUMPAD_SIZE = 9; 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 { static {
System.out.println("Total run: " + TOTAL_RUN); System.out.println("Total run: " + TOTAL_RUN);

View File

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

View File

@ -21,6 +21,10 @@ public abstract class SolutionTestBase {
assertTrue(padlock.isPasscodeCorrect()); 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 @Test
void verify1to7() { void verify1to7() {
for (int i = 1; i <= 7; i++) { for (int i = 1; i <= 7; i++) {

View File

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