有效地计算Lucas序列

时间:2014-05-19 15:45:54

标签: c# performance algorithm

我正在实施p + 1分解算法。为此,我需要计算由以下定义的卢卡序列的元素:

(1) x_0 = 1, x_1 = a
(2) x_n+l  =  2 * a * x_n - x_n-l

我以递归方式实现了它(C#),但对于更大的索引来说效率很低。

static BigInteger Lucas(BigInteger a, BigInteger Q, BigInteger N)
    {
        if (Q == 0)
            return 1;
        if (Q == 1)
            return a;
        else
            return (2 * a * Lucas(a, Q - 1, N) - Lucas(a, Q - 2, N)) % N;
    }

我也知道

(3) x_2n = 2 * (x_n)^2 - 1
(4) x_2n+1 = 2 * x_n+1 * x_n - a
(5) x_k(n+1) = 2 * x_k * x_kn - x_k(n-1)

(3)和(4)应该有助于计算更大的Qs。但我不确定如何。 以某种方式使用Q的二进制形式我认为。

感谢任何帮助。

4 个答案:

答案 0 :(得分:3)

Here我们可以看到如何使用矩阵驱动矩阵找到Nth Fibbonaci数

      n
(1 1)
(1 0)

你可以利用这种方法来计算卢卡斯数,使用矩阵(对于你的情况x_n+l = 2 * a * x_n - x_n-l

        n
(2a -1)
(1   0)

注意矩阵的N次方可以通过exponentiation by squaring

找到log(N)矩阵乘法

答案 1 :(得分:1)

(3) x_2n = 2 * (x_n)^2 - 1
(4) x_2n+1 = 2 * x_n+1 * x_n - a

每当您看到2n时,您应该认为“这可能表示偶数”,同样2n+1可能意味着“这是一个奇数”。

您可以修改x索引,以便左侧有n(以便更容易理解这对应于递归函数调用),只需要小心舍入。

3) 2n     n
=> n      n/2

4) it is easy to see that if x = 2n+1, then n = floor(x/2)
     and similarly n+1 = ceil(x/2)

因此,对于#3,我们有:(伪代码)

if Q is even
   return 2 * (the function call with Q/2) - 1

#4:

else // following from above if
   return 2 * (the function call with floor(Q/2))
            * (the function call with ceil(Q/2)) - a

然后我们还可以合并一些memoization以防止多次计算相同参数的返回值:

  • 保留Q值的地图以返回值。
  • 在函数的开头,检查地图中是否存在Q的值。如果是,则返回相应的返回值。
  • 返回时,将Q的值和返回值添加到地图中。

答案 2 :(得分:0)

第n个卢卡斯数字的值为:

enter image description here

Exponentiation by squaring可用于评估函数。例如,如果n = 1000000000,则n = 1000 * 1000 ^ 2 = 10 * 10 ^ 2 * 1000 ^ 2 = 10 * 10 ^ 2 *(10 * 10 ^ 2)^ 2。通过这种方式简化,您可以大大减少计算次数。

答案 3 :(得分:0)

你可以得到一些改进(只有一百万......),而不需要真正花哨的数学。

首先,让数据流更加明确:

    static BigInteger Lucas(BigInteger a, BigInteger Q, BigInteger N)
    {
        if (Q == 0)
        {
            return 1;
        }
        else if (Q == 1)
        {
            return a;
        }
        else
        {
            BigInteger q_1 = Lucas(a, Q - 1, N);
            BigInteger q_2 = Lucas(a, Q - 2, N);
            return (2 * a * q_1 - q_2) % N;
        }
    }

不出所料,这并没有真正改变性能。

然而,它确实表明我们只需要两个先前的值来计算下一个值。这让我们可以将函数颠倒过来变成迭代版本:

    static BigInteger IterativeLucas(BigInteger a, BigInteger Q, BigInteger N)
    {
        BigInteger[] acc = new BigInteger[2];
        Action<BigInteger> push = (el) => {
                acc[1] = acc[0];
                acc[0] = el;
        };
        for (BigInteger i = 0; i <= Q; i++)
        {
            if (i == 0)
            {
                push(1);
            }
            else if (i == 1)
            {
                push(a);
            }
            else
            {
                BigInteger q_1 = acc[0];
                BigInteger q_2 = acc[1];
                push((2 * a * q_1 - q_2) % N);
            }
        }
        return acc[0];
    }

可能有更清晰的方式来写这个,但它有效。它也快得多。它的速度要快得多,这是一种不切实际的衡量标准。在我的系统上,Lucas(4000000, 47, 4000000)大约需要30分钟,而IterativeLucas(4000000, 47, 4000000)大约需要2毫秒。我想比较48,但我没有耐心。

你可以使用模运算的这些属性来挤出更多(可能是两倍?)

(a + b) % n = (a%n + b%n) % n
(a * b) % n = ((a%n) * (b%n)) % n

如果你应用这些,你会发现a%N发生了几次,所以你可以通过在循环之前预先计算一次来获胜。当a远大于N时,这尤其有用;我不确定你的申请是否会发生这种情况。

可能有一些聪明的数学技术可以将这个解决方案从水中吹出来,但我认为这样的改进只需通过改变一些代码即可实现。