2

I want to write a function which generates all possible numbers from a standard phone keypad (figure 1), using following set of rules:

  • phone numbers begin with the digit 2
  • phone number are 10 digit long
  • successive digits in each phone number are chosen as a knight moves in chess

In chess, a knight (sometimes called a horse) moves two steps vertically and one step horizontally OR two steps horizontally and one step vertically.

enter image description here

Only numerical digits can be used in phone numbers - i.e. the (#) and (*) keys are not allowed.

The function has to take length of the phone number and initial position as input and for the output gives the number of unique phone numbers.

I am a newbie and facing difficulty to build the logic. I tried to do it as follow which is definitely not a right approach.

def genNumbers(len, initpos):
numb = list('2xxxxxxxxx')

#index 1
numb[1] = 7 or 9

if numb[1] == 7:
    numb[2] == 2 or 6
elif numb[1] == 9:
    numb[2] == 2 or 4

#index 2
if numb[2]== 2:
    numb[3] == 7 or 9
elif numb[2]== 4:
    numb[3] == 3 or 9
elif numb[2]== 6:
    numb[3] == 1 or 7

#index 3
if numb[3]== 1:
    numb[4] == 6 or 8  
elif numb[3]== 3:
    numb[4] == 4 or 8 
elif numb[3]== 7:
    numb[4] == 2 or 6 
elif numb[3]== 9:
    numb[4] == 2 or 4 

#index 4
if numb[4] == 8:
    numb[5]== 1 or 3
elif numb[4] == 2:
    numb[5]== 7 or 9
elif numb[4] == 4:
    numb[5]== 3 or 9
elif numb[4] == 6:
    numb[5]== 1 or 7

#index 5
if numb[5] == 1:
    numb[6]== 6 or 8
elif numb[5] == 3:
    numb[6]== 4 or 8
elif numb[5] == 7:
    numb[6]== 2 or 6 
elif numb[5] == 9:
    numb[6]== 2 or 4

#index 6 
if numb[6] == 2:
    numb[7]== 7 or 9
elif numb[6] == 4:
    numb[7]== 3 or 9 
elif numb[6] == 6:
    numb[7]== 1 or 7
elif numb[6] == 8:
    numb[7]== 1 or 3

#index 7 
if numb[7] == 1:
    numb[8]== 6 or 8
elif numb[7] == 3:
    numb[8]== 4 or 8
elif numb[7] == 7:
    numb[8]== 2 or 6   
elif numb[7] == 9:
    numb[8]== 2 or 4

#index 8
if numb[8] == 6:
    numb[9]== 1 or 7
elif numb[8] == 8:
    numb[9]== 1 or 3
elif numb[8] == 4:
    numb[9]== 3 or 9
elif numb[8] == 2:
    numb[9]== 7 or 9


return numb

Any help would be highly appreciated!

FJ_Abbasi
  • 413
  • 2
  • 5
  • 16
  • 2
    This is not only probably not the right approach, but also lacks some basic understanding of how a computer program works. You can't simply assign one _or_ another value to a variable, hoping the computer now understands that this variable shall not have other values than these two... Second: assigning is `=`, comparing is `==`. Besides that, two comments at the described problem: nice logic challenge, but a little strange that no phone number will contain a `5`. And: you should try to get in touch with a concept called `recursive functions` – SpghttCd Dec 09 '18 at 22:41
  • 1
    Here's a good write up of this problem that discusses several solutions in detail: https://hackernoon.com/google-interview-questions-deconstructed-the-knights-dialer-f780d516f029 – Patrick Haugh Dec 10 '18 at 00:37

2 Answers2

4

Prelude

Lets state another way to solve your problem that does not involve Linear Algebra but still rely on Graph Theory.

Representation

A natural representation of your problem is a Graph as shown below:

enter image description here

And is equivalent to:

enter image description here

We can represent this Graph by a dictionary:

G = {
    0: [4, 6],
    1: [6, 8],
    2: [7, 9],
    3: [4, 8],
    4: [0, 3, 9],
    5: [],  # This vertex could be ignored because there is no edge linked to it
    6: [0, 1, 7],
    7: [2, 6],
    8: [1, 3],
    9: [2, 4],
}

This kind of structure will spare you the need of writing if statements.

Adjacency Matrix

The representation above contains the same information than the Adjacency Matrix. Further more, we can generate it from the structure above (conversion of a Boolean Sparse Matrix into an Integral Matrix):

def AdjacencyMatrix(d):
    A = np.zeros([len(d)]*2)
    for i in d:
        for j in d[i]:
            A[i,j] = 1
    return A

C = AdjacencyMatrix(G)
np.allclose(A, C) # True

Where A is the Adjacency Matrix defined in the another answer.

Recursion

Now we can generate all phone numbers using recursion:

def phoneNumbers(n=10, i=2, G=G, number='', store=None):
    if store is None:
        store = list()
    number += str(i)
    if n > 1:
        for j in G[i]:
            phoneNumbers(n=n-1, i=j, G=G, number=number, store=store)
    else:
        store.append(number)
    return store

Then we build the phone numbers list:

plist = phoneNumbers(n=10, i=2)

It returns:

['2727272727',
 '2727272729',
 '2727272760',
 '2727272761',
 '2727272767',
 ...
 '2949494927',
 '2949494929',
 '2949494940',
 '2949494943',
 '2949494949']

Now it is just about taking the length of the list:

len(plist) # 1424

Checks

We can check there is no duplicates:

len(set(plist)) # 1424

We can check than the observation we have made about last digit in the another answer still holds in this version:

d = set([int(n[-1]) for n in plist]) # {0, 1, 3, 7, 9}

Phone numbers cannot end with:

set(range(10)) - d # {2, 4, 5, 6, 8}

Comparison

This second answer:

  • Does not require numpy (no need of Linear Algebra), it makes only use of Python Standard Library;
  • Does use a Graph representation because it is a natural representation of your problem;
  • Generates all phone numbers before counting them, the previous answer did not generate all of them, we only had details on numbers in the form x********y;
  • Makes use recursion to solve the problem and seems to have an exponential time complexity, if we don't need the phone numbers to be generated we should use the Matrix Power version.

Benchmark

The complexity of recursive function should be bounded between O(2^n) and O(3^n) because the recursion tree has a depth of n-1 (and all branches has the same depth) and each inner node creates minimum 2 edges and at maximum 3 edges. The methodology here is not divide-and-conquer algorithm it is a Combinatorics string generator, this is why we expect the complexity to be exponential.

Benchmarking two functions seems to validate this claim:

enter image description here

The recursive function shows a linear behavior in logarithmic scale which confirms an exponential complexity and is bounded as stated. Worse, in addition with computation, it will require a growing amount of memory to store the list. I could not get any further than n=23, then my laptop freezes before having the MemoryError. A better estimation of complexity is O((20/9)^n) where the base is equal to the mean of vertices degrees (disconnected vertices are ignored).

The Matrix Power method seems to have a constant time versus the problem size n. There is no implementation details on numpy.linalg.matrix_power documentation but this is a known eigenvalues problem. Therefore we can explain why the complexity seems to be constant before n. It is because the matrix shape is independent of n (it remains a 10x10 matrix). Most of the computation time is dedicated to solve the eigenvalues problem and not to raise a diagonal eigenvalues matrix to the n-th power which is a trivial operation (and the only dependence to n). This why this solution performs with a "constant time". Further more, it will also requires a quasi constant amount of memory to store the Matrix and its decomposition, but this is also independent of n.

Bonus

Find below the code used to benchmark functions:

import timeit

nr = 20
ns = 100
N = 15
nt = np.arange(N) + 1
t = np.full((N, 4), np.nan)
for (i, n) in enumerate(nt):
    t[i,0] = np.mean(timeit.Timer("phoneNumbersCount(n=%d)" % n, setup="from __main__ import phoneNumbersCount").repeat(nr, number=ns))
    t[i,1] = np.mean(timeit.Timer("len(phoneNumbers(n=%d, i=2))" % n, setup="from __main__ import phoneNumbers").repeat(nr, number=ns))
    t[i,2] = np.mean(timeit.Timer("len(phoneNumbers(n=%d, i=0))" % n, setup="from __main__ import phoneNumbers").repeat(nr, number=ns))
    t[i,3] = np.mean(timeit.Timer("len(phoneNumbers(n=%d, i=6))" % n, setup="from __main__ import phoneNumbers").repeat(nr, number=ns))
    print(n, t[i,:])
jlandercy
  • 7,183
  • 1
  • 39
  • 57
  • what would be time complexity of this function? can you please shed some light on that? – FJ_Abbasi Dec 11 '18 at 14:16
  • @FJ_Abbasi, This is a totally different question. Let me think a bit about it. You may also want to ask it on https://mathoverflow.net/ which is a community that handles this kind of question. If you do so, cross link the questions (makes posts pointing to each other) I will be glad to see another feedback on this new question. – jlandercy Dec 11 '18 at 14:37
  • @FJ_Abbasi, Matrix Product is bounded by `O(n ^3)`. And, I think my first guest was correct: the recursion three has a depth of `n-1` and create at each inner node 3 edges at maximum, assuming other operations are `O(1)`, the global complexity must be bounded by `O(3^n)`. Not a divided-and-conquer algorithm but a combinatorics string generator. The later solution will be less efficient as `n` grows. – jlandercy Dec 11 '18 at 14:44
  • I am going to post it on mathoverflow.net. So you think it has a time complexity of O(3^n)? – FJ_Abbasi Dec 11 '18 at 14:59
  • It seems weird at the first glance, awful performance, but if you draw the recursion tree you will see that it is about `3^(n-1)` nodes at the end. It would be interesting to check this by professional mathematician. It is long time ago I had computed time complexity, there you will get an accurate answer. I will benchmark the two functions in order to see if this asymptotic behavior shows up. – jlandercy Dec 11 '18 at 15:04
  • @FJ_Abbasi, I think the complexity is exponential. My first solution behaves better. Please send me the link of your new question if you ask it on MSO. – jlandercy Dec 11 '18 at 15:48
  • @FJ_Abbasi, time complexity can be nicely estimated by `(20/9)^n`. I think the recursion function is already memoized because it does not create any unnecessary slot, string grows and are passed by reference and are stored to the list when it is complete. Anyway the only benefit you get will be about the memory allocation, you still need to generate the recursion tree (call stack). Definitely, with the function signature you provided (no need to generate numbers), I will pick the Matrix Power version. Interesting problem, I had a lot of fun investigating it. – jlandercy Dec 12 '18 at 09:15
  • Even with the conclusion and comments the less performant anwser/methodology is marked as the answer and the most upvoted post. This is odd. The answer should be the https://stackoverflow.com/a/53697489/3067485 for the given OP. – jlandercy Sep 03 '22 at 06:22
3

Function Signature

The function has to take length of the phone number and initial position as input and for the output gives the number of unique phone numbers.

Key Idea

Your question can be addressed by Graph Theory and Linear Algebra (an interesting place where those disciplines meet is Discrete Mathematics).

First we create an Adjacency Matrix representing legal moves on the phone keyboard:

import numpy as np

A = np.zeros((10, 10))
A[0,4]=1
A[0,6]=1
A[1,6]=1
A[1,8]=1
A[2,7]=1
A[2,9]=1
A[3,4]=1
A[3,8]=1
A[4,0]=1
A[4,3]=1
A[4,9]=1
A[6,0]=1
A[6,1]=1
A[6,7]=1
A[7,2]=1
A[7,6]=1
A[8,1]=1
A[8,3]=1
A[9,2]=1
A[9,4]=1

We can check that the matrix is symmetric (not required but it is a property of the system):

np.allclose(A, A.T) # True

Entry of the Adjacency Matrix reads as this: A[0,4]=1 means there is a move from vertex 0 to vertex 4 and A[0,5]=0 means there is no move from 0 to 5.

[[0. 0. 0. 0. 1. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 1. 0. 1. 0.]
 [0. 0. 0. 0. 0. 0. 0. 1. 0. 1.]
 [0. 0. 0. 0. 1. 0. 0. 0. 1. 0.]
 [1. 0. 0. 1. 0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [1. 1. 0. 0. 0. 0. 0. 1. 0. 0.]
 [0. 0. 1. 0. 0. 0. 1. 0. 0. 0.]
 [0. 1. 0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 1. 0. 1. 0. 0. 0. 0. 0.]]

Then we compute A raised at the power 9 which gives us the number of walks of length 9 (this correspond to the count of unique phone numbers of length 10) between two given vertices (starting with digit x and ending with digit y):

W = np.linalg.matrix_power(A, 9)

Path length is n-1 because vertices are numbers and edges are moves on the keyboard, thus to dial a 10-digits phone number you need 9 moves (walks of length 9).

It gives us:

[[  0.   0. 336.   0. 544.   0. 544.   0. 336.   0.]
 [  0.   0. 264.   0. 432.   0. 448.   0. 280.   0.]
 [336. 264.   0. 264.   0.   0.   0. 280.   0. 280.]
 [  0.   0. 264.   0. 448.   0. 432.   0. 280.   0.]
 [544. 432.   0. 448.   0.   0.   0. 432.   0. 448.]
 [  0.   0.   0.   0.   0.   0.   0.   0.   0.   0.]
 [544. 448.   0. 432.   0.   0.   0. 448.   0. 432.]
 [  0.   0. 280.   0. 432.   0. 448.   0. 264.   0.]
 [336. 280.   0. 280.   0.   0.   0. 264.   0. 264.]
 [  0.   0. 280.   0. 448.   0. 432.   0. 264.   0.]]

Matrix W entry reads as: W[2,1] = 264 means there is 264 phone numbers of length 10 starting with 2 and ending with 1.

Now we sum up walks starting from vertex 2:

np.sum(W[2,:]) # 1424.0

There are 1424 phone numbers of length 10 starting with digit 2 for the set of rules you provided.

Function

The function is then trivial to write:

def phoneNumbersCount(n=10, i=2, A=A):
    return np.sum(np.linalg.matrix_power(A, n-1)[i,:])

Most part of the job consists to encode the Matrix which describes the set of rules (allowed moves on the keyboard).

Checks

Based on observations we can derive from the problem description, such as @SpghttCd did, we can check that there is no number of length 10 starting from 2 containing the digit 5:

W[2,5] # 0.0

We can check that no number of length 10 can be written starting from 5:

phoneNumbersCount(10, 5) # 0.0

Actually digit 5 is not available at all for the given set of rules.

We can also check another properties that are not obvious, eg.: There is no number of length 10 starting with 2 and ending by either 2, 4, 5, 6 or 8:

W[2,:] # [336. 264.   0. 264.   0.   0.   0. 280.   0. 280.]

Hint

Because the Graph is not oriented (each edge exists in both direction), the Adjacency Matrix is symmetric. Therefore the matrix creation can be reduced to:

B = np.zeros((10, 10))
B[0,4]=1
B[0,6]=1
B[1,6]=1
B[1,8]=1
B[2,7]=1
B[2,9]=1
B[3,4]=1
B[3,8]=1
B[4,9]=1
B[6,7]=1
B = np.maximum(B, B.T)

References

Some useful references to understand how and why it works:

jlandercy
  • 7,183
  • 1
  • 39
  • 57