algorithm - 使用矩阵找出将 n 写为 1、3 和 4 之和的不同方式的数量?

标签 algorithm matrix dynamic-programming fibonacci

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

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

谢谢

最佳答案

是的,您可以使用快速斐波那契实现中的技术在时间 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,剩下的总和仅使用 1 .
  • 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 的下一个值学习这十二个值,如下所示:

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

关于algorithm - 使用矩阵找出将 n 写为 1、3 和 4 之和的不同方式的数量?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41111249/

相关文章:

algorithm - 预期运行时间与最坏情况运行时间

arrays - 在给定矩阵的每一行中找到最后一个非零元素的索引?

python - 如何找到覆盖屋顶孔洞所需的最少瓷砖数量

algorithm - 使用最少的插入将字符串转换为回文字符串

java - 一种在字母数组中搜索单词的算法

c - C 中合并两个链表时遇到问题

algorithm - Shazam/soundhound 是如何工作的?

python - 将大矩阵与dask相乘

ruby - 使用 Ruby 搜索矩阵

python - 给定索引约束的最大和子数组