The time complexity of the brute force algorithm is o(n) if i am not mistaken. I found a way to make it a o(log10(n)) which I hope is fast enough. The algorithm works thanks to the folowing observation:
Let f(x) be the function we want.
f(20) = 2f(10)
f(30) = 3f(10)
but f(100) = 9f(10) + 10
because every number between 70 and 80 needs to be counted.
We can deduce two important properties:
- f(x10y) = xf(10y)
if x < 7 and
- f(10x) = 9f(10x-1)+10x-1
which can be solved for our case:
But, this function only works for powers of 10, so we will call it g(x). This simplifies the problem a lot. Now we dont need to check every number up to x, but only the powers of 10 preceding it. I found a way to make the idea work in python but it is quite messy. If you can improve it or find a better formula, please let me know.
from math import log as ln
# I know that you said 'no modules' but I need logarithms :)
def g(x):
return x - 9**(ln(x)/ln(10))
def step(x): # step function
if x < 7:
return 0
else:
return 1
def int_(x): # same as int() but returns 0 for ''
if x == '':
return 0
else:
return int(x)
def get_7s(num):
answer = 0
str_num = str(num)
if '7' in str_num:
split = str_num.split('7', 1) # cut the number at the first '7' occurence
answer += int_(split[1]) + 1 # add the second half to the answer
str_num = str(int(split[0] + '7' + (len(split[1]))*'0') - 1)
# comes back to the integer before
# 1379 -> 1369
# 18753 -> 18699
else:
pass
for power_of_10, digit in enumerate(str_num[::-1]): # [::-1] lets us go power of 10 by power of 10
digit = int(digit)
answer += (digit - step(digit)) * g(10**power_of_10) + step(digit) * 10**power_of_10
# the idea is to make property 1 work for all x.
# Two modifications are necessary:
# 'digit - step(digit)' to avoid counting 7s twice.
# '+ step(digit) * 10**power_of_10' because if step(digit) returns 1,
# it means that a 7 was passed in the counting, so we need to add them back
return answer
test:
print(len([x for x in range(8756+1) if "7" in str(x)]))
>> 3087
print(get_7s(8756))
>> 3087
And it is way faster than the original:
%timeit len([x for x in range(10_000_000) if "7" in str(x)])
>> 1.91 s ± 51 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit get_7s(10_000_000)
>> 9.73 µs ± 406 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
I hope this helps!