0

Project Euler problem 14:

The following iterative sequence is defined for the set of positive integers:

n → n/2 (n is even) n → 3n + 1 (n is odd)

Using the rule above and starting with 13, we generate the following sequence: 13 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1

It can be seen that this sequence (starting at 13 and finishing at 1) contains 10 terms. Although it has not been proved yet (Collatz Problem), it is thought that all starting numbers finish at 1.

Which starting number, under one million, produces the longest chain?

My first instinct is to create a function to calculate the chains, and run it with every number between 1 and 1 million. Obviously, that takes a long time. Way longer than solving this should take, according to Project Euler's "About" page. I've found several problems on Project Euler that involve large groups of numbers that a program running for hours didn't finish. Clearly, I'm doing something wrong.

How can I handle large groups of numbers quickly?

What am I missing here?

John
  • 15,418
  • 12
  • 44
  • 65
  • @Brad: No problem. I just got my "Cleanup" badge. :D – John Mar 23 '11 at 19:51
  • 1
    possible duplicate of [Project Euler Question 14 (Collatz Problem)](http://stackoverflow.com/questions/2643260/project-euler-question-14-collatz-problem) –  Mar 23 '11 at 21:01
  • @Moron: That is an unknown error that the OP is asking for help with. In my case, I have tried several problems on PE but all run into time problems because it's just so many numbers! The other question is "Why doesn't this work?" and mine is "How can this be faster?" The upvoted answers here **do not** match the upvoted answers there. – John Mar 24 '11 at 15:44
  • @John: Apologies. Didn't read the other carefully enough. But doing a search for project Euler 14 brought up at least 3 different questions dealing with this, I just picked one. btw, Did you look at all of those and concluded you needed a new question? I am only trying to keep the dupes down. New users tend to ask without searching. If you did and found none of them useful, please accept my apologies, and please mention that in the question to avoid further issues like this. –  Mar 24 '11 at 16:23
  • @Moron: I did not search for project Euler 14 because this isn't specific to PE 14 (that's just the example I picked). I *did* however search various combinations of my title's words and found nothing useful to me. – John Mar 24 '11 at 16:45
  • @John: If your question is just "How do I handle large group of numbers quickly", that is a very vague question. That could be one reason why you did not find it. Anyway, I will stop bothering you now :-) –  Mar 24 '11 at 16:49

6 Answers6

4

Have a read about memoization. The key insight is that if you've got a sequence starting A that has length 1001, and then you get a sequence B that produces an A, you don't to repeat all that work again.

Jeff Foster
  • 43,770
  • 11
  • 86
  • 103
3

This is the code in Mathematica, using memoization and recursion. Just four lines :)

f[x_] := f[x] = If[x == 1, 1, 1 + f[If[EvenQ[x], x/2, (3 x + 1)]]]; 
Block[{$RecursionLimit = 1000, a = 0, j},
 Do[If[a < f[i], a = f[i]; j = i], {i, Reverse@Range@10^6}];
 Print@a; Print[j];
]

Output .... chain length´525´ and the number is ... ohhhh ... font too small ! :)

BTW, here you can see a plot of the frequency for each chain length

enter image description here

Dr. belisarius
  • 60,527
  • 15
  • 115
  • 190
2

Starting with 1,000,000, generate the chain. Keep track of each number that was generated in the chain, as you know for sure that their chain is smaller than the chain for the starting number. Once you reach 1, store the starting number along with its chain length. Take the next biggest number that has not being generated before, and repeat the process.

This will give you the list of numbers and chain length. Take the greatest chain length, and that's your answer.

I'll make some code to clarify.

 public static long nextInChain(long n) {
    if (n==1) return 1;

    if (n%2==0) {
        return n/2;
    } else {
        return (3 * n) + 1;
    }
}


public static void main(String[] args) {
    long iniTime=System.currentTimeMillis();
    HashSet<Long> numbers=new HashSet<Long>();
    HashMap<Long,Long> lenghts=new HashMap<Long, Long>();

    long currentTry=1000000l;
    int i=0;
    do {
        doTry(currentTry,numbers, lenghts);
        currentTry=findNext(currentTry,numbers);
        i++;
    } while (currentTry!=0);
    Set<Long> longs = lenghts.keySet();
    long max=0;
    long key=0;
    for (Long aLong : longs) {
        if (max < lenghts.get(aLong)) {
            key = aLong;
            max = lenghts.get(aLong);
        }
    }
    System.out.println("number = " + key);
    System.out.println("chain lenght = " + max);
    System.out.println("Elapsed = " + ((System.currentTimeMillis()-iniTime)/1000));
}


private static long findNext(long currentTry, HashSet<Long> numbers) {
    for(currentTry=currentTry-1;currentTry>=0;currentTry--) {
        if (!numbers.contains(currentTry)) return currentTry;
    }
    return 0;
}

private static void doTry(Long tryNumber,HashSet<Long> numbers, HashMap<Long, Long> lenghts) {
    long i=1;
    long n=tryNumber;
    do {
        numbers.add(n);
        n=nextInChain(n);
        i++;
    } while (n!=1);
    lenghts.put(tryNumber,i);
}
Soronthar
  • 1,601
  • 10
  • 10
  • Either you are doing something wrong, or I am messing up. My code processed all million values in ~120ms. What happens if you remove the `println`? Will that speed things up? – Elian Ebbing Mar 23 '11 at 20:30
  • I did something wrong. I'll edit my code. In my system, it ran in 14 seconds (525 being the longest chain lenght) – Soronthar Mar 23 '11 at 21:30
  • With a number ending with 799 as the start point? Then we get the same results :-) My brute force method ran in 5 seconds on my computer. It's C#, but it think it's also valid java. How long does that method take on your computer? – Elian Ebbing Mar 23 '11 at 21:54
  • Same result, yes :) It takes 14 secs in my system. – Soronthar Mar 24 '11 at 12:31
  • I just realized that you only need to iterate from n to n/2 (n being the biggest number to try). All chains starting with numbers less than n/2 are guaranteed to be shorted that chains generated by numbers greater than n/2. That won't help if you start iterating from n, but helps you if you start iterating from 1. – Soronthar Mar 24 '11 at 12:39
2

Suppose you have a function CalcDistance(i) that calculates the "distance" to 1. For instance, CalcDistance(1) == 0 and CalcDistance(13) == 9. Here is a naive recursive implementation of this function (in C#):

public static int CalcDistance(long i)
{
    if (i == 1)
        return 0;

    return (i % 2 == 0) ? CalcDistance(i / 2) + 1 : CalcDistance(3 * i + 1) + 1;
}

The problem is that this function has to calculate the distance of many numbers over and over again. You can make it a little bit smarter (and a lot faster) by giving it a memory. For instance, lets create a static array that can store the distance for the first million numbers:

static int[] list = new int[1000000];

We prefill each value in the list with -1 to indicate that the value for that position is not yet calculated. After this, we can optimize the CalcDistance() function:

public static int CalcDistance(long i)
{
    if (i == 1)
        return 0;

    if (i >= 1000000)
        return (i % 2 == 0) ? CalcDistance(i / 2) + 1 : CalcDistance(3 * i + 1) + 1;

    if (list[i] == -1)
        list[i] = (i % 2 == 0) ? CalcDistance(i / 2) + 1: CalcDistance(3 * i + 1) + 1;

    return list[i];
}

If i >= 1000000, then we cannot use our list, so we must always calculate it. If i < 1000000, then we check if the value is in the list. If not, we calculate it first and store it in the list. Otherwise we just return the value from the list. With this code, it took about ~120ms to process all million numbers.

This is a very simple example of memoization. I use a simple list to store intermediate values in this example. You can use more advanced data structures like hashtables, vectors or graphs when appropriate.

Elian Ebbing
  • 18,779
  • 5
  • 48
  • 56
  • 1
    As a note I'll add that experiments on a version of mine of the code seems to show that it's useless (and it even slow it down) to have an array bigger than the maximum start number you are using. So if you are using start numbers < 1000000, having an array of 900000 or of 1100000 will slow down. – xanatos Mar 24 '11 at 09:11
0

Minimize how many levels deep your loops are, and use an efficient data structure such as IList or IDictionary, that can auto-resize itself when it needs to expand. If you use plain arrays they need to be copied to larger arrays as they expand - not nearly as efficient.

Winger
  • 676
  • 3
  • 7
0

This variant doesn't use an HashMap but tries only to not repeat the first 1000000 numbers. I don't use an hashmap because the biggest number found is around 56 billions, and an hash map could crash.

I have already done some premature optimization. Instead of / I use >>, instead of % I use &. Instead of * I use some +.

void Main()
{
    var elements = new bool[1000000];       

    int longestStart = -1;
    int longestRun = -1;

    long biggest = 0;

    for (int i = elements.Length - 1; i >= 1; i--) {
        if (elements[i]) {
            continue;
        }

        elements[i] = true;

        int currentStart = i;
        int currentRun = 1;

        long current = i;

        while (current != 1) {
            if (current > biggest) {
                biggest = current;
            }

            if ((current & 1) == 0) {
                current = current >> 1;
            } else {
                current = current + current + current + 1;
            }

            currentRun++;

            if (current < elements.Length) {
                elements[current] = true;
            }
        }

        if (currentRun > longestRun) {
            longestStart = i;
            longestRun = currentRun;
        }
    }   

    Console.WriteLine("Longest Start: {0}, Run {1}", longestStart, longestRun);
    Console.WriteLine("Biggest number: {0}", biggest);
}
xanatos
  • 109,618
  • 12
  • 197
  • 280