【问题标题】:Algorithm to efficiently determine the [n][n] element in a matrix有效确定矩阵中 [n][n] 元素的算法
【发布时间】:2015-04-30 06:19:06
【问题描述】:

这是一个关于课程作业的问题,所以希望你没有完全回答这个问题,而是提供一些技巧来提高我当前算法的运行时间复杂度。

我得到了以下信息:

函数 g(n) 由 g(n) = f(n,n) 给出,其中 f 可以递归定义

我已经用以下代码递归地实现了这个算法:

public static double f(int i, int j) 
{

    if (i == 0 && j == 0) {
        return 0;
    }
    if (i ==0 || j == 0) {
        return 1;
    }

    return ((f(i-1, j)) + (f(i-1, j-1)) + (f(i, j-1)))/3;
}

此算法给出了我正在寻找的结果,但效率极低,我现在的任务是提高运行时间复杂度。

我编写了一个算法来创建一个 n*n 矩阵,然后它计算每个元素直到 [n][n] 元素,然后返回 [n][n] 元素,例如 f(1, 1) 将返回 0.6 重复。 [n][n] 元素是 0.6 重复的,因为它是 (1+0+1)/3 的结果。

我还创建了一个从 f(0,0) 到 f(7,7) 的结果的电子表格,如下所示:

虽然这比我的递归算法快得多,但创建 n*n 矩阵的开销很大。

任何关于如何改进此算法的建议将不胜感激!

我现在可以看到是否可以使算法复杂度为 O(n),但是是否可以在不创建 [n][n] 2D 数组的情况下计算结果?

我已经创建了一个在 O(n) 时间和 O(n) 空间中运行的 Java 解决方案,并将在我提交课程作业后发布解决方案以阻止任何抄袭。

【问题讨论】:

  • 将 for 循环从 O(n*n) 转换为 O(n)。
  • "现在虽然这比我的递归算法快得多,但我知道它可以变得更有效率!"因为 .... ?我不是说你错了,我只是说当你对某事如此确定时,你可以解释是什么让你如此确定
  • 您可以“展开”递归并在可能的情况下应用算术简化。
  • 是的,你不必创建一个 [n][n] 数组,因为在你的计算中你只需要 [x-1] 和 [y-1] 值,所以你可以只[n] 的线性数组,用于存储上一次迭代的值。
  • 虽然到目前为止的一些帖子包含一些对此类任务有用的一般信息(特别是对 OEIS 的引用),但其中没有确实有助于回答这个问题: 分母部分相当琐碎。与此相反,直到现在,分子都拒绝了我的所有分析尝试(顺便说一句,这远远超出了尝试将序列粘贴到 OEIS 的范围......)。我很好奇是否有人为此提出了一个封闭形式的解决方案......

标签: java algorithm matrix big-o


【解决方案1】:

感谢will的(第一个)回答,我有了这个想法:

考虑任何正解仅来自沿xy 轴的1。对f 的每个递归调用都将解决方案的每个组成部分除以 3,这意味着我们可以组合地求和每个1 作为解决方案组成部分的方式,并认为它是“距离”(测量为来自目标的f的调用次数)作为3的负幂。

JavaScript 代码:

function f(n){

  var result = 0;

  for (var d=n; d<2*n; d++){

    var temp = 0;

    for (var NE=0; NE<2*n-d; NE++){

      temp += choose(n,NE);
    }

    result += choose(d - 1,d - n) * temp / Math.pow(3,d);
  }

  return 2 * result;
 }

function choose(n,k){
  if (k == 0 || n == k){
    return 1;
  }
  var product = n;
  for (var i=2; i<=k; i++){
    product *= (n + 1 - i) / i
  }
  return product;
}

输出:

for (var i=1; i<8; i++){
  console.log("F(" + i + "," + i + ") = " + f(i));
}

F(1,1) = 0.6666666666666666
F(2,2) = 0.8148148148148148
F(3,3) = 0.8641975308641975
F(4,4) = 0.8879743941472337
F(5,5) = 0.9024030889600163
F(6,6) = 0.9123609205913732
F(7,7) = 0.9197747256986194

【讨论】:

  • 你确定这行得通吗? 0 0 delannoy 数是 1,而这里的数列是 0。 delannoy 数也没有因子 3 作为除数。毫无疑问,它与答案有关,因为它与加泰罗尼亚数字有关,并且它们在这样的问题中无处不在。
  • @will 正如您所指出的,Delannoy 递归关系与我的想法仅相似但不一样。虽然 Delannoy 数计算有对角线和没有对角线的路径,但我的方法必须根据路径的长度来分隔路径(然后将其应用为 3 的明显负幂)。看看我的逻辑——正如我的例子所示,我相信它是合理的。
  • 啊,自从我阅读以来,您似乎添加了一些新内容。我找到了另一种使用帕斯卡三角形导数的解决方案 - 这总是涉及!
  • 我现在正在阅读的论文中也提到了 Delannoy 数,这里有很多我不是 100% 的东西,还有一些关于广义帕斯卡三角形等的东西.
  • 有人可以提供有关此答案的更多信息吗?我仍在尝试为最多 f(1000000,1000000) 的大数编写算法,我目前的工作方式仅限于可用的内存。
【解决方案2】:

这是另一个问题,最好在深入研究和编写代码之前对其进行检查。

我要说你应该做的第一件事是查看数字网格,不要将它们表示为小数,而是用分数表示。

首先应该清楚的是,您拥有的 的总数只是与原点 的距离的度量。

如果你这样看一个网格,你可以得到所有的分母:

请注意,第一行和第一列并不都是1s - 它们被选择遵循模式,以及适用于所有其他方格的通用公式。

分子有点棘手,但仍然可行。与大多数此类问题一样,答案与组合、阶乘以及一些更复杂的事情有关。这里的典型条目包括Catalan numbersStirling's numbersPascal's triangle,您几乎总是会看到使用了Hypergeometric functions

除非您大量数学,否则您不太可能熟悉所有这些,并且有大量的文学作品。所以我有一个更简单的方法来找出你需要的关系,这几乎总是有效的。它是这样的:

  1. 编写一个简单、低效的算法来获得您想要的序列。
  2. 将相当多的数字复制到 google 中。
  3. 希望Online Encyclopedia of Integer Sequences 的结果弹出。

    3.b。如果没有,请查看您的序列中的一些差异,或与您的数据相关的其他一些序列。

  4. 使用您找到的信息来实现所述序列。

所以,按照这个逻辑,这里是分子:

现在,不幸的是,谷歌搜索没有任何结果。但是,您可以注意到一些关于它们的事情,主要是第一行/列只是 3 的幂,而第二行/列是小于 3 的幂。这种边界与帕斯卡三角形一模一样,还有很多相关的序列。

这是分子和分母之间的差异矩阵:

我们决定 f(0,0) 元素应该遵循相同的模式。这些数字看起来已经简单多了。还要注意——相当有趣的是,这些数字遵循与初始数字相同的规则——除了第一个数字是一个(并且它们被一列和一行偏移)。 T(i,j) = T(i-1,j) + T(i,j-1) + 3*T(i-1,j-1):

                     1 
                  1     1
               1     5     1
            1     9     9     1
         1     13    33    13    1
      1     17    73    73    17    1
   1     21    129   245   192   21    1
1     25    201   593   593   201   25    1

这看起来更像你在组合数学中经常看到的序列。

If you google numbers from this matrix, you do get a hit.

然后如果你切断原始数据的链接,你会得到序列A081578,它被描述为“Pascal-(1,3,1) 数组”,这完全有道理 - 如果你旋转矩阵,使0,0元素在顶部,元素组成一个三角形,那么你取左边元素1*,上面元素3*,右边元素1*

现在的问题是实现用于生成数字的公式。

不幸的是,这往往说起来容易做起来难。比如页面上给出的公式:

T(n,k)=sum{j=0..n, C(k,j-k)*C(n+k-j,k)*3^(j-k)}

是错误的,需要大量阅读 the paper(链接在页面上)才能得出正确的公式。你想要的部分是命题 26,推论 28。在命题 13 之后的表 2 中提到了序列。注意r=4

命题 26 给出了正确的公式,但那里也有一个错字:/。总和中的k=0 应该是j=0

其中T 是包含系数的三角矩阵。

OEIS 页面确实提供了几个计算数字的实现,但是它们都不是 java 的,而且它们都不能轻易地转录为 java:

有一个数学例子:

Table[ Hypergeometric2F1[-k, k-n, 1, 4], {n, 0, 10}, {k, 0, n}] // Flatten 

一如既往地简洁得可笑。还有一个 Haskell 版本,同样简洁:

a081578 n k = a081578_tabl !! n !! k
a081578_row n = a081578_tabl !! n
a081578_tabl = map fst $ iterate
   (\(us, vs) -> (vs, zipWith (+) (map (* 3) ([0] ++ us ++ [0])) $
                      zipWith (+) ([0] ++ vs) (vs ++ [0]))) ([1], [1, 1])

我知道您在 java 中这样做,但我懒得抄写我对 java 的回答(抱歉)。这是一个python实现:

from __future__ import division
import math

#
# Helper functions
#

def cache(function):
  cachedResults = {}
  def wrapper(*args):
    if args in cachedResults:
      return cachedResults[args]
    else:
      result = function(*args)
      cachedResults[args] = result
      return result
  return wrapper


@cache
def fact(n):
 return math.factorial(n)

@cache
def binomial(n,k):
  if n < k: return 0
  return fact(n) / ( fact(k) * fact(n-k) )





def numerator(i,j):
  """
  Naive way to calculate numerator
  """
  if i == j == 0:
    return 0
  elif i == 0 or j == 0:
    return 3**(max(i,j)-1)
  else:
    return numerator(i-1,j) + numerator(i,j-1) + 3*numerator(i-1,j-1)

def denominator(i,j):
  return 3**(i+j-1)



def A081578(n,k):
  """
  http://oeis.org/A081578
  """
  total = 0
  for j in range(n-k+1):
    total += binomial(k, j) * binomial(n-k, j) * 4**(j)
  return int(total)

def diff(i,j):
  """
  Difference between the numerator, and the denominator. 
  Answer will then be 1-diff/denom.
  """
  if i == j == 0:
    return 1/3
  elif i==0 or j==0:
    return 0
  else:
    return A081578(j+i-2,i-1)

def answer(i,j):
  return 1 - diff(i,j) / denominator(i,j)




# And a little bit at the end to demonstrate it works.
N, M = 10,10

for i in range(N):
  row = "%10.5f"*M % tuple([numerator(i,j)/denominator(i,j) for j in range(M)])
  print row

print ""
for i in range(N):
  row = "%10.5f"*M % tuple([answer(i,j) for j in range(M)])
  print row

所以,对于一个封闭的形式:

只是二项式系数。

结果如下:

最后一个加法,如果您希望对大数执行此操作,那么您将需要以不同的方式计算二项式系数,因为您会溢出整数。不过,您的答案是 lal 浮点数,而且由于您显然对大 f(n) = T(n,n) 感兴趣,那么我想您可以使用斯特林近似值之类的。

【讨论】:

  • 我正在尝试遵循您的封闭公式 - 如果我错了,请纠正我:F(2,2) = 1 - (2 choose 0 * 0 choose 0 * 4^2 + 2 choose 1 * 0 choose 1 * 4^2) / 3^3 = 0.4074 这个结果似乎与 OP 和我的不同。你能帮我理解发生了什么吗?
  • @גלעדברקן 哎呀,我忘了转换矩阵索引。等一下。
  • 酷。顺便说一句,我终于在我的答案中添加了一个公式和 JavaScript 中的概念证明,尽管从你发现的材料来看,我的数学似乎可以进一步减少。无论如何,我很自豪能够自己想出它。
  • 你可以让这个公式在计算机上运行,​​实际上,n = 1000000 吗?我没有取得太大的成功。
  • 您需要更改在nCrs 中计算阶乘的方式——这些都是您一直会遇到的问题。如果一个近似值没问题,那么n! 有很多近似值。获得正确的整数表示将变得更加困难。
【解决方案3】:

如果问题是关于如何输出0&lt;=i&lt;N0&lt;=j&lt;N的函数的所有值,这里有一个时间O(N²)和空间O(N)的解决方案。时间行为是最优的。

Use a temporary array T of N numbers and set it to all ones, except for the first element.

Then row by row,

    use a temporary element TT and set it to 1,
    then column by column, assign simultaneously T[I-1], TT = TT, (TT + T[I-1] + T[I])/3.

【讨论】:

    【解决方案4】:

    为了描述时间复杂度,我们通常使用大 O 表示法。重要的是要记住,它只描述了给定输入的增长。 O(n) 是线性时间复杂度,但它没有说明当我们增加输入时时间增长的速度(或缓慢)。例如:

    n=3 -> 30 seconds
    n=4 -> 40 seconds
    n=5 -> 50 seconds
    

    这是O(n),我们可以清楚的看到n每增加一次,时间增加了10秒。

    n=3 -> 60 seconds
    n=4 -> 80 seconds
    n=5 -> 100 seconds
    

    这也是 O(n),尽管对于每 n 我们需要两倍的时间,并且每增加 n 增加 20 秒,时间复杂度线性增长。

    因此,如果您的时间复杂度为 O(n*n),并且您将执行的操作数量减半,您将得到 O(0.5*n*n) 等于 O(n*n) - 即您的时间复杂度不会改变。

    这是理论,在实践中操作的数量有时会有所不同。因为你有一个 n×n 的网格,你需要填充 n*n 个单元格,所以你可以达到的最佳时间复杂度是 O(n*n),但是你可以做一些优化:

    • 网格边缘的单元格可以在单独的循环中填充。目前,在大多数情况下,i 和 j 等于 0 有两个不必要的条件。
    • 您的网格有一条对称线,您可以利用它只计算一半,然后将结果复制到另一半。对于每个 i 和 j grid[i][j] = grid[j][i]

    最后一点,代码的清晰度和可读性比性能更重要——如果你能阅读和理解代码,你可以改变它,但如果代码丑到你看不懂,你就不能优化它。这就是为什么我只会做第一个优化(它也增加了可读性),但不会做第二个 - 这会使代码更难理解。

    根据经验,不要优化代码,除非性能确实导致问题。正如威廉·沃尔夫所说:

    以效率的名义(不一定实现)比任何其他单一原因(包括盲目愚蠢)犯下的计算罪更多。

    编辑:

    我认为有可能以 O(1) 的复杂度来实现这个功能。虽然当您需要填充整个网格时它没有任何好处,但在 O(1) 时间复杂度下,您可以立即获得任何值,而根本不需要网格。

    一些观察:

    • 分母等于3 ^ (i + j - 1)
    • 如果 i = 2 或 j = 2,分子比分母小一

    编辑 2:

    分子可以用以下函数表示:

    public static int n(int i, int j) {
        if (i == 1 || j == 1) {
            return 1;
        } else {
            return 3 * n(i - 1, j - 1) + n(i - 1, j) + n(i, j - 1);
        }
    }
    

    与原来的问题非常相似,但没有除法,所有数字都是整数。

    【讨论】:

    • 你上一次编辑错了,你可以减少加法的次数,但那样会增加乘法的次数。所以你至少需要做Ω(n) 算术运算。为每个可能的n 预先计算因子是不可能的。
    • 我认为最后的一点是解决此类问题的正确方法。看问题,找出规律。
    • 另外,如果你必须填满整个网格,实际上用幼稚的方法会更有效,因为单个计算会更简单。但问题特别是关于对角线项。
    【解决方案5】:

    该算法的最小复杂度为Ω(n),因为您只需要将矩阵的第一列和第一行中的值乘以一些因子,然后将它们相加即可。这些因素源于展开递归n 次。

    但是,您因此需要展开递归。它本身的复杂度为O(n^2)。但是通过平衡展开和递归评估,您应该能够将复杂性降低到O(n^x),其中1 &lt;= x &lt;= 2。这有点类似于矩阵-矩阵乘法的算法,其中朴素情况的复杂度为 O(n^3),但 Strassens 的算法例如是 O(n^2.807)

    另一点是原始公式使用了1/3 的因子。由于这不能用定点数或 ieee 754 浮点数准确表示,因此在连续评估递归时误差会增加。因此,展开递归可以为您带来更高的准确性,这是一个很好的副作用。

    例如,当您展开递归 sqr(n) 次时,您将拥有复杂性 O((sqr(n))^2+(n/sqr(n))^2)。第一部分用于展开,第二部分用于评估大小为n/sqr(n) 的新矩阵。这种新的复杂性实际上可以简化为 O(n)

    【讨论】:

      【解决方案6】:

      首先要记住以下几点:

      这种情况只能出现一次,但您每次循环都对其进行测试。

      if (x == 0 && y == 0) {
          matrix[x][y] = 0;
      }
      

      您应该改为:matrix[0][0] = 0; 在您进入第一个循环并将 x 设置为 1 之前。由于您知道 x 永远不会为 0,您可以删除第二个条件的第一部分 x == 0

      for(int x = 1; x <= i; x++)
              {
                  for(int y = 0; y <= j; y++)
                  {             
                      if (y == 0) {
                          matrix[x][y] = 1;
                      }
                      else
                      matrix[x][y] = (matrix[x-1][y] + matrix[x-1][y-1] + matrix[x][y-1])/3;
                  }
              }
      

      声明行和列没有意义,因为您只使用一次。 double[][] matrix = new double[i+1][j+1];

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-12-13
        • 2013-01-26
        • 2021-12-31
        • 2020-09-06
        • 1970-01-01
        相关资源
        最近更新 更多