简体   繁体   English

在不使用BigInteger的情况下处理Java中的溢出

[英]Dealing with overflow in Java without using BigInteger

Suppose I have a method to calculate combinations of r items from n items: 假设我有一个方法来计算来自n个项目的r个项目的组合:

    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. 例如,如果我对于foctorial方法有事实(200),我得到零。 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. 一种方法(但这可能是错误的)是,如果结果超过一些大数,例如1,400,000,000,那么返回结果的余数为1,400,000,001。 Can you explain what this means and how can I do that in Java? 你能解释一下这意味着什么,我怎么能用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. 请注意,我只能使用int或long,如果不可避免,也可以使用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. 我只想练习用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. 今天的学生很幸运,他们甚至可以像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. 我在你的代码中找到的唯一“bug”没有任何溢出检测,因为你知道它可能是一个问题。 ;) ;)

您可以使用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). 如果你使返回类型为double ,它可以处理fact(170) ,但是你会因为double的性质而失去一些精度(我不知道为什么你需要精确的精确度来获得这么大的数字)。

For input over 170 , the result is infinity 对于超过170输入,结果是无穷大

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! 为了回答你的第一个问题(为什么你得零),通过模运算计算的fact()的值是这样的,你得到所有64位为零的结果! 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. 看起来你想为适合的nr值给出精确的整数(呃, long )答案,如果没有,则抛出异常。 This is a fair exercise. 这是一个公平的练习。

To do this properly you should not use factorial at all. 要做到这一点,你不应该使用factorial。 The trick is to recognize that C(n,r) can be computed incrementally by adding terms. 诀窍是认识到可以通过添加项来递增地计算C(n,r) This can be done using recursion with memoization, or by the multiplicative formula mentioned by Stefan Kendall. 这可以通过使用memoization的递归或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. 当您将结果累积到将用于答案的long变量时,请在每次添加后检查该值以查看它是否为负值。 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 要了解其原因,请考虑Pascal的三角形

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. 使用memoization计算C(n,r)的值时,在适当的结构(如数组或散列映射)中遇到它们时,存储递归调用的结果。 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. 在整个过程中只需要一次溢出就可以让你知道你要找的C(n,r)太大了。

This line of argument could be turned into a nice inductive proof, but that might be for another assignment, and perhaps another StackExchange site. 这一论点可以转化为一个很好的归纳证明,但这可能是另一个任务,也许是另一个StackExchange站点。

ADDENDUM 附录

Here is a complete application you can run. 这是一个完整的应用程序,您可以运行。 (I haven't figured out how to get Java to run on codepad and ideone). (我还没弄清楚如何让Java在codepad和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. 请注意,java.lang.Long包含long的最小值和最大值的常量。

When you add together two signed 2s-complement positive values of a given size, and the result overflows, the result will be negative. 当您将给定大小的两个带符号2s补码正值相加并且结果溢出时,结果将为负。 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.) (这实际上就像手动进行十进制乘法,但每个“数字”就是32位。)

You can copy the code from java.math.BigInteger to deal with arbitrarily large numbers. 您可以从java.math.BigInteger复制代码来处理任意大数字。 Go ahead and plagiarize. 来吧抄袭吧。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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