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:
		@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user