5

1. Introduction to problem

I am trying to find the number of monotonely increasing numbers with a certain number of digits. A monotonely increasing number with k digits can be written as

n = a_0 a_1 ... a_k-1

where a_i <= a_(i+1) for all i in range(0, k). A more concrete example are 123 or 12234489. I am trying to create a function such that

increasing(2) = 45
increasing(3) = 165
increasing(4) = 495
increasing(5) = 1287
increasing(6) = 3003

Because there are 45 numbers with two digits that are increasing, 11, 12, ..., 22, 23, ..., 88, 89, 99. And so forth.

I saw this as a nice opportunity to use recursion. I have tried to write a code that does this, however there is something wrong with the result. My psudo-code goes like this

2. Psudo-code

  • Start with the numbers [1, 2, ..., 9] loop through these numbers. Increase length by one.
  • Loop over the numbers [i, ..., 9] where last_digit was the number from the previous recursion.
  • If length = number of digits wanted add one to total and return else repeat the steps above.

3. Code

global total
total = 0
nums = range(1, 10)

def num_increasing(digits, last_digit = 1, length = 0):
    global total

    # If the length of the number is equal to the number of digits return
    if digits == length:
        total += 1
        return

    possible_digits = nums[last_digit-1::]

    for i in possible_digits:
        last_digit = i
        num_increasing(digits, last_digit, length + 1)
    return total

if __name__ == '__main__':

    num_increasing(6)
    print total

4. Question:

Is my psudocode correct for finding these numbers? How can one use recursion correctly to tackle this problem?

I will not ask to find the error in my code, however some pointers or an example of code that works would be much obliged.

N3buchadnezzar
  • 471
  • 7
  • 12
  • 2
    Not sure what your actual question is, but using a global seems contrary to the purpose of using recursion. You should be using the return value of the recursion and adding it to the current value of total. – Daniel Roseman May 04 '16 at 15:00

5 Answers5

4

This can be computed in closed form.

We have a budget of 8 units, which we can allocate to each digit or to "leftovers". A digit with n units of budget allocated to it is n greater than the digit before it; for the first digit, if we allocate n units of budget there, its value is n+1. Leftover budget does nothing.

Budget allocations are in 1-to-1 correspondence with monotonely increasing numbers, as each budget allocation produces a monotonely increasing number, and each monotonely increasing number has a corresponding budget allocation. Thus, the number of monotonely increasing numbers of length k is the number of ways to allocate 8 units of budget to k+1 buckets, one bucket per digit and one bucket of leftovers.

By the classic stars and bars result, this is (8 + k) choose 8, or (8+k)!/(8!k!):

def monotone_number_count(k):
    total = 1
    for i in xrange(k+1, k+9):
        total *= i
    for i in xrange(1, 9):
        total //= i
    return total

For monotonely decreasing numbers, the same idea can be applied. This time we have 9 units of budget, because our digits can go from 9 down to 0 instead of starting at 1 and going up to 9. A digit with n units of budget allocated to it is n lower than the previous digit; for the first digit, n units of budget gives it value 9-n. The one caveat is that this counts 0000 as a four-digit number, and similarly for other values of k, so we have to explicitly uncount this, making the result ((9 + k) choose 9) - 1:

def monotonely_decreasing_number_count(k):
    total = 1
    for i in xrange(k+1, k+10):
        total *= i
    for i in xrange(1, 10):
        total //= i
    total -= 1
    return total
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Great answer! Well explained. I was also looking for a constant-time algorithm, but you nailed it. – André Laszlo May 04 '16 at 16:34
  • Is the decreasing one correct? Testing i get `[45, 255, 960, 2952]` for `digits = [ 2, 3, 4, 5]`. Yours seem to ble slightly off giving `[ 9, 63, 282, 996, 2997]`. Not quite sure why it is off though. – N3buchadnezzar May 04 '16 at 16:48
  • 1
    @N3buchadnezzar: I'm not getting either of those results when I test it; when I try those inputs, I get `[54, 219, 714, 2001]`. This output looks right to me. – user2357112 May 04 '16 at 16:53
  • I was in the wrong. I looked at the wrong code for a minute. I am sorry, your answer is excellent =) – N3buchadnezzar May 04 '16 at 17:32
3

You could use a simple recursion based on the following relation: the count of monotonic numbers of k digits starting at i (0<i≤9) is the sum of the counts of monotonic numbers of k-1 digits starting with j, i≤j≤9.

For k=1 the result is trivial: 10-i

It would lead to the following recursive function:

def num_increasing(ndigits, first=1):
    n = 0
    if ndigits == 1:
        n = 10 - first
    else:
        for digit in range(first, 10):
            n += num_increasing(ndigits - 1, digit)
    return n

For ndigits = 6, it gives 3003.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Awesome! Any idea how to make a similar function that is decreasing instead? =) – N3buchadnezzar May 04 '16 at 15:39
  • This takes time proportional to the number of monotonely increasing numbers, since the call tree essentially generates all such numbers. That's O(k^8), so it gets really slow for large k. You could improve the performance with memoization, but for performance improvement, the closed-form solution is better. – user2357112 May 04 '16 at 16:32
0

Here is a non-recursive solution to this:

def is_monotonic(num, reverse=False):
    num = str(num)
    # check if the string representation of num is same as the sorted one
    return num == ''.join(sorted(num, reverse=reverse))

def get_monotonic_nums(ndigit, reverse=False):
    start = 10**(ndigit-1) if reverse else int('1' * ndigit)
    end = 10**ndigit
    return sum(is_monotonic(num, reverse) for num in xrange(start, end))

And, then the usage:

>>> get_monotonic_nums(2)
45
>>> get_monotonic_nums(6)
3003

And, if you need the decreasing order:

>>> get_monotonic_nums(2, reverse=True)
54
AKS
  • 18,983
  • 3
  • 43
  • 54
  • for the `end` you can simple do `end = 10**ndigit` – Copperfield May 04 '16 at 16:11
  • Why is this so much slower than the recursive version? – N3buchadnezzar May 04 '16 at 16:17
  • mmm, in the `get_monotonic_nums(2, reverse=True)` doing it in another way I get 54, here you forget to include 10 with the `int('1' * ndigit)`, for the reverse case you should start in `start = 10**(ndigit-1)` – Copperfield May 04 '16 at 16:18
  • 2 digits monotonics aren't 45? – Nizam Mohamed May 04 '16 at 16:21
  • @N3buchadnezzar It is indeed slower because the `sum` function waits for all the `is_monotonic` calls to get completed and also I have `sorted` there. Obviously the recursive solution posted above is a lot faster and better than this one. I just wanted to post an alternate solution. – AKS May 04 '16 at 16:21
  • @N3buchadnezzar because this one check every single number, while the recursive version of Serge Ballesta use the patter of the number to count the desire result so there is no need to check every single one – Copperfield May 04 '16 at 16:21
  • @NizamMohamed: It is 45 in the OP and also using the recursive solution. – AKS May 04 '16 at 16:22
  • it's off-by-one, I get 54 for decreasing or equal. – Nizam Mohamed May 04 '16 at 16:38
  • Thanks, fixed for reverse using @Copperfield's suggestion – AKS May 04 '16 at 16:46
0

This is what I came up with;

def is_monotonic(n):
    n = str(n)
    for x, y in zip(n[:-1], n[1:]):
        if x > y:
            return False
    return True

def get_monotonic_nums(digits):
    digits = abs(digits)
    start = 0 if digits == 1 else int('1{}'.format('0'*(digits-1)))
    end = start * 10 if start else 10
    for n in range(start, end):
        if is_monotonic(n):
            yield n

test;

len(list(get_monotonic_nums(2)))
45
Nizam Mohamed
  • 8,751
  • 24
  • 32
0

After some searching on the internet I was able to figure out the following one liner solution. Based on the sum formula for the binomial coefficients. I now realize how slow my recursive solution was compared to this one.

def choose(n, k):
    """
    A fast way to calculate binomial coefficients by Andrew Dalke (contrib).
    """
    if 0 <= k <= n:
        ntok = 1
        ktok = 1
        for t in xrange(1, min(k, n - k) + 1):
            ntok *= n
            ktok *= t
            n -= 1
        return ntok // ktok
    else:
        return 0

def increasing(digit):
    return choose(digit + 9,9) - 1

def decreasing(digit):
    return choose(digit + 10,10) - 10*digit - 1
N3buchadnezzar
  • 471
  • 7
  • 12