Time complexity 如何通过逐步分析确定递归回溯算法的时间和空间复杂度
背景信息:我用下面的C#算法解决了N皇后问题,该算法返回给定大小为N x N的电路板的总解决方案数。这是可行的,但我不明白为什么这会是O(n!)时间复杂性,或者它是不同的时间复杂性。我也不确定递归堆栈中使用的空间(但知道布尔交错数组中使用的额外空间)。我似乎无法理解这些解决方案的时间和空间复杂性。了解这一点在技术面试中尤其有用,因为在没有运行代码能力的情况下,复杂度分析非常有用 初步调查:我读过几篇SO帖子,作者直接要求社区提供他们算法的时间和空间复杂性。我想了解如何计算回溯算法的时间和空间复杂度,这样我就可以继续前进 我也读过很多关于内部和外部的文章,所以一般来说,递归回溯算法的时间复杂度是O(n!),因为在n次迭代中的每一次,你都会少看一项:n,然后n-1,然后n-2。。。1.然而,我没有找到任何解释,说明为什么会出现这种情况。我也没有找到任何解释来解释这种算法的空间复杂性 问题:有人能解释一下逐步解决问题的方法,以确定递归回溯算法的时间和空间复杂性吗Time complexity 如何通过逐步分析确定递归回溯算法的时间和空间复杂度,time-complexity,backtracking,space-complexity,n-queens,recursive-backtracking,Time Complexity,Backtracking,Space Complexity,N Queens,Recursive Backtracking,背景信息:我用下面的C#算法解决了N皇后问题,该算法返回给定大小为N x N的电路板的总解决方案数。这是可行的,但我不明白为什么这会是O(n!)时间复杂性,或者它是不同的时间复杂性。我也不确定递归堆栈中使用的空间(但知道布尔交错数组中使用的额外空间)。我似乎无法理解这些解决方案的时间和空间复杂性。了解这一点在技术面试中尤其有用,因为在没有运行代码能力的情况下,复杂度分析非常有用 初步调查:我读过几篇SO帖子,作者直接要求社区提供他们算法的时间和空间复杂性。我想了解如何计算回溯算法的时间和空间复杂
public class Solution {
public int NumWays { get; set; }
public int TotalNQueens(int n) {
if (n <= 0)
{
return 0;
}
NumWays = 0;
bool[][] board = new bool[n][];
for (int i = 0; i < board.Length; i++)
{
board[i] = new bool[n];
}
Solve(n, board, 0);
return NumWays;
}
private void Solve(int n, bool[][] board, int row)
{
if (row == n)
{
// Terminate since we've hit the bottom of the board
NumWays++;
return;
}
for (int col = 0; col < n; col++)
{
if (CanPlaceQueen(board, row, col))
{
board[row][col] = true; // Place queen
Solve(n, board, row + 1);
board[row][col] = false; // Remove queen
}
}
}
private bool CanPlaceQueen(bool[][] board, int row, int col)
{
// We only need to check diagonal-up-left, diagonal-up-right, and straight up.
// this is because we should not have a queen in a later row anywhere, and we should not have a queen in the same row
for (int i = 1; i <= row; i++)
{
if (row - i >= 0 && board[row - i][col]) return false;
if (col - i >= 0 && board[row - i][col - i]) return false;
if (col + i < board[0].Length && board[row - i][col + i]) return false;
}
return true;
}
}
公共类解决方案{
public int NumWays{get;set;}
公共整数总计(整数n){
如果(n首先,递归回溯算法都在O(n!)
中肯定不是真的:当然这取决于算法,而且可能更糟。尽管如此,一般的方法是为时间复杂度T(n)写一个递归关系
,然后尝试求解它或至少描述它的渐近行为
第一步:使问题精确
我们对最坏情况、最佳情况或平均情况感兴趣吗?输入参数是什么
在本例中,假设我们要分析最坏情况的行为,并且相关的输入参数是Solve
方法中的n
在递归算法中,找到一个以输入参数的值开始,然后随着每次递归调用而减小,直到到达基本情况的参数是有用的(尽管并非总是可能的)
在本例中,我们可以定义k=n-row
。因此,对于每个递归调用,k
从n
开始递减到0
第2步:注释并删除代码
不,我们查看代码,将其拆分为相关的位,并对其进行复杂的注释
我们可以将您的示例归结为以下几点:
private void Solve(int n, bool[][] board, int row)
{
if (row == n) // base case
{
[...] // O(1)
return;
}
for (...) // loop n times
{
if (CanPlaceQueen(board, row, col)) // O(k)
{
[...] // O(1)
Solve(n, board, row + 1); // recurse on k - 1 = n - (row + 1)
[...] // O(1)
}
}
}
第三步:写下循环关系
此示例的重复关系可以直接从代码中读取:
T(0) = 1 // base case
T(k) = k * // loop n times
(O(k) + // if (CanPlaceQueen(...))
T(k-1)) // Solve(n, board, row + 1)
= k T(k-1) + O(k)
第四步:求解递推关系
对于这一步,了解一些递归关系的一般形式及其解是很有用的。上面的关系是一般形式的
T(n) = n T(n-1) + f(n)
哪个有精确解
我们可以通过归纳很容易地证明:
T(n) = n T(n-1) + f(n) // by def.
= n((n-1)!(T(0) + Sum { f(i)/i!, for i = 1..n-1 })) + f(n) // by ind. hypo.
= n!(T(0) + Sum { f(i)/i!, for i = 1..n-1 }) + f(n)/n!)
= n!(T(0) + Sum { f(i)/i!, for i = 1..n }) // qed
现在,我们不需要精确解;我们只需要当n
接近无穷大时的渐近行为
让我们看看无穷级数
Sum { f(i)/i!, for i = 1..infinity }
在我们的例子中,f(n)=O(n)
,但让我们看看更一般的情况,f(n)
是n
中的一个任意多项式(因为它会证明它真的不重要)。很容易看出级数收敛,使用:
第五步:结果
由于T(n)
的极限是T(0)n!
,我们可以编写
T(n) ∈ Θ(n!)
这是对算法最坏情况复杂性的严格限制
此外,我们已经证明,除了递归调用之外,在for循环中做多少工作都无关紧要,只要它是多项式,复杂性就保持在Θ(n!)
(对于这种形式的递归关系)。(粗体,因为有很多这样的答案是错误的。)
有关使用不同形式的递归关系的类似分析,请参见
更新
我在代码的注释中犯了一个错误(我将保留它,因为它仍然具有指导意义)。实际上,循环和循环中完成的功都不依赖于k=n-row
,而是依赖于初始值n
(为了清楚起见,我们称之为n0
)
因此,递归关系变为
T(k) = n0 T(k-1) + n0
其精确解为
T(k) = n0^k (T(0) + Sum { n0^(1-i), for i = 1..k })
但从最初的n0=k
,我们
T(k) = k^k (T(0) + Sum { n0^(1-i), for i = 1..k })
∈ Θ(k^k)
这比Θ(k!)更糟糕
我投票将这个问题作为离题题结束,因为它属于主题。人们可能会争论这个问题,但你可能会在那里得到更好的答案。即使你介绍了这样一个算法的实现,我投票的原因仍然是:理论与实践。例如:…这一切归结为识别proper exit-criteria.谢谢,Mo B.这是一个非常有用的解释。虽然我不确定技术面试官要求应聘者证明时间复杂性的程度,但这为确定总体时间复杂性提供了一个很好的框架。那么空间复杂性呢?@BlueTriangles空间复杂性通常更直截了当d、 您是否返回任何需要存储的数据?(在您的情况下,否)您是否有任何大小动态增长的数据?(在您的情况下,否)您是否有任何需要存储在堆栈上的递归调用?(在您的情况下,是:自ea以来)
T(k) = n0 T(k-1) + n0
T(k) = n0^k (T(0) + Sum { n0^(1-i), for i = 1..k })
T(k) = k^k (T(0) + Sum { n0^(1-i), for i = 1..k })
∈ Θ(k^k)