使用数组来改善递归二项分布算法的执行时间?

时间:2012-04-27 19:48:48

标签: java arrays algorithm math

作为一名初学程序员,我最近买了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)]的方法,我已经使这个算法变得更快了,它使用迭代方法来寻找阶乘。但是,我不明白如何使用数组来改善此上下文中的执行时间。

非常感谢任何帮助!

......在有人问之前,这不是作业!

3 个答案:

答案 0 :(得分:5)

本书试图教你一种名为memoization的特殊编程技术,这种技术被称为dynamic programming。当然,在现实生活中,了解封闭形式的解决方案要好得多,但不能解决这个问题。

无论如何,我们的想法是将2D数组作为第四个参数传递,最初用NaN填充它,然后检查是否存在n和{k的给定组合的解决方案计算任何东西之前在数组中{1}}。如果有,请归还;如果没有,则递归计算,存储在数组中,然后才返回。

答案 1 :(得分:3)

此处的递归算法最终会反复调用特定条件。例如:

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;
}

答案 2 :(得分:0)

有点晚了但是对于那些正在寻找完整解决方案的人来说,这是我的下方。首先,我建议其他人阅读这里给出的答案: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)

是的,你可以使用memoization方法,基本上正如其他人已经说明的那样,它基本上是先前计算过的缓存值。 对于某些人而言,memoization意味着保持递归策略并检查我们需要的值是否已计算,如果不是程序必须计算并缓存它,它实际上很容易实现:

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];
}

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

希望这有帮助。