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:
parent
8386ace107
commit
152d9dc3ce
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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++) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user