Time complexity 如何通过逐步分析确定递归回溯算法的时间和空间复杂度

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帖子,作者直接要求社区提供他们算法的时间和空间复杂性。我想了解如何计算回溯算法的时间和空间复杂

背景信息:我用下面的C#算法解决了N皇后问题,该算法返回给定大小为N x N的电路板的总解决方案数。这是可行的,但我不明白为什么这会是O(n!)时间复杂性,或者它是不同的时间复杂性。我也不确定递归堆栈中使用的空间(但知道布尔交错数组中使用的额外空间)。我似乎无法理解这些解决方案的时间和空间复杂性。了解这一点在技术面试中尤其有用,因为在没有运行代码能力的情况下,复杂度分析非常有用

初步调查:我读过几篇SO帖子,作者直接要求社区提供他们算法的时间和空间复杂性。我想了解如何计算回溯算法的时间和空间复杂度,这样我就可以继续前进

我也读过很多关于内部和外部的文章,所以一般来说,递归回溯算法的时间复杂度是O(n!),因为在n次迭代中的每一次,你都会少看一项:n,然后n-1,然后n-2。。。1.然而,我没有找到任何解释,说明为什么会出现这种情况。我也没有找到任何解释来解释这种算法的空间复杂性

问题:有人能解释一下逐步解决问题的方法,以确定递归回溯算法的时间和空间复杂性吗

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)