【问题标题】:Using an array to improve the execution time of a recursive binomial distribution algorithm?使用数组来提高递归二项分布算法的执行时间?
【发布时间】:2012-05-08 13:47:38
【问题描述】:

作为一名初级程序员,我最近购买了 Robert Sedgewick/Kevin Wayne 的《算法 - 第四版》一书,我非常感谢每章末尾的练习。然而,有一个练习(看起来很简单)让我抓狂,因为我找不到解决方案。

您必须采用这种递归算法来计算在 n 次试验中恰好获得 k 次成功的概率,其中 p 是一个事件成功的概率。给出的算法基于递归二项分布公式。

public static double binomial(int n, int k, double p) {
    if (n == 0 && k == 0)
        return 1.0;
    else if (n < 0 || k < 0)
        return 0.0;
    return (1 - p) * binomial(n - 1, k, p) + p * binomial(n - 1, k - 1, p);
}

本练习的目标是通过将计算值保存在数组中来使该算法更快。我已经通过使用另一种获取二项式分布 [p(x) = nCr * p^k * (1 - p)^(n - k)] 的方法使该算法变得更快,该方法使用迭代方法来查找阶乘。但是,我不明白在这种情况下如何使用数组来缩短执行时间。

任何帮助将不胜感激!

...在有人问之前,这不是功课!

【问题讨论】:

  • 您的递归是否需要重新计算您已有的任何数字?如果是这种情况,通过保存这些数字,您可以直接参考它们而不是再次计算它们。
  • 如果这不是家庭作业,你应该使用二项分布的封闭形式:en.wikipedia.org/wiki/Binomial_distribution

标签: java arrays algorithm math


【解决方案1】:

本书试图教你一种特殊的编程技术,称为memoization,一种更广泛的技术,称为dynamic programming。当然,在现实生活中知道一个封闭形式的解决方案要好得多,但在解决这个练习的上下文中却不是这样。

无论如何,我们的想法是传递一个二维数组作为您的第四个参数,最初用NaNs 填充它,并在计算之前检查数组中的nk 的给定组合是否有解决方案任何事物。如果有,请退还;如果没有,则递归计算,存储在数组中,然后才返回。

【讨论】:

  • 所以不要每次都进行计算,先检查我们是否已经知道它。对吗?
  • 值得注意的是,您实际上并不需要 2D 数组来存储值。如果您使用迭代从 n = 0 系统地向上计算值,而不是使用递归从 n 向下计算,则更容易避免这种情况。 (由于历史原因,“动态编程”一词在指代这种迭代结构时更常用,而“记忆”一词在指代递归方法时更常用。两者通常是等价的。)
  • @ismaelga 是的,这就是记忆技术背后的想法。从本质上讲,您使用内存来加快计算速度。
  • @GarethMcCaughan 你是完全正确的。如果在本书后面的练习中通过使用 DP 来优化内存,我不会感到惊讶。
【解决方案2】:

这里的递归算法最终会一遍又一遍地调用特定条件。例如:

3, 3
  2, 3
    1, 3
      0, 3
      0, 2
    1, 2
      0, 2
      0, 1
  2, 2
    1, 2
      0, 2
      0, 1
    1, 1
      0, 1
      0, 0

例如,通过记住 (1, 2) 的值,并在再次使用这些参数调用时立即返回该值,可以提高效率。使用 Guava 的 Table,它看起来像:

public static double binomial(int n, int k, double p, Table<Integer, Integer, Double> memo) {
    if(memo.contains(n, k))
        return memo.get(n, k);

    double result;
    if (n == 0 && k == 0)
        result = 1.0;
    else if (n < 0 || k < 0)
        result = 0.0;
    else 
        result = (1 - p) * binomial(n - 1, k, p) + p * binomial(n - 1, k - 1, p);

    memo.put(n, k, result);
    return result;
}

【讨论】:

    【解决方案3】:

    有点晚了,但对于那些正在寻找完整解决方案的人来说,下面是我的。首先,我建议其他人阅读此处给出的答案:https://stackoverflow.com/a/6165124/4636721 以了解动态编程、记忆和制表的含义

    无论如何我的解决方案,所以基本上我们有给定的方法:

    // Not efficient at all
    private static double binomial(int N, int k, double p)
    {
        if (N == 0 && k == 0)
        {
            return 1.0;
        }
        else if ((N < 0) || (k < 0))
        {
            return 0.0;
        }
        else
        {
            return (1.0 - p) * binomial(N - 1, k, p) + p * binomial(N - 1, k - 1, p);
        }
    }
    

    是的,这真的很慢......递归调用的数量有点大(大约〜N^2)

    是的,您可以使用基本上就像其他人已经说过的那样基本上缓存先前计算的值的记忆方法。 对于某些人来说,记忆意味着保持递归策略并检查我们需要的值是否已计算,如果没有程序必须计算并缓存它,它真的很容易实现:

    private static double binomialTopDown(int N, int k, double p)
    {
        double[][] cache = new double[N + 1][k + 1];
    
        for (int i = 0; i < (N + 1); i++)
        {
             Arrays.fill(cache[i], Double.NaN);
        }
    
        return binomialTopDown(N, k, p, cache);
    }
    
    // More efficient
    private static double binomialTopDown(int N, int k, double p, double[][] cache)
    {
        if ((N == 0) && (k == 0))
        {
            return 1.0;
        }
        else if ((N < 0) || (k < 0))
        {
            return 0.0;
        }
        else if (Double.isNaN(cache[N][k]))
        {
            cache[N][k] = (1.0 - p) * binomialTopDown(N - 1, k, p, cache) + p * binomialTopDown(N - 1, k - 1, p, cache);
        }
    
        return cache[N][k];
    }
    

    诀窍实际上是使用自下而上的方法(也称为制表法)以更有效的方式对计算进行排序。这通常通过使用上述算法的迭代版本来实现。

    // Much more efficient
    private static double binomialBottomUp(int N, int k, double p)
    {
        /*
        double[][] cache = new double[N + 1][k + 1];
    
        cache[0][0] = 1.0;
    
        for (int i = 1; i <= N; i++)
        {
            cache[i][0] = Math.pow(1.0 - p, i);
    
            for (int j = 1; j <= k; j++)
            {
                cache[i][j] =  p * cache[i - 1][j - 1] + (1.0 - p) * cache[i - 1][j];
            }
        }
    
        return cache[N][k];
        */
    
        // Optimization using less memory, swapping two arrays
        double[][] cache = new double[2][k + 1];
        double[] previous = cache[0];
        double[] current = cache[1];
        double[] temp;
    
        previous[0] = 1.0;
    
        for (int i = 1; i <= N; i++)
        {
            current[0] = Math.pow(1.0 - p, i);
    
            for (int j = 1; j <= k; j++)
            {
                current[j] =  p * previous[j - 1] + (1.0 - p) * previous[j];
            }
    
            temp = current;
            current = previous;
            previous = temp;
        }
    
        return previous[k];
    }
    

    使用自下而上方法的动态编程是最有效的方法。

    希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-09-06
      • 2014-12-01
      • 1970-01-01
      • 2020-08-10
      • 2015-06-16
      • 2017-07-15
      • 2015-11-08
      • 2019-09-03
      相关资源
      最近更新 更多