Search code examples
pythonalgorithmdepth-first-searchbacktracking

Problem counting screen locking patterns with backtracking


The problem is counting the number of paths of length n from a given vertex in a graph like the used to unlock Android devices.I'm trying to use backtracking to solve it but I don't get right, I'm I'm still learning how to use it. So here is some code I've been trying

G = {
    'a': set('bed'),
    'b': set('cfeda'),
    'c': set('feb'),
    'd': set('abehg'),
    'e': set('bcfihgda'),
    'f': set('ciheb'),
    'g': set('deh'),
    'h': set('efigd'),
    'i': set('fhe')
}

result = 0


def count_patterns(node, length):
    if length == 1:
        return len(G[node])
    global result
    for neighbor in G[node]:
        result += count_patterns(neighbor, length - 1) - 1
    return result

I expect the count_patterns('a',2) to return 15 and it does return it; however, for n>2 all the results are wrong by far. I think it must be that I don't actually getting track of the node visited, for example if takes this route for n = 3 a -> b -> c when it backtracks to a -> b it can take a -> b -> a which is wrong so it cannot take the parent of the node as a neighbor, I know the problem but I don't know how to fix it.


Solution

  • First of all, you don't need the last -1. So,

    result += count_patterns(neighbor, length - 1) - 1

    Should become

    result += count_patterns(neighbor, length - 1)

    The main problem with your code is that if you go, for example, from a->b and then b->a, you count this as a path of length 2. But it's not. A path shouldn't have repeated vertices. There are two ways you can deal with this: (I will only mention the main idea)

    1. Have a global visited array that has boolean values (true or false). If you have n nodes, this array should have a capacity as much as the number of nodes. Then, you change your code as follows: (pseudocode)

    ``

    def count_patterns(node, length):
        if length == 1:
            return len(G[node])
        global result
        for neighbor in G[node]:
            if neighbor is not visited
                 mark neighbor as visited
                 result += count_patterns(neighbor, length - 1)
                 mark neighbor as unvisited //This is very important
        return result
    

    ``

    The reason that you need to "mark neighbor as unvisited" is because you don't want to repeat a vertex in a specific branch; but you want to be able to use it on another path after you have returned from your recursive call.

    1. You can just pass a third argument to your function which is a list of the vertices you've picked so far; then you only pick a new vertex if you it's not in the list. And you update the list as well:

    ``

    def count_patterns(node, length, list):
        if length == 1:
            return len(G[node])
        global result
        for neighbor in G[node]:
    
            if neighbor is not in list
    
                 result += count_patterns(neighbor, length - 1, list.append(neighbor))
        return result
    

    ``

    I personally, prefer the first way because it's gonna be faster and simpler.