Blog of Manuel Vieda

Blog of Manuel Vieda


Thoughts, stories and ideas.

Software Engineer, Application Developer and Information Technology enthusiast with over 8 years of experience working with Java and related technologies.

Share


Tags


The 3n + 1 problem solved

Problems in Computer Science are often classified as belonging to a certain class of problems (e.g., NP, Unsolvable, Recursive). In this problem you will be analyzing a property of an algorithm whose classification is not known for all possible inputs.

The Problem

Consider the following algorithm:

1. input n
2. print n
3. if n = 1 then STOP
     4. if n is odd then n = 3n + 1
     5. else n = n / 2
6. GOTO 2

Given the input 22, the following sequence of numbers will be printed 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1

It is conjectured that the algorithm above will terminate (when a 1 is printed) for any integral input value. Despite the simplicity of the algorithm, it is unknown whether this conjecture is true. It has been verified, however, for all integers n such that 0 < n < 1,000,000 (and, in fact, for many more numbers than this.)

Given an input n, it is possible to determine the number of numbers printed (including the 1). For a given n this is called the cycle-length of n. In the example above, the cycle length of 22 is 16.

For any two numbers i and j you are to determine the maximum cycle length over all numbers between i and j.

The input

The input will consist of a series of pairs of integers i and j, one pair of integers per line. All integers will be less than 1,000,000 and greater than 0.

You should process all pairs of integers and for each pair determine the maximum cycle length over all integers between and including i and j.

You can assume that no operation overflows a 32-bit integer.

The Output

For each pair of input integers i and j you should output i, j, and the maximum cycle length for integers between and including i and j. These three numbers should be separated by at least one space with all three numbers on one line and with one line of output for each line of input. The integers i and j must appear in the output in the same order in which they appeared in the input and should be followed by the maximum cycle length (on the same line).

Sample Input:
1 10
100 200
201 210
900 1000

Sample Output:
1 10 20
100 200 125
201 210 89
900 1000 174

Online Judges

Hints

Read the problem statement carefully. Competitive programming problems have a reputation for including extraneous information and confusing language in their descriptions, so take notes about key ideas as you read so you can use them later instead of re-reading the problem.

Pay attention to boundary cases: The type of input (32 bits signed integers is enough?) The order of the input (Can be the first number higher than second? Is it possible that the first and second numbers to be equals?).

Solution

The given algorithm corresponds with the Collatz Problem and the Hailstone Sequence. If each number is the start of the Hailstone sequence, we need to find the the number that generates the longer sequence. Using pseudo code, what we want to solve can be expressed as:

read min, max from input
for each x between min and max
  length = hailstone sequence length (x)
return max(length)

The first thing we need to do is calculate the length of the hailstone sequence of a given integer:

public static long cycleLength(long n) {
   if (n == 1) {
      return 1;
   }
   return 1 + cycleLength(n%2 == 0 ? n/2 : 3*n+1);
}

After calculating the cycle length (Hailstone sequence length) for a given number, the next step is to calculate it for all the numbers in the given range and return the maximum value. Remember that the start number can be equals or even grater than the end number.

public static long maxCycleLength(long start, long end) {

    long min = Math.min(start, end);
    long max = Math.max(start, end);
    long maxCycleLength = 0;

    for (long i = min; i <= max; i++) {
        long cycleLength = cycleLength(i);
        if (cycleLength > maxCycleLength) {
            maxCycleLength = cycleLength;
        }
    }

    return maxCycleLength;
}

The last thing we have to do to get a working solution is to implement the main method. We need a while-loop that reads from the input the test cases and then print the solution given by the last method.

public static void main(final String[] args) {

  try (
      final Scanner in = new Scanner(System.in);
      PrintWriter out = new PrintWriter(System.out);
  ) {

    while (in.hasNextInt()) {
      int i = in.nextInt();
      int j = in.nextInt();
      long resp = maxCycleLength(i, j);
      out.print(i + " " + j + " " + resp);
    }
  }
}

Optimization - Caching

At this point our solutions looks complete, but if look closer we notice that we are calculating the cycle length of some values multiple times. Therefore it's a good idea to save what we’ve computed, so we don’t have to compute it again (Caching). The easy and effective way to implement the cache is an array where the n-th element is the cycle length of the number n. A value of 0 in the array means that the length for n has not yet been calculated.

To do it, we need to include the cache and modify cycleLength() method as follows:

public static final int cacheSize = 1000000;
private static long[] cache = new long[cacheSize];

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

  // Return cycle length from cache if already calculated
  if (n < cacheSize && cache[(int) n] != 0) {
    return cache[(int) n];
  }

  // Calculate cycle length and save in the cache
  long length = 1 + cycleLength(n % 2 == 0 ? n / 2 : 3 * n + 1);
  if (n < cacheSize) {
    cache[(int) n] = length;
  }
  return length;
}

Finally, we can add some initial values to the cache (e.g. We already knows the last elements of all the Hailstone sequence for any number). With this initial values, we can also delete the validation for n=1 since it will be retrieved from the cache:

public static final int cacheSize = 1000000;
private static long[] cache = new long[cacheSize];

static {
  cache[1] = 1;
  cache[2] = 2;
  cache[4] = 3;
  cache[8] = 4;
  cache[16] = 5;
}

public static long cycleLength(long n) {

  // Return cycle length from cache if already calculated
  if (n < cacheSize && cache[(int) n] != 0) {
    return cache[(int) n];
  }

  // Calculate cycle length and save in the cache
  long length = 1 + cycleLength(n % 2 == 0 ? n / 2 : 3 * n + 1);
  if (n < cacheSize) {
    cache[(int) n] = length;
  }
  return length;
}

Finally, some micro-optimizations that can be used if want to save some nanosecond: Replace the modulus and multiplication operation with bitwise operations:

long length = 1 + cycleLength((n & 1) == 0 ? n >> 1 : n << 1 + n + 1);

Code

You can take a look at the complete source code here.

So what do you think? Did/would you solve it differently? Let me know in the comments.

Software Engineer, Application Developer and Information Technology enthusiast with over 8 years of experience working with Java and related technologies.

View Comments