1

I am in the midst of a project and would like to find all solutions to the android pattern unlock. If you have not seen it before, here it is, with a link to a stack overflow post discussing it in more detail.

The base rules are:

  • Only visit a node 0 or 1 times
  • No jumping over unvisited nodes
  • No cyclic paths

enter image description here

My implementation deals with solving the problem for a N by M grid, with a cap on the max length of a pattern. Here it is:

def get_all_sols(grid_size: (int, int), max_len: int) -> list:
    """
    Return all solutions to the android problem as a list
    :param grid_size: (x, y) size of the grid
    :param max_len: maximum number of nodes in the solution
    """
    sols = []

    def r_sols(current_sol):
        current_y = current_sol[-1] // grid_size[1]        # The solution values are stored as ids ->>   0, 1, 2     for an example 3x3 grid
        current_x = current_sol[-1] - current_y * grid_size[1]  # Cache x and y of last visited node     3, 4, 5
        grid = {}  # Prepping a dict to store options for travelling                                     6, 7, 8
        grid_id = -1

        for y in range(grid_size[1]):
            for x in range(grid_size[0]):
                grid_id += 1
                if grid_id in current_sol:  # Avoid using the same node twice
                    continue
                dist = (x - current_x) ** 2 + (y - current_y) ** 2   # Find some kind of distance, no need to root since all values will be like this
                slope = math.atan2((y - current_y), (x - current_x))  # Likely very slow, but need to hold some kind of slope value,
                # so that jumping over a point can be detected

                # If the option table doesnt have the slope add a new entry with distance and id
                # if it does, check distances and pick the closer one
                grid[slope] = (dist, grid_id) if grid.get(slope) is None or grid[slope][0] > dist else grid[slope]

        # The code matches the android login criteria, since:
        # - Each node is visited either 0 or 1 time(s)
        # - The path cannot jump over unvisited nodes, but can over visited ones
        # - The path is not a cycle

        r_sol = [current_sol]
        if len(current_sol) == max_len:  # Stop if hit the max length and return
            return r_sol

        for _, opt in grid.values():  # Else recurse for each possible choice
            r_sol += r_sols(current_sol + [opt])
        return r_sol

    for start in range(grid_size[0] * grid_size[1]):
        sols += r_sols([start])
    return sols

My current issue is the runtime as the paths or grid get bigger. Could I get some help optimizing the function? For verification, a 4x4 grid should have these path stats:

1 nodes: 16 paths
2 nodes: 172 paths
3 nodes: 1744 paths
4 nodes: 16880 paths
5 nodes: 154680 paths
6 nodes: 1331944 paths
7 nodes: 10690096 paths
Delta qyto
  • 125
  • 8
  • Interesting approach. A couple things... I don't get the answers you post above from your code. Your code has a fault in detecting jumps: In a 3x3 with length 2, your solution contains [0, 7]. I think the jumping will be very difficult to decipher when the grid gets bigger/elongated. – AirSquid Mar 18 '22 at 17:43
  • 1
    @AirSquid [0,7] is a possible solution (I just tried on my phone) although it is not easy to do ;) . – Jérôme Richard Mar 18 '22 at 21:14
  • 1
    @JérômeRichard Well, I don't have an Android so I can't experiment, but if you say top left to bottom middle is possible (overflying the whole middle row, which is unvisited), then I'd (strongly) suggest that the rules of "jumping" are ill defined and might depend on finger size, button size, curly cues, and individual dexterity. :) – AirSquid Mar 18 '22 at 21:20
  • @AirSquid, given that i am matching a prexisting implementation where this is possible, I have defined that any connection with line of sight (given infinitely thin nodes etc) are valid. Thus [0, 7] on a 3x3 is accepted, along with [0, 7] on a 4x4 grid etc – Delta qyto Mar 19 '22 at 10:50

1 Answers1

1

Assuming the algorithm is correct, you can apply some small optimizations. The biggest one is to cut the algorithm earlier by moving the len(current_sol) == max_len earlier. Then, you can compute set(current_sol) so to speed up list searching. Then, you can replace val**2 by val*val and store some temporary result not to recompute them. In fact, every basic operation is slow with CPython and it performs almost no optimization. Here is the resulting code:

def get_all_sols_faster(grid_size: (int, int), max_len: int) -> list:
    sols = []

    def r_sols(current_sol):
        r_sol = [current_sol]

        if len(current_sol) == max_len:
            return r_sol

        current_y = current_sol[-1] // grid_size[1]
        current_x = current_sol[-1] - current_y * grid_size[1]
        grid = {}
        grid_id = -1

        current_sol_set = set(current_sol)
        for y in range(grid_size[1]):
            for x in range(grid_size[0]):
                grid_id += 1
                if grid_id in current_sol_set:
                    continue
                diff_x, diff_y = x - current_x, y - current_y
                dist = diff_x * diff_x + diff_y * diff_y
                slope = math.atan2(diff_y, diff_x)

                tmp = grid.get(slope)
                grid[slope] = (dist, grid_id) if tmp is None or tmp[0] > dist else tmp

        for _, opt in grid.values():
            r_sol += r_sols(current_sol + [opt])
        return r_sol

    for start in range(grid_size[0] * grid_size[1]):
        sols += r_sols([start])
    return sols

This code is about 3 time faster.

Honestly, for such a bruteforce algorithm, CPython is a mess. I think you should use a native compiled language to get a much faster code (certainly at least an order of magnitude faster). Note that counting results instead of producing all the solution should also be faster.

Jérôme Richard
  • 41,678
  • 6
  • 29
  • 59
  • Thank you. What do you mean by counting results? As in returning a count of how many unique solutions exist? – Delta qyto Mar 19 '22 at 10:52
  • Yes, I meant returning `len(sols)`. – Jérôme Richard Mar 19 '22 at 10:55
  • Right now that is not feasible. The program is playing a souped up version of [breaklock](https://www.mathsisfun.com/games/breaklock.html), which requires knowing all possible solutions in order to solve it. I am looking at ways to dynamically generate these paths as the game is played, which would help alleviate the problem. – Delta qyto Mar 19 '22 at 11:05