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 {
|
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
|
||||||
|
@ -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
|
* 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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
* 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
|
||||||
*/
|
*/
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
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++) {
|
||||||
|
@ -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