【问题标题】:Using matrices to find the number of different ways to write n as the sum of 1, 3, and 4?使用矩阵找出将 n 写为 1、3 和 4 之和的不同方式的数量?
【发布时间】:2017-04-27 22:22:58
【问题描述】:

这是本演示文稿中提出的问题。 Dynamic Programming

现在我已经使用递归实现了算法,它适用于小值。但是当 n 大于 30 时,它变得非常慢。演示文稿提到,对于较大的 n 值,应该考虑类似于 the matrix form of Fibonacci numbers 。我无法理解如何使用斐波那契数列的矩阵形式提出解决方案。有人能给我一些提示或伪代码

谢谢

【问题讨论】:

  • 这是有道理的,当您进行动态编程时,您通常希望使用memoization 以避免一遍又一遍地重新计算相同的值
  • 如果总和中有不同的数字,或者其中有不同的数字顺序,您是否认为总和不同?例如,1 + 3 是否与 3 + 1 相同?
  • @templatetypedef 不,它们应该被视为唯一的
  • @alfasin:这可能只是术语上的差异,但我将“动态编程”定义为当您从“底部”开始以确定性/方法性方式填充数组时(因此替代名称“自下而上递归”)和“记忆化”,就像您只使用标准的自上而下递归策略一样,但添加了结果缓存,以便可以重用已经计算的中间结果。所以它们是相关的,但相互排斥的方法;两者都不使用另一个。
  • @ruakh 感谢上帝... :))) stackoverflow.com/questions/6184869/…

标签: algorithm matrix dynamic-programming fibonacci


【解决方案1】:

是的,您可以使用快速斐波那契实现中的技术在 O(log n) 时间内解决这个问题!以下是操作方法。

让我们从问题陈述中的定义开始,即 1 + 3 被视为与 3 + 1 相同。那么你有以下递归关系:

  • A(0) = 1
  • A(1) = 1
  • A(2) = 1
  • A(3) = 2
  • A(k+4) = A(k) + A(k+1) + A(k+3)

这里的矩阵技巧是注意到

 | 1  0  1  1 | |A( k )|   |A(k) + A(k-2) + A(k-3)|   |A(k+1)|
 | 1  0  0  0 | |A(k-1)|   |         A( k )       |   |A( k )|
 | 0  1  0  0 | |A(k-2)| = |         A(k-1)       | = |A(k-1)|
 | 0  0  1  0 | |A(k-3)|   |         A(k-2)       | = |A(k-2)|

换句话说,将系列中最后四个值的向量相乘会产生一个向量,其中这些值向前移动了一步。

我们称那个矩阵为 M。然后注意

     |A( k )|   |A(k+2)|
     |A(k-1)|   |A(k+1)|
 M^2 |A(k-2)| = |A( k )|
     |A(k-3)|   |A(k-1)|

换句话说,乘以这个矩阵的平方会使序列向下移动两步。更一般地说:

     |A( k )|   |  A(k+n)  |
     |A(k-1)|   |A(k-1 + n)|
 M^n |A(k-2)| = |A(k-2 + n)|
     |A(k-3)|   |A(k-3 + n)|

因此乘以 Mn 将序列向下移动 n 步。现在,如果我们想知道 A(n+3) 的值,我们可以计算

     |A(3)|   |A(n+3)|
     |A(2)|   |A(n+2)|
 M^n |A(1)| = |A(n+1)|
     |A(0)|   |A(n+2)|

并读取向量的顶部条目!这可以通过平方取幂在时间 O(log n) 内完成。这是一些可以做到这一点的代码。这里使用a matrix library I cobbled together a while back:

#include "Matrix.hh"
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <algorithm>
using namespace std;

/* Naive implementations of A. */
uint64_t naiveA(int n) {
    if (n == 0) return 1;
    if (n == 1) return 1;
    if (n == 2) return 1;
    if (n == 3) return 2;
    return naiveA(n-1) + naiveA(n-3) + naiveA(n-4);
}

/* Constructs and returns the giant matrix. */
Matrix<4, 4, uint64_t> M() {
  Matrix<4, 4, uint64_t> result;
  fill(result.begin(), result.end(), uint64_t(0));

  result[0][0] = 1;
  result[0][2] = 1;
  result[0][3] = 1;
  result[1][0] = 1;
  result[2][1] = 1;
  result[3][2] = 1;

  return result;
}

/* Constructs the initial vector that we multiply the matrix by. */
Vector<4, uint64_t> initVec() {
  Vector<4, uint64_t> result;
  result[0] = 2;
  result[1] = 1;
  result[2] = 1;
  result[3] = 1;
  return result;
}

/* O(log n) time for raising a matrix to a power. */
Matrix<4, 4, uint64_t> fastPower(const Matrix<4, 4, uint64_t>& m, int n) {
  if (n == 0) return Identity<4, uint64_t>();
  auto half = fastPower(m, n / 2);

  if (n % 2 == 0) return half * half;
  else return half * half * m;
}

/* Fast implementation of A(n) using matrix exponentiation. */
uint64_t fastA(int n) {
  if (n == 0) return 1;
  if (n == 1) return 1;
  if (n == 2) return 1;
  if (n == 3) return 2;

  auto result = fastPower(M(), n - 3) * initVec();
  return result[0];
}

/* Some simple test code showing this in action! */
int main() {
  for (int i = 0; i < 25; i++) {
    cout << setw(2) << i << ": " << naiveA(i) << ", " << fastA(i) << endl;
  }
}

现在,如果将 3 + 1 和 1 + 3 视为等价,这将如何改变?这意味着我们可以考虑通过以下方式解决这个问题:

  • 令 A(n) 为将 n 写为 1、3 和 4 之和的方式数。
  • 令 B(n) 为将 n 写为 1 和 3 之和的方式数。
  • 令 C(n) 为将 n 写为 1 之和的方式数。

然后我们有以下内容:

  • 对于所有 n ≤ 3,A(n) = B(n),因为对于该范围内的数字,唯一的选择是使用 1 和 3。
  • A(n + 4) = A(n) + B(n + 4),因为您的选择是 (1) 使用 4 或 (2) 不使用 4,剩下的总和使用 1 和3 秒。
  • 对于所有 n ≤ 2,B(n) = C(n),因为对于该范围内的数字,唯一的选择是使用 1。
  • B(n + 3) = B(n) + C(n + 3),因为您的选择是 (1) 使用 3 或 (2) 不使用 3,剩下的总和仅使用 1s .
  • C(0) = 1,因为只有一种方法可以将 0 写为无数之和。
  • C(n+1) = C(n),因为用 1 写东西的唯一方法是取出一个 1 并将剩余的数字写为 1 的总和。

需要考虑的内容很多,但请注意以下几点:我们最终关心的是 A(n),并且为了评估它,我们只需要知道 A(n)、A(n-1) 的值, A(n-2)、A(n-3)、B(n)、B(n-1)、B(n-2)、B(n-3)、C(n)、C(n-1) )、C(n-2) 和 C(n-3)。

例如,假设我们知道这十二个值对于某个固定的 n 值。我们可以为 n 的 next 值学习这十二个值,如下所示:

C(n+1) = C(n)
B(n+1) = B(n-2) + C(n+1) = B(n-2) + C(n)
A(n+1) = A(n-3) + B(n+1) = A(n-3) + B(n-2) + C(n)

然后剩余的值向下移动。

我们可以将其表述为一个巨大的矩阵方程:

  A( n ) A(n-1) A(n-2) A(n-3) B( n ) B(n-1) B(n-2) C( n )
|    0      0      0      1      0      0      1      1   | |A( n )| = |A(n+1)|
|    1      0      0      0      0      0      0      0   | |A(n-1)| = |A( n )|
|    0      1      0      0      0      0      0      0   | |A(n-2)| = |A(n-1)|
|    0      0      1      0      0      0      0      0   | |A(n-3)| = |A(n-2)|
|    0      0      0      0      0      0      1      1   | |B( n )| = |B(n+1)|
|    0      0      0      0      1      0      0      0   | |B(n-1)| = |B( n )|
|    0      0      0      0      0      1      0      0   | |B(n-2)| = |B(n-1)|
|    0      0      0      0      0      0      0      1   | |C( n )| = |C(n+1)|

我们把这个巨大的矩阵称为 M。然后如果我们计算

     |2|  // A(3) = 2, since 3 = 3 or 3 = 1 + 1 + 1
     |1|  // A(2) = 1, since 2 = 1 + 1
     |1|  // A(1) = 1, since 1 = 1
 M^n |1|  // A(0) = 1, since 0 = (empty sum)
     |2|  // B(3) = 2, since 3 = 3 or 3 = 1 + 1 + 1
     |1|  // B(2) = 1, since 2 = 1 + 1
     |1|  // B(1) = 1, since 1 = 1
     |1|  // C(3) = 1, since 3 = 1 + 1 + 1

我们将返回一个向量,其第一个条目是 A(n+3),将 n+3 写入 1、3 和 4 的总和的方式数。 (我实际上已经对此进行了编码以检查它 - 它有效!)然后您可以使用使用矩阵计算斐波那契数的技术,以高效地使用斐波那契数来解决这个问题 O(log n)。

这里有一些代码:

#include "Matrix.hh"
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <algorithm>
using namespace std;

/* Naive implementations of A, B, and C. */
uint64_t naiveC(int n) {
  return 1;
}

uint64_t naiveB(int n) {
  return (n < 3? 0 : naiveB(n-3)) + naiveC(n);
}

uint64_t naiveA(int n) {
  return (n < 4? 0 : naiveA(n-4)) + naiveB(n);
}

/* Constructs and returns the giant matrix. */
Matrix<8, 8, uint64_t> M() {
  Matrix<8, 8, uint64_t> result;
  fill(result.begin(), result.end(), uint64_t(0));

  result[0][3] = 1;
  result[0][6] = 1;
  result[0][7] = 1;
  result[1][0] = 1;
  result[2][1] = 1;
  result[3][2] = 1;
  result[4][6] = 1;
  result[4][7] = 1;
  result[5][4] = 1;
  result[6][5] = 1;
  result[7][7] = 1;

  return result;
}

/* Constructs the initial vector that we multiply the matrix by. */
Vector<8, uint64_t> initVec() {
  Vector<8, uint64_t> result;
  result[0] = 2;
  result[1] = 1;
  result[2] = 1;
  result[3] = 1;
  result[4] = 2;
  result[5] = 1;
  result[6] = 1;
  result[7] = 1;
  return result;
}

/* O(log n) time for raising a matrix to a power. */
Matrix<8, 8, uint64_t> fastPower(const Matrix<8, 8, uint64_t>& m, int n) {
  if (n == 0) return Identity<8, uint64_t>();
  auto half = fastPower(m, n / 2);

  if (n % 2 == 0) return half * half;
  else return half * half * m;
}

/* Fast implementation of A(n) using matrix exponentiation. */
uint64_t fastA(int n) {
  if (n == 0) return 1;
  if (n == 1) return 1;
  if (n == 2) return 1;
  if (n == 3) return 2;

  auto result = fastPower(M(), n - 3) * initVec();
  return result[0];
}

/* Some simple test code showing this in action! */
int main() {
  for (int i = 0; i < 25; i++) {
    cout << setw(2) << i << ": " << naiveA(i) << ", " << fastA(i) << endl;
  }
}

【讨论】:

  • 感谢您的详细解释。但是 1+3 和 3+1 应该被视为两个独特的实体
  • 我刚刚为这种情况添加了更新。 :-)
  • “你可以使用快速斐波那契实现中的技术在 O(log n) 时间内解决这个问题”——好吧,你可以在算术是常数时间的计算模型中。随着结果的巨大,恒定时间算术假设在实践中很快就失效了。
  • @user2357112 完全正确。我假设我们将使用固定宽度的数字并以一些大素数为模工作,因为这看起来像是一个编程竞赛问题。 :-)
  • 与您所说的相反,我认为您的答案的第一部分给出了 1+3 和 3+1 被视为不同的结果,而第二部分则给出了相同的结果.
【解决方案2】:

您可以通过添加 memoization 轻松改进您当前的递归实现,从而再次加快解决方案。 C#代码:

// Dictionary to store computed values
private static Dictionary<int, long> s_Solutions = new Dictionary<int, long>();

private static long Count134(int value) {
  if (value == 0)
    return 1;
  else if (value <= 0)
    return 0;

  long result;

  // Improvement: Do we have the value computed? 
  if (s_Solutions.TryGetValue(value, out result))
    return result;

  result = Count134(value - 4) + 
           Count134(value - 3) + 
           Count134(value - 1);

  // Improvement: Store the value computed for future use
  s_Solutions.Add(value, result);

  return result;
}

这样你就可以轻松调用

Console.Write(Count134(500));

结果(大约需要 2 毫秒)是

3350159379832610737

【讨论】:

    【解决方案3】:

    这是一个非常有趣的序列。它几乎是但不完全是 4 阶斐波那契(又名 Tetranacci)数。从它的伴生矩阵中提取了doubling formulas for Tetranacci 之后,我忍不住为了这个非常相似的递归关系再做一次。

    在我们进入实际代码之前,一些定义和所用公式的简短推导是有序的。定义一个整数序列A,这样:

    A(n) := A(n-1) + A(n-3) + A(n-4)
    

    初始值为A(0), A(1), A(2), A(3) := 1, 1, 1, 2

    对于n &gt;= 0,这是integer compositions 的数量n 从集合{1, 3, 4} 中的部分。这是我们最终希望计算的序列。

    为方便起见,定义一个序列T,这样:

    T(n) := T(n-1) + T(n-3) + T(n-4)
    

    初始值为T(0), T(1), T(2), T(3) := 0, 0, 0, 1

    请注意,A(n)T(n) 只是相互转换。更准确地说,A(n) = T(n+3) 表示所有整数 n。因此,正如another answer 所阐述的,两个序列的伴随矩阵是:

    [0  1  0  0]
    [0  0  1  0]
    [0  0  0  1]
    [1  1  0  1]
    

    调用这个矩阵C,然后让:

    a, b, c, d := T(n), T(n+1), T(n+2), T(n+3)
    
    a', b', c', d' := T(2n), T(2n+1), T(2n+2), T(2n+3)
    

    通过归纳,很容易证明:

    [0  1  0  0]^n = [d-c-a  c-b  b-a  a]
    [0  0  1  0]     [  a    d-c  c-b  b]
    [0  0  0  1]     [  b    b+a  d-c  c]
    [1  1  0  1]     [  c    c+b  b+a  d]
    

    如上所示,对于任何nC^n 都可以仅从其最右侧的列中完全确定。此外,将C^n 与其最右边的列相乘会产生C^(2n) 的最右边的列:

    [d-c-a  c-b  b-a  a][a] = [a'] = [a(2d - 2c - a) + b(2c - b)]
    [  a    d-c  c-b  b][b]   [b']   [     a^2 + c^2 + 2b(d - c)]
    [  b    b+a  d-c  c][c]   [c']   [     b(2a + b) + c(2d - c)]
    [  c    c+b  b+a  d][d]   [d']   [     b^2 + d^2 + 2c(a + b)]
    

    因此,如果我们希望通过重复平方计算某些nC^n,我们只需要在每一步执行矩阵向量乘法,而不是完整的矩阵矩阵乘法。


    现在,用 Python 实现:

    # O(n) integer additions or subtractions
    def A_linearly(n): 
        a, b, c, d = 0, 0, 0, 1 # T(0), T(1), T(2), T(3)
    
        if n >= 0:
            for _ in range(+n):
                a, b, c, d = b, c, d, a + b + d
        else: # n < 0
            for _ in range(-n):
                a, b, c, d = d - c - a, a, b, c
    
        return d # because A(n) = T(n+3)
    
    # O(log n) integer multiplications, additions, subtractions.
    def A_by_doubling(n):
        n += 3 # because A(n) = T(n+3)
    
        if n >= 0:
            a, b, c, d = 0, 0, 0, 1 # T(0), T(1), T(2), T(3)
        else: # n < 0
            a, b, c, d = 1, 0, 0, 0 # T(-1), T(0), T(1), T(2)
    
        # Unroll the final iteration to avoid computing extraneous values
        for i in reversed(range(1, abs(n).bit_length())):
            w = a*(2*(d - c) - a) + b*(2*c - b)
            x = a*a + c*c + 2*b*(d - c)
            y = b*(2*a + b) + c*(2*d - c)
            z = b*b + d*d + 2*c*(a + b)
    
            if (n >> i) & 1 == 0:
                a, b, c, d = w, x, y, z
            else: # (n >> i) & 1 == 1
                a, b, c, d = x, y, z, w + x + z
    
        if n & 1 == 0:
            return a*(2*(d - c) - a) + b*(2*c - b) # w
        else: # n & 1 == 1
            return a*a + c*c + 2*b*(d - c)         # x
    
    
    print(all(A_linearly(n) == A_by_doubling(n) for n in range(-1000, 1001)))
    

    因为编码相当简单,所以序列以通常的方式扩展到负n。还提供了一个简单的线性实现作为参考点。

    对于足够大的n,通过简单(即不严格,并且可能存在缺陷)时序比较,上述对数实现比直接用numpy 对伴随矩阵求幂快10-20 倍。据我估计,计算A(10**12) 仍需要大约 100 年的时间!尽管上面的算法还有改进的空间,但这个数字实在是太大了。另一方面,为某些M 计算A(10**12) mod M 更容易实现。


    与卢卡斯数和斐波那契数直接相关

    事实证明,T(n) 更接近斐波那契,Lucas numbers 比它更接近 Tetranacci。要看到这一点,请注意T(n) 的特征多项式是x^4 - x^3 - x - 1 = 0,它会影响(x^2 - x - 1)(x^2 + 1) = 0。第一个因素是斐波那契和卢卡斯的特征多项式! (x^2 - x - 1)(x^2 + 1) = 0的4个根是两个斐波那契根,phipsi = 1 - phi,以及i-i——-1的两个平方根。

    T(n) 的封闭式表达式或“Binet”公式将具有一般形式:

    T(n) = U(n) + V(n)
    U(n) = p*(phi^n) + q*(psi^n)
    V(n) = r*(i^n) + s*(-i)^n
    

    对于一些常数系数p, q, r, s

    使用T(n) 的初始值,求解系数,应用一些代数,并注意到卢卡斯数具有闭式表达式:L(n) = phi^n + psi^n,我们可以推导出以下关系:

           L(n+1) - L(n)    L(n-1)   F(n) + F(n-2)
    U(n) = ------------- = -------- = ------------
                 5            5           5
    

    其中L(n)L(0), L(1) := 2, 1 的第n 个卢卡斯数,F(n)F(0), F(1) := 0, 1 的第n 个斐波那契数。我们还有:

    V(n) =  1 / 5   if n = 0 (mod 4)
         | -2 / 5   if n = 1 (mod 4)
         | -1 / 5   if n = 2 (mod 4)
         |  2 / 5   if n = 3 (mod 4)
    

    这很丑陋,但对代码来说微不足道。请注意V(n)can also be succinctly expressed 的分子为cos(n*pi/2) - 2sin(n*pi/2)(3-(-1)^n) / 2 * (-1)^(n(n+1)/2),但为了清楚起见,我们使用分段定义。

    这是一个更好、更直接的身份:

    T(n) + T(n+2) = F(n)
    

    本质上,我们可以使用斐波那契数和卢卡斯数来计算 T(n)(因此是 A(n))。从理论上讲,这应该比类似 Tetranacci 的方法更有效。

    众所周知,卢卡斯数可以比斐波那契数更有效地计算,因此我们将根据卢卡斯数计算A(n)。我所知道的最有效、最简单的卢卡斯数算法是 L.F. Johnson 的算法(请参阅他的 2010 paperMiddle and Ripple,卢卡斯数的快速简单 O(lg n) 算法)。一旦我们有了 Lucas 算法,我们就使用恒等式:T(n) = L(n - 1) / 5 + V(n) 来计算 A(n)

    # O(log n) integer multiplications, additions, subtractions
    def A_by_lucas(n):
        n += 3 # because A(n) = T(n+3)
        offset = (+1, -2, -1, +2)[n % 4]
        L = lf_johnson_2010_middle(n - 1)
        return (L + offset) // 5
    
    def lf_johnson_2010_middle(n):
        "-> n'th Lucas number. See [L.F. Johnson 2010a]."
        #: The following Lucas identities are used:
        #:
        #:      L(2n)   = L(n)^2 - 2*(-1)^n
        #:      L(2n+1) = L(2n+2) - L(2n)
        #:      L(2n+2) = L(n+1)^2 - 2*(-1)^(n+1)
        #:
        #: The first and last identities are equivalent.
        #: For the unrolled iteration, the following is also used:
        #:
        #:      L(2n+1) = L(n)*L(n+1) - (-1)^n
        #:
        #: Since this approach uses only square multiplications per loop,
        #: It turns out to be slightly faster than standard Lucas doubling,
        #: which uses 1 square and 1 regular multiplication.
        if n >= 0:
            a, b, sign = 2, 1, +1  # L(0), L(1), (-1)^0
        else: # n < 0
            a, b, sign = -1, 2, -1 # L(-1), L(0), (-1)^(-1)
    
        # unroll the last iteration to avoid computing unnecessary values
        for i in reversed(range(1, abs(n).bit_length())):
            a = a*a - 2*sign # L(2k)
            c = b*b + 2*sign # L(2k+2)
            b = c - a        # L(2k+1)
            sign = +1
    
            if (n >> i) & 1:
                a, b = b, c
                sign = -1
    
        if n & 1:
            return a*b - sign
        else:
            return a*a - 2*sign
    

    您可以验证 A_by_lucas 产生的结果与之前的 A_by_doubling 函数相同,但速度大约快 5 倍。仍然不够快,无法在任何合理的时间内计算 A(10**12)

    【讨论】:

      猜你喜欢
      • 2023-04-11
      • 1970-01-01
      • 1970-01-01
      • 2013-11-30
      • 1970-01-01
      • 1970-01-01
      • 2021-11-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多