4

I don't know how to go about this programming problem.

Given two integers n and m, how many numbers exist such that all numbers have all digits from 0 to n-1 and the difference between two adjacent digits is exactly 1 and the number of digits in the number is atmost 'm'.

What is the best way to solve this problem? Is there a direct mathematical formula?

Edit: The number cannot start with 0.

Example: for n = 3 and m = 6 there are 18 such numbers (210, 2101, 21012, 210121 ... etc)

Update (some people have encountered an ambiguity): All digits from 0 to n-1 must be present.

Anirudh Rayabharam
  • 737
  • 1
  • 6
  • 12
  • 3
    I suggest to raise it in algorithm forum. Not related to C++/C language. Or atleast remove C++/C tag. – kumar_m_kiran Aug 20 '13 at 12:07
  • I think this is not about computing, but rather about mathematics. I'd suggest posting [there](http://math.stackexchange.com/) ? – d-stroyer Aug 20 '13 at 12:08
  • 1
    An example would be nice. – MrSmith42 Aug 20 '13 at 12:09
  • 2
    This is a programming problem and not math... – Anirudh Rayabharam Aug 20 '13 at 12:11
  • This appears to rather belong to math.SE, since the OP is asking about a mathematical algorithm rather than about anything programming-specific. – SingerOfTheFall Aug 20 '13 at 12:11
  • I don't know if a mathematical algorithm exists or I have to use a brute force approach. – Anirudh Rayabharam Aug 20 '13 at 12:14
  • @SingerOfTheFall This is definitely in the domain of programming and not in mathematics. – VoronoiPotato Aug 20 '13 at 14:10
  • @VoronoiPotato "a direct mathematical formula" is more mathematics than programming (all maths in fact), but if there doesn't exist such a formula, it's a programming question, though it doesn't conform to [so] guidelines, since OP didn't show an attempt at solving the problem him/herself. – Bernhard Barker Aug 20 '13 at 14:33
  • The comments on this question remind me strongly of what happened with http://stackoverflow.com/questions/11314077/algorithm-for-exclusion-of-numbers. And people like @Dukeling are absolutely wrong - this is a **programming** question where if you don't know the **programming** technique to solve it you won't have a hope of figuring out how to tackle it. Which I'd be willing to bet Dukeling can't. – btilly Aug 20 '13 at 15:53
  • 1
    For those who don't know the **programming** technique in question, go read http://www.topcoder.com/tc?d1=tutorials&d2=dynProg&module=Static and educate yourself. – btilly Aug 20 '13 at 16:00
  • @btilly "A direct **mathematical** formula" seems to indicate mathematics. I didn't say there **is** such a formula, I just tried to say it's off topic no matter what. And I'm sure some of the 142 algorithm questions I've answered will show you that I'm more than capable of solving this. – Bernhard Barker Aug 20 '13 at 16:20
  • @Dukeling He's asking if it's possible and sure that aspect of the question is off topic, but not wildly so. It shouldn't mark the whole question as "off topic". This kind of obsessive compulsive behavior prevents real questions from being answered. – VoronoiPotato Aug 20 '13 at 17:09
  • @VoronoiPotato I didn't, but I could've since "Questions asking for code must demonstrate a minimal understanding of the problem being solved. Include attempted solutions, why they didn't work, and the expected results." (that's one of the off topic reasons). – Bernhard Barker Aug 20 '13 at 17:13
  • @Dukeling Yeah you also could have for almost every algorithms question you've answered http://stackoverflow.com/questions/17448814/count-points-in-a-rectangle/17449106#17449106 has no implementation, I could go on. I wonder why something like algorithms doesn't tend to have "attempted solutions" *ponders* – VoronoiPotato Aug 20 '13 at 17:17
  • @VoronoiPotato I'm not saying every question I've ever **answered** is the perfect question. Actually that would be very improbable and I'm not sure why that would even matter. Minute variations (or even difference in mood) can be enough to vote on one question and not on another, but I didn't vote, so I'm not really sure what your point is. – Bernhard Barker Aug 20 '13 at 18:57
  • My point is while it may lack some implementation that doesn't mean the question itself is without merit. I'd argue most algo questions are that way, but that doesn't mean they shouldn't be on SO. – VoronoiPotato Aug 20 '13 at 19:12
  • @VoronoiPotato I'm not saying they're bad. And the problem is with lacking **any** implementation, not "some". Honestly algorithm questions are my favourite, but not many show attempts and thus don't conform to StackOverflow rules. These rules are there to prevent copy-paste questions from homework, which there are already so many of (not that I think they're making a big difference). – Bernhard Barker Aug 20 '13 at 20:37

4 Answers4

2

This Python code computes the answer in O(nm) by keeping track of the numbers ending with a particular digit.

Different arrays (A,B,C,D) are used to track numbers that have hit the maximum or minimum of the range.

n=3
m=6
A=[1]*n # Number of ways of being at digit i and never being to min or max
B=[0]*n # number of ways with minimum being observed
C=[0]*n # number of ways with maximum being observed
D=[0]*n # number of ways with both being observed
A[0]=0 # Cannot start with 0
A[n-1]=0 # Have seen max so this 1 moves from A to C
C[n-1]=1 # Have seen max if start with highest digit
t=0
for k in range(m-1):
    A2=[0]*n
    B2=[0]*n
    C2=[0]*n
    D2=[0]*n
    for i in range(1,n-1):
        A2[i]=A[i+1]+A[i-1]
        B2[i]=B[i+1]+B[i-1]
        C2[i]=C[i+1]+C[i-1]
        D2[i]=D[i+1]+D[i-1]
    B2[0]=A[1]+B[1]
    C2[n-1]=A[n-2]+C[n-2]
    D2[0]=C[1]+D[1]
    D2[n-1]=B[n-2]+D[n-2]
    A=A2
    B=B2
    C=C2
    D=D2
    x=sum(d for d in D2)
    t+=x
print t
Peter de Rivaz
  • 33,126
  • 4
  • 46
  • 75
  • i don't think your algorithmic complexity can be correct. consider `n=3` and all numbers having a `1` on odd positions. you have `floor(m/2)` positions left each of which you can set freely to either `0` or `2`. this already produces `O(2^(m/2))` items to output. this still holds if you need the numbers containing all digits if you consider the case of `m > 4` (in general `m > n+1`) as you can prefix the constructed numbers with `2101` (`<(n-1)(n-2)...01>`), complexity being `O(2^((m-(n+1))/2))`. – collapsar Aug 20 '13 at 14:22
  • I think they wanted just the total number of solutions, rather than every solution to be output. – Peter de Rivaz Aug 20 '13 at 14:51
  • There is ambiguity in *"have all digits from 0 to n-1"*. Does it mean that all of the digits from `0` to `n-1` have to be present? Or does it mean that all of the digits have to be in that range? You've solved the first (and harder) variant. The other variant is similar and easier - you don't need to worry about whether the whole range is present. – btilly Aug 20 '13 at 15:58
  • Good point, I hadn't spotted that ambiguity! However, in this case I think it must be the first choice as otherwise there would be more than 18 solutions. – Peter de Rivaz Aug 20 '13 at 18:28
  • is the complexity pointed out by collapsar true? Or is it O(mn)? – Anirudh Rayabharam Aug 21 '13 at 11:12
  • Collapsar's complexity is correct if you want to output all the solutions, while I believe O(nm) is correct if you want to just output the total number of solutions (as this code does). – Peter de Rivaz Aug 21 '13 at 11:43
  • Please add some more comments to explain your logic. – Anirudh Rayabharam Aug 21 '13 at 11:56
  • The code contains two nested loops, the outer iterates m-1 times, and the inner iterates n-1 times, so the code will take O(nm) cycles. It is up to you to decide whether you need the total number of solutions (as this code produces) or whether you need to list each individual solution (as the code by groovy produces). – Peter de Rivaz Aug 21 '13 at 12:16
  • Yes but what are A2, B2, C2 and D2 doing? (I just need the number of solutions) – Anirudh Rayabharam Aug 21 '13 at 12:21
  • but I don't understand it...O.o – גלעד ברקן Aug 22 '13 at 21:51
  • I think there may be a mathematical solution. Check my idea out if you like http://stackoverflow.com/questions/18334599/caculating-total-combinations/18426978#18426978 – גלעד ברקן Aug 25 '13 at 14:07
2

After doing some more research, I think there may actually be a mathematical approach after all, although the math is advanced for me. Douglas S. Stones pointed me in the direction of Joseph Myers' (2008) article, BMO 2008–2009 Round 1 Problem 1—Generalisation, which derives formulas for calculating the number of zig-zag paths across a rectangular board.

As I understand it, in Anirudh's example, our board would have 6 rows of length 3 (I believe this would mean n=3 and r=6 in the article's terms). We can visualize our board so:

0 1 2   example zig-zag path:  0
0 1 2                            1
0 1 2                          0
0 1 2                            1
0 1 2                              2
0 1 2                            1

Since Myers' formula m(n,r) would generate the number for all the zig-zag paths, that is, the number of all 6-digit numbers where all adjacent digits are consecutive and digits are chosen from (0,1,2), we would still need to determine and subtract those that begin with zero and those that do not include all digits.

If I understand correctly, we may do this in the following way for our example, although generalizing the concept to arbitrary m and n may prove more complicated:

Let m(3,6) equal the number of 6-digit numbers where all adjacent digits  
are consecutive and digits are chosen from (0,1,2). According to Myers,
m(3,r) is given by formula and also equals OEIS sequence A029744 at 
index r+2, so we have 

m(3,6) = 16

How many of these numbers start with zero? Myers describes c(n,r) as the
number of zig-zag paths whose colour is that of the square in the top 
right corner of the board. In our case, c(3,6) would include the total 
for starting-digit 0 as well as starting-digit 2. He gives c(3,2r) as 2^r, 
so we have

c(3,6) = 8. For starting-digit 0 only, we divide by two to get 4.

Now we need to obtain only those numbers that include all the digits in 
the range, but how? We can do this be subtracting m(n-1,r) from m(n,r).
In our case, we have all the m(2,6) that would include only 0's and 1's,
and all the m(2,6) that would include 1's and 2's. Myers gives 
m(2,anything) as 2, so we have

2*m(2,6) = 2*2 = 4

But we must remember that one of the zero-starting numbers is included 
in our total for 2*m(2,6), namely 010101. So all together we have

m(3,6) - c(3,6)/2 - 4 + 1 
= 16 - 4 - 4 + 1 
= 9

To complete our example, we must follow a similar process for m(3,5), 
m(3,4) and m(3,3). Since it's late here, I might follow up tomorrow...
Community
  • 1
  • 1
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • Ah. This is on the right path. The way you finish is to use inclusion-exclusion. See http://en.wikipedia.org/wiki/Inclusion%E2%80%93exclusion_principle for that. So in this example you'd start with all of the paths that fit within width 3, subtract off paths that fit in `0-1` and paths that fit in `1-2`, then add back in the path that fits in `1`. This generalizes to wider tracks as well. – btilly Aug 25 '13 at 17:30
  • @btilly thank you for your comment. Isn't it what I did? For the m(3,6) example, I subtracted the paths for 0,1 and 1,2 and the paths starting on 0, except for the one overlap. Or do you mean there may be a way to calculate all the lengths from n to m (e.g., 3-6 in our example) at once? – גלעד ברקן Aug 25 '13 at 17:56
1

One approach could be to program it recursively, calling the function to add as well as subtract from the last digit.

Haskell code:

import Data.List (sort,nub)

f n m = concatMap (combs n) [n..m]

combs n m = concatMap (\x -> combs' 1 [x]) [1..n - 1] where
  combs' count result
    | count == m = if test then [concatMap show result] else []
    | otherwise  = combs' (count + 1) (result ++ [r + 1])
                ++ combs' (count + 1) (result ++ [r - 1])
   where r = last result
         test = (nub . sort $ result) == [0..n - 1]

Output:

*Main> f 3 6
["210","1210","1012","2101","12101","10121","21210","21012"
,"21010","121210","121012","121010","101212","101210","101012"
,"212101","210121","210101"]

In response to Anirudh Rayabharam's comment, I hope the following code will be more 'pseudocode' like. When the total number of digits reaches m, the function g outputs 1 if the solution has hashed all [0..n-1], and 0 if not. The function f accumulates the results for g for starting digits [1..n-1] and total number of digits [n..m].

Haskell code:

import qualified Data.Set as S

g :: Int -> Int -> Int -> Int -> (S.Set Int, Int) -> Int
g n m digitCount lastDigit (hash,hashCount)
  | digitCount == m = if test then 1 else 0
  | otherwise       =
      if lastDigit == 0
         then g n m d' (lastDigit + 1) (hash'',hashCount')
         else if lastDigit == n - 1
                 then g n m d' (lastDigit - 1) (hash'',hashCount')
                 else g n m d' (lastDigit + 1) (hash'',hashCount') 
                    + g n m d' (lastDigit - 1) (hash'',hashCount') 
 where test = hashCount' == n
       d' = digitCount + 1
       hash'' = if test then S.empty else hash'
       (hash',hashCount')
         | hashCount == n          = (S.empty,hashCount)
         | S.member lastDigit hash = (hash,hashCount)
         | otherwise               = (S.insert lastDigit hash,hashCount + 1)

f n m = foldr forEachNumDigits 0 [n..m] where
  forEachNumDigits numDigits accumulator = 
    accumulator + foldr forEachStartingDigit 0 [1..n - 1] where 
      forEachStartingDigit startingDigit accumulator' =
        accumulator' + g n numDigits 1 startingDigit (S.empty,0)

Output:

*Main> f 3 6
18
(0.01 secs, 571980 bytes)

*Main> f 4 20
62784
(1.23 secs, 97795656 bytes)

*Main> f 4 25
762465
(11.73 secs, 1068373268 bytes)
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • This produces the solutions. But the number of solutions grows exponentially, so this will quickly prove impracticable. – btilly Aug 20 '13 at 17:06
  • @btilly good point. For the larger `n` and `m` we'll use the mathematical formula :) – גלעד ברקן Aug 20 '13 at 17:37
  • what mathematical formula? – Anirudh Rayabharam Aug 21 '13 at 11:11
  • It would be awesome if you can convert that Haskell code to pseudocode since many people don't know Haskell. – Anirudh Rayabharam Aug 21 '13 at 12:27
  • This is better solved through exploring the tree of possible solutions. Start with data S a = Solution a | PartialSolution [S a], then write a generator :: [S a]->[a] – Sassa NF Aug 22 '13 at 17:04
  • @AnirudhRayabharam Thank you for your comment. I hope my update will be more readable for people less familiar with Haskell. You may also view Sassa NF's suggestion here in an answer to my own question: http://stackoverflow.com/questions/18370537/recursion-confusion-in-haskell/18387349#18387349 . Sassa NF helped me learn something new and solve a problem with my recursion. – גלעד ברקן Aug 22 '13 at 21:08
  • @SassaNF Sassa, do you have an idea about how to make the calculation of just the number of solutions (rather than the solutions themselves) fast for larger n and m? (by the way, `foldl'` seemed to produce similar results to `foldr` with my code above) – גלעד ברקן Aug 22 '13 at 21:17
  • @AnirudhRayabharam regarding the mathematical formula.. I was just kidding, although I looked into the combinations approach. You can calculate mathematically all combinations that include all 0..n-1 and do not start with zero. The problem is to calculate only the combinations that have adjacent numbers only one-step removed. – גלעד ברקן Aug 22 '13 at 21:19
  • @groovy yes, for a commutative monoid foldr mappend v produces the same results as foldl mappend v. Also, if v is monoid's unit (mzero), then, too, both folds produce the same result. In your case the monoid is (+,0). – Sassa NF Aug 23 '13 at 10:19
0

model your problem as 2 superimposed lattices in 2 dimensions, specifically as pairs (i,j) interconnected with oriented edges ((i0,j0),(i1,j1)) where i1 = i0 + 1, |j1 - j0| = 1, modified as follows:

  • dropping all pairs (i,j) with j > 9 and its incident edges
  • dropping all pairs (i,j) with i > m-1 and its incident edges
  • dropping edge ((0,0), (1,1))

this construction results in a structure like in this diagram:

sample diagram for n=5, m=7:

the requested numbers map to paths in the lattice starting at one of the green elements ((0,j), j=1..min(n-1,9)) that contain at least one pink and one red element ((i,0), i=1..m-1, (i,n-1), i=0..m-1 ). to see this, identify the i-th digit j of a given number with point (i,j). including pink and red elements ('extremal digits') guarantee that all available diguts are represented in the number.

Analysis

for convenience, let q1, q2 denote the position-1.

let q1 be the position of a number's first digit being either 0 or min(n-1,9). let q2 be the position of a number's first 0 if the digit at position q1 is min(n-1,9) and vv.

case 1: first extremal digit is 0

the number of valid prefixes containing no 0 can be expressed as sum_{k=1..min(n-1,9)} (paths_to_0(k,1,q1), the function paths_to_0 being recursively defined as

paths_to_0(0,q1-1,q1)   = 0;
paths_to_0(1,q1-1,q1)   = 1;
paths_to_0(digit,i,q1)  = 0; if q1-i < digit;
paths_to_0(x,_,_)       = 0; if x >= min(n-1,9)
                               // x=min(n-1,9) mustn't occur before position q2,
                               // x > min(n-1,9) not at all
paths_to_0(x,_,_)       = 0; if x <= 0;         
                               // x=0 mustn't occur before position q1,
                               // x < 0 not at all

and else  paths_to_0(digit,i,q1) =
             paths_to_0(digit+1,i+1,q1) + paths_to_0(digit-1,i+1,q1);

similarly we have

paths_to_max(min(n-1,9),q2-1,q2) = 0;
paths_to_max(min(n-2,8),q2-1,q2) = 1;
paths_to_max(digit,i,q2)         = 0  if q2-i < n-1;
paths_to_max(x,_,_)              = 0; if x >= min(n-1,9)
                                       // x=min(n-1,9) mustn't occur before
                                       // position q2,
                                       // x > min(n-1,9) not at all
paths_to_max(x,_,_)              = 0; if x < 0;

and else  paths_to_max(digit,q1,q2) =
              paths_max(digit+1,q1+1,q2) + paths_to_max(digit-1,q1+1,q2);

and finally

paths_suffix(digit,length-1,length) = 2;  if digit > 0 and digit < min(n-1,9)
paths_suffix(digit,length-1,length) = 1;  if digit = 0 or  digit = min(n-1,9)
paths_suffix(digit,k,length)        = 0;  if    length > m-1
                                             or length < q2
                                             or k > length
paths_suffix(digit,k,0)             = 1;  // the empty path

and else  paths_suffix(digit,k,length) = 
             paths_suffix(digit+1,k+1,length) + paths_suffix(digit-1,k+1,length);

... for a grand total of

number_count_case_1(n, m) =
    sum_{first=1..min(n-1,9), q1=1..m-1-(n-1), q2=q1..m-1, l_suffix=0..m-1-q2} (
          paths_to_0(first,1,q1)
        + paths_to_max(0,q1,q2)
        + paths_suffix(min(n-1,9),q2,l_suffix+q2)
    )   

case 2: first extremal digit is min(n-1,9)

case 2.1: initial digit is not min(n-1,9) this is symmetrical to case 1 with all digits d replaced by min(n,10) - d. as the lattice structure is symmetrical, this means number_count_case_2_1 = number_count_case_1.

case 2.2: initial digit is min(n-1,9) note that q1 is 1 and the second digit must be min(n-2,8). thus

number_count_case_2_2 (n, m) = 
    sum_{q2=1..m-2, l_suffix=0..m-2-q2} (
          paths_to_max(1,1,q2)
        + paths_suffix(min(n-1,9),q2,l_suffix+q2)
    )   

so the grand grand total will be

number_count ( n, m ) = 2 * number_count_case_1 (n, m) + number_count_case_2_2 (n, m);

Code

i don't know whether a closed expression for number_count exists, but the following perl code will compute it (the code is but a proof of concept as it does not use memoization techniques to avoid recomputing results already obtained):

use strict;
use warnings;

my ($n, $m) = ( 5, 7 ); # for example

$n = ($n > 10) ? 10 : $n; # cutoff 

sub min
sub paths_to_0 ($$$) {
    my (
          $d
        , $at
        , $until
            ) = @_;
    #
    if (($d == 0) && ($at == $until - 1))   { return 0; }
    if (($d == 1) && ($at == $until - 1))   { return 1; }
    if ($until - $at < $d)                  { return 0; }
    if (($d <= 0) || ($d >= $n)))           { return 0; }

    return paths_to_0($d+1, $at+1, $until) + paths_to_0($d-1, $at+1, $until);    
} # paths_to_0

sub paths_to_max ($$$) {
    my (
          $d
        , $at
        , $until
            ) = @_;
    #
    if (($d == $n-1) && ($at == $until - 1))    { return 0; }
    if (($d == $n-2) && ($at == $until - 1))    { return 1; }
    if ($until - $at < $n-1)                    { return 0; }
    if (($d < 0) || ($d >= $n-1))               { return 0; }

    return paths_to_max($d+1, $at+1, $until) + paths_to_max($d-1, $at+1, $until);    
} # paths_to_max

sub paths_suffix ($$$) {
    my (
          $d
        , $at
        , $until
            ) = @_;
    #
    if (($d < $n-1) && ($d > 0)     && ($at == $until - 1))     { return 2; }
    if ((($d == $n-1) && ($d == 0)) && ($at == $until - 1))     { return 1; }
    if (($until > $m-1) || ($at > $until))                      { return 0; }
    if ($until == 0)                                            { return 1; }

    return paths_suffix($d+1, $at+1, $until) + paths_suffix($d-1, $at+1, $until);    
} # paths_suffix

#
# main
#
    number_count =
        sum_{first=1..min(n-1,9), q1=1..m-1-(n-1), q2=q1..m-1, l_suffix=0..m-1-q2} (
              paths_to_0(first,1,q1)
            + paths_to_max(0,q1,q2)
            + paths_suffix(min(n-1,9),q2,l_suffix+q2)
        )   

my ($number_count, $number_count_2_2) = (0, 0);
my ($first, $q1, i, $l_suffix);

for ($first = 1; $first <= $n-1; $first++) {
    for ($q1 = 1; $q1 <= $m-1 - ($n-1); $q1++) {
        for ($q2 = $q1; $q2 <= $m-1; $q2++) {
            for ($l_suffix = 0; $l_suffix <= $m-1 - $q2; $l_suffix++) {
                $number_count =
                      $number_count
                    + paths_to_0($first,1,$q1)
                    + paths_to_max(0,$q1,$q2)
                    + paths_suffix($n-1,$q2,$l_suffix+$q2)
                ;
            }
        }
    }
}

#
#  case 2.2
#
for ($q2 = 1; $q2 <= $m-2; $q2++) {
    for ($l_suffix = 0; $l_suffix <= $m-2 - $q2; $l_suffix++) {
        $number_count_2_2 =
              $number_count_2_2
            + paths_to_max(1,1,$q2)
            + paths_suffix($n-1,$q2,$l_suffix+$q2)
        ;
    }
}

$number_count = 2 * $number_count + number_count_2_2;
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
collapsar
  • 17,010
  • 4
  • 35
  • 61
  • in which way? it shows fine with me (chrome 29) and the incomplete border is not part of the content. – collapsar Aug 21 '13 at 11:14
  • I think there may be a mathematical solution. Check my idea out if you like http://stackoverflow.com/questions/18334599/caculating-total-combinations/18426978#18426978 – גלעד ברקן Aug 25 '13 at 14:10