Increasing the speed of this algorithm

Viewed 601

I'm trying to make this code run faster, and I am having trouble doing so. I can pass most of my test cases, but as the numbers get larger I cant pass them.

Input: 92871036442 3363728910382456 output: 1160053175781729
Input: 1 1000000000000000000        output: 264160473575034274 

These test cases won't pass. What can I do to speed up my algorithm and prevent it from timing out?

#include<iostream>

using namespace std;

bool isLuckyNumber(unsigned long long n)
{
    bool found6 = false, found8 = false;
    
    while (n > 0)
    {
        int digit = n % 10;
        if (digit == 6)
        {
            found6 = true;
        }
        else if (digit == 8)
        {
            found8 = true;
        }
        
        //removing last digit
        n = n / 10;
    }
    
    if (found6 && found8)
    {
        return false;
    }
    //otherwise if any one of them is true, it is a lucky number
    else if (found6 || found8)
    {
        return true;
    }
    
    //otherwise not a lucky number
    return false;
}

int main()
{
    //creating needed variables, using unsigned long long as data type as it
    //can store huge values (between 0 and 18446744073709551615)
    unsigned long long L, R, count = 0;
    
    //reading L and R
    cin >> L >> R;
    
    //looping as long as L<=R
    while (L <= R)
    {
        //if L is lucky number, incrementing count
        
        if (isLuckyNumber(L))
        {
            count++;
        }
        
        //incrementing L
        L++;
    }
    
    //displaying count at the end
    cout << count << endl;
    return 0;
}
3 Answers

Consider the numbers in range 0 - 10n-1, they are the combinations with repetitions of the 10 digits.

The number of combinations without a specified digit is 9n, if you want to exclude two digits is 8n.

Lucky numbers are defined as the set of combinations containing either 6 or 8 but not both. Using basic set theory we can extract some formulas.

N is the total range with cardinality |N| = 10n

N-6, N-8 are the set of number not containing 6 or 8, |N-6| = |N-8| = 9n

N-68, numbers containing neither 6 nor 8, |N-68| = 8n.

From this we can calculate all cardinalities:

|N+6| = |N| - |N-6|

|N+8| = |N| - |N-8|

|N+6| + |N+8| - |N+68| + |N-68| = |N|

|LUCKY| = |N+6| + |N+8| - |N+68| *2 = 2 * ( 9n - 8n )

Now, we need to know the lucky number in a range that is not a power of 10, then we have to split our input in several blocks, each block with the form J * 10^K - (J+1) * 10^K - 1.

The number of such blocks is linear in the input size, that is no more than 10 * [digits of [number + 1]], so the total cost of the algorithm is also linear in the input size, that is log(n) where n is the greatest number in input.

When calculating each block we must take into account J, the prefix:

  • if J contains both 6 and 8 we can skip the block since the number is not lucky
  • if J contains either 6 or 8 but not both, then the formula for this block is |N-8| or |N-6| = 9^K
  • in the remaining case the formula is 9^K - 8^K.

So, in total:

  • add 1 to your input numbers since the algorithm exclude the upper bound of the range
  • slice your number in blocks
  • calculate the lucky numbers and for each input number, then subtract them to find the result

To do a quick check consider the case where you have the range 1 to 1000000000000000000 (18 digits), since 1 and 1000000000000000000 are not lucky you can directly apply the formula 2 * ( 918 - 8 18 ).

Code:

#include <iostream>
using namespace std;

long countLucky(long number) {
    //Algorithm excludes upper bound, so increase it by to include
    number++;

    long ncopy=number;
    //Count digits
    int size=0;
    while (ncopy>0) {
        ncopy/=10;
        size++;
    }

    ncopy=number;

    //Extract digits into array
    int digits[size];
    for (int d=0;ncopy>0;d++) {
        digits[d]=ncopy%10;
        ncopy/=10;
    }

    //Calculate powers of 10, 9, 8 starting with size-1
    long pow10=1;
    long pow9=1;
    long pow8=1;
    for (int i=0;i<size-1;i++) {
        pow10*=10;
        pow9*=9;
        pow8*=8;
    }

    bool prefix6=false;
    bool prefix8=false;

    long count=0;

    for (int d=size-1;d>=0;d--) {
        //Both digits present in prefix, so no more lucky numbers can be found
        if (prefix6 && prefix8) {
            break;
        }
        for (int block=0;block<digits[d];block++) {
            if ((prefix6 || (block==6)) && (prefix8 || (block==8))) {
                continue;
            }
            if ((prefix6 || (block==6)) || (prefix8 || (block==8))) {
                count+=pow9;
            } else {
                count+=2*(pow9-pow8);
            }
        }

        //Calculate new powers
        pow10/=10;
        pow9/=9;
        pow8/=8;

        //Update prefix status
        prefix6 = prefix6 || (digits[d]==6);
        prefix8 = prefix8 || (digits[d]==8);
    }

    cout << endl;

    return count;
}

int main() {
    long c=countLucky(3363728910382456)-countLucky(92871036442);
    cout << "Result    " << c << endl;
    cout << "Solution  " << 1160053175781729 << endl;
}

The idea here is not to test each numbers in that range, but instead you should be finding the answer through number theory.

A way to find the answer, is to first find all the numbers in a range that has the number 6, then find all the numbers in that same range that has the number 8 in it. Then you minus the numbers that have both of them.

Below I have a piece of code that just count the lucky number from any 10^n (which includes 10^0, or 1) to any 10^m:

unsigned long long luckyCount(unsigned long long N)
{
    for(auto tempN = N; tempN != 1; tempN /= 10) if(tempN % 10) throw;
    // Just some error check, so you can't input any number other than 10^n

    unsigned long long T = 0, D = 0, C = 0;
    while(N /= 10)
    {
        D = D * 8 + T * 2;
        T = T * 9 + std::pow(10, C);
        ++C;
    }
    return T * 2 - D * 2;
}

unsigned long long luckyCount(unsigned long long L, unsigned long long R)
{
    return luckyCount(R) - luckyCount(L);
}

int main()
{
    unsigned long long L, R;
    cin >> L >> R;
    std::cout << luckyCount(L, R) << "\n";
}

For each iteration, the amount of number that have a 6 in it and a 8 in it are the same, and both of them are denoted as T. And the amount of numbers that have both 6 and 8 in it are denoted as D.

Hopefully this will give you a starting point, and you algorithm will be running at O(log(N)).

I won't give you a complete algorithm, but just a starting point.

How many lucky numbers are there between 1 and 100?

All the ones in the form 6x + the ones like x6 + 1 (66) + all the 8x + all the x8 + 1 (88), where x can be 0, 1, 2, 3, 4, 5, 7, 9. So there are

(8 + 8 + 1) * 2 = 34 lucky numbers (*).

How many lucky numbers are between a and b?

The ones between 1 and b minus the ones between a and 1.

keep up the reasoning and you'll end up with an algorithm with a complexity better than O(n).

(*) Which are: 6, 8, 16, 18, 26, 28, 36, 38, 46, 48, 56, 58, 60, 61, 62, 63, 64, 65, 66, 67, 69, 76, 78, 80, 81, 82, 83, 84, 85, 87, 88, 89, 96, 98.

Related