简体   繁体   中英

Dealing with overflow in Java without using BigInteger

Suppose I have a method to calculate combinations of r items from n items:

    public static long combi(int n, int r) {

        if ( r == n) return 1;
        long numr = 1;
        for(int i=n; i > (n-r); i--) {
            numr *=i;

        }
        return numr/fact(r);

    }


public static long fact(int n) {

        long rs = 1;
        if(n <2) return 1;
        for (int i=2; i<=n; i++) {
            rs *=i;
        }
        return rs;

    }

As you can see it involves factorial which can easily overflow the result. For example if I have fact(200) for the foctorial method I get zero. The question is why do I get zero?

Secondly how do I deal with overflow in above context? The method should return largest possible number to fit in long if the result is too big instead of returning wrong answer.

One approach (but this could be wrong) is that if the result exceed some large number for example 1,400,000,000 then return remainder of result modulo 1,400,000,001. Can you explain what this means and how can I do that in Java?

Note that I do not guarantee that above methods are accurate for calculating factorial and combinations. Extra bonus if you can find errors and correct them.

Note that I can only use int or long and if it is unavoidable, can also use double. Other data types are not allowed.

I am not sure who marked this question as homework. This is NOT homework. I wish it was homework and i was back to future, young student at university. But I am old with more than 10 years working as programmer. I just want to practice developing highly optimized solutions in Java. In our times at university, Internet did not even exist. Today's students are lucky that they can even post their homework on site like SO.

Use the multiplicative formula , instead of the factorial formula.

在此输入图像描述

Since its homework, I won't want to just give you a solution. However a hint I will give is that instead of calculating two large numbers and dividing the result, try calculating both together. eg calculate the numerator until its about to over flow, then calculate the denominator. In this last step you can chose the divide the numerator instead of multiplying the denominator. This stops both values from getting really large when the ratio of the two is relatively small.

I got this result before an overflow was detected.

combi(61,30) = 232714176627630544 which is 2.52% of Long.MAX_VALUE

The only "bug" I found in your code is not having any overflow detection, since you know its likely to be a problem. ;)

您可以使用java.math.BigInteger类来处理任意大数。

If you make the return type double , it can handle up to fact(170) , but you'll lose some precision because of the nature of double (I don't know why you'd need exact precision for such huge numbers).

For input over 170 , the result is infinity

To answer your first question (why did you get zero), the values of fact() as computed by modular arithmetic were such that you hit a result with all 64 bits zero! Change your fact code to this:

public static long fact(int n) {
    long rs = 1;
    if( n <2) return 1;
    for (int i=2; i<=n; i++) {
        rs *=i;
        System.out.println(rs);
    }
    return rs;
}

Take a look at the outputs! They are very interesting.

Now onto the second question....

It looks like you want to give exact integer (er, long ) answers for values of n and r that fit, and throw an exception if they do not. This is a fair exercise.

To do this properly you should not use factorial at all. The trick is to recognize that C(n,r) can be computed incrementally by adding terms. This can be done using recursion with memoization, or by the multiplicative formula mentioned by Stefan Kendall.

As you accumulate the results into a long variable that you will use for your answer, check the value after each addition to see if it goes negative. When it does, throw an exception. If it stays positive, you can safely return your accumulated result as your answer.

To see why this works consider Pascal's triangle

1
1  1
1  2   1
1  3   3   1
1  4   6   4   1
1  5  10  10   5  1
1  6  15  20  15  6  1

which is generated like so:

C(0,0) = 1 (base case)
C(1,0) = 1 (base case)
C(1,1) = 1 (base case) 
C(2,0) = 1 (base case) 
C(2,1) = C(1,0) + C(1,1) = 2
C(2,2) = 1 (base case)
C(3,0) = 1 (base case)
C(3,1) = C(2,0) + C(2,1) = 3
C(3,2) = C(2,1) + C(2,2) = 3
...

When computing the value of C(n,r) using memoization, store the results of recursive invocations as you encounter them in a suitable structure such as an array or hashmap. Each value is the sum of two smaller numbers. The numbers start small and are always positive. Whenever you compute a new value (let's call it a subterm) you are adding smaller positive numbers. Recall from your computer organization class that whenever you add two modular positive numbers, there is an overflow if and only if the sum is negative. It only takes one overflow in the whole process for you to know that the C(n,r) you are looking for is too large.

This line of argument could be turned into a nice inductive proof, but that might be for another assignment, and perhaps another StackExchange site.

ADDENDUM

Here is a complete application you can run. (I haven't figured out how to get Java to run on codepad and ideone).

/**
 * A demo showing how to do combinations using recursion and memoization, while detecting
 * results that cannot fit in 64 bits.
 */
public class CombinationExample {

    /**
     * Returns the number of combinatios of r things out of n total.
     */
    public static long combi(int n, int r) {
        long[][] cache = new long[n + 1][n + 1];
        if (n < 0 || r > n) {
            throw new IllegalArgumentException("Nonsense args");
        }
        return c(n, r, cache);
    }

    /**
     * Recursive helper for combi.
     */
    private static long c(int n, int r, long[][] cache) {
        if (r == 0 || r == n) {
            return cache[n][r] = 1;
        } else if (cache[n][r] != 0) {
            return cache[n][r];
        } else {
            cache[n][r] = c(n-1, r-1, cache) + c(n-1, r, cache);
            if (cache[n][r] < 0) {
                throw new RuntimeException("Woops too big");
            }
            return cache[n][r];
        }
    }

    /**
     * Prints out a few example invocations.
     */
    public static void main(String[] args) {
        String[] data = ("0,0,3,1,4,4,5,2,10,0,10,10,10,4,9,7,70,8,295,100," +
                "34,88,-2,7,9,-1,90,0,90,1,90,2,90,3,90,8,90,24").split(",");
        for (int i = 0; i < data.length; i += 2) {
            int n = Integer.valueOf(data[i]);
            int r = Integer.valueOf(data[i + 1]);
            System.out.printf("C(%d,%d) = ", n, r);
            try {
                System.out.println(combi(n, r));
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

Hope it is useful. It's just a quick hack so you might want to clean it up a little.... Also note that a good solution would use proper unit testing, although this code does give nice output.

Note that java.lang.Long includes constants for the min and max values for a long.

When you add together two signed 2s-complement positive values of a given size, and the result overflows, the result will be negative. Bit-wise, it will be the same bits you would have gotten with a larger representation, only the high-order bit will be truncated away.

Multiplying is a bit more complicated, unfortunately, since you can overflow by more than one bit.

But you can multiply in parts. Basically you break the to multipliers into low and high halves (or more than that, if you already have an "overflowed" value), perform the four possible multiplications between the four halves, then recombine the results. (It's really just like doing decimal multiplication by hand, but each "digit" is, say, 32 bits.)

You can copy the code from java.math.BigInteger to deal with arbitrarily large numbers. Go ahead and plagiarize.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM