简体   繁体   中英

Python 3 - Recursively finding n-digit code with and without unique digits

I am a Python novice who took one course on the subject last semester, which really sparked my interest. Right now I am trying to figure out some more "advanced" concepts on my own, the first one being recursion. I have been trying an exercise from a handbook I'm following, which basically boils down to: "recursively find a n-digit code consisting out of unique digits between 0 and 9."

This builds upon the previous exercise, which does not have the requirement of unique digits. I solved that one as shown below.

#Ask user for a code
code = input("Insert code as integer:\n")
#Usable digits in the code
digits = ['0','1','2','3','4','5','6','7','8','9']
#Initialization
result = ['' for i in range(len(code))]
counter = 0

def crack_code(digits, pos, code, result):
    global counter
    
    #Fill code
    #For loop tries all digits sequentially
    #Recursion: all positions will be filled
    #0000 , 0001 ... 9998 , 9999
    if pos < len(code):
        for digit in digits:
            result[pos] = digit
            crack_code(digits, pos + 1, code, result)
    
    #Code filled completely
    else:
        counter += 1
    
    #Print matching result
        if ''.join(result) == code:
            print(f"\nCracked code: {''.join(result)}\nCounter: {counter}")
            return
        
import time
t1 = time.time()
crack_code(digits, 0, code, result)
t2 = time.time()
t_tot = t2 - t1
print(f"Computation time: {t_tot:.3f} seconds")

However, upon visualizing this code on PythonTutor (a website you guys are likely familiar with) it turns out that this code is not efficient at all. Upon asking to find eg "29" it continues all the way to 99 before quitting. It will always exhausts all possibilities, even after the correct one has been found.

Question 1: [SOLVED by joao] how can I incorporate a stop once the result has been found? I though I did that by including a return in the #Print matching result block ...

Question 2: how could I include the constraint of unique digits? I tried:

if pos < len(code):
    for digit in digits:
        if digit not in result:
            result[pos] = digit
        crack_code(digits, pos + 1, code, result)

Which worked as long as the digits are sorted, ie 1234, 2345 but not 7654, 9172... I feel like I'm somewhat on the right track but not quite there. This version also does not appear any quicker than the unconstrained one (likely because all options are tried anyway as mentioned before).

Question 3: when inputting 6789 as the code, I get a very strange output, which I can not explain. However, this only happens in the constrained version.

Insert code as integer:
6789

Cracked code:6789
Counter:6790

Cracked code:6789
Counter:6800

Cracked code:6789
Counter:6890

Cracked code:6789
Counter:6900

Cracked code:6789
Counter:6990

Cracked code:6789
Counter:7000

Cracked code:6789
Counter:7790

Cracked code:6789
Counter:7800

Cracked code:6789
Counter:7890

Cracked code:6789
Counter:7900

Cracked code:6789
Counter:7990

Cracked code:6789
Counter:8000

Cracked code:6789
Counter:8790

Cracked code:6789
Counter:8800

Cracked code:6789
Counter:8890

Cracked code:6789
Counter:8900

Cracked code:6789
Counter:8990

Cracked code:6789
Counter:9000

Cracked code:6789
Counter:9790

Cracked code:6789
Counter:9800

Cracked code:6789
Counter:9890

Cracked code:6789
Counter:9900

Cracked code:6789
Counter:9990

Cracked code:6789
Counter:10000
Computation time: 0.018 seconds

Any suggestions would be greatly appreciated!

Thanks for your time!

Ben

Answering Question1 : this is an ideal use case for exceptions .

The problem with 'return' is that your recursion has piled up many calls to crack_code, and return only brings you back one level, to where you last called crack_code. Since that's inside a 'for' loop, it'll continue looping.

To break out of all the crack_code calls, define a new exception class:

class MyException(Exception):
    pass

(name it whatever you want). When you have found the result, instead of return use this:

raise MyException()

This will propagate up the entire call stack. If you leave it like this, your program will end (which is what you want) with an ugly error message and stack trace (that's messy), so to finish gracefully you need to catch the exception:

import time
t1 = time.time()
try:
    crack_code(digits, 0, code, result)
except MyException:
    # We go through here only if we cracked the code
    print('found!')
# We execute this in every case
t2 = time.time()
t_tot = t2 - t1
print(f"Computation time: {t_tot:.3f} seconds")

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM