如何通过分步分析确定递归回溯算法的时间和空间复杂度 步骤 1:使问题准确第 2 步:注释和剥离代码第三步:写出递推关系第四步:求解递推关系步骤 5:结果更新

如何解决如何通过分步分析确定递归回溯算法的时间和空间复杂度 步骤 1:使问题准确第 2 步:注释和剥离代码第三步:写出递推关系第四步:求解递推关系步骤 5:结果更新

背景信息:我使用下面的 C# 算法解决了 N-Queens 问题,该算法返回给定大小为 n x n 的棋盘的解决方案总数。它有效,但我不明白为什么这会是 O(n!) 时间复杂度,或者它是否是不同的时间复杂度。我也不确定递归堆栈中使用的空间(但我知道布尔锯齿状数组中使用的额外空间)。我似乎无法理解这些解决方案的时间和空间复杂性。拥有这种理解在技术面试中特别有用,对于没有运行代码能力的复杂性分析。

初步调查:我读过几篇 SO 帖子,其中作者直接要求社区提供他们算法的时间和空间复杂度。与其做同样的事情并寻求快速简单的答案,我想了解如何计算回溯算法的时间和空间复杂度,以便我可以继续前进。

我还在 SO 内外的许多地方读到过,一般来说,递归回溯算法的时间复杂度为 O(n!),因为在 n 次迭代中的每一次,您都会少看一个项目:n,然后是 n - 1,then 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,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;
    }
}

解决方法

首先,递归回溯算法都在O(n!)中绝对不是真的:当然这取决于算法,而且很可能会更糟。话虽如此,一般的方法是写下时间复杂度 T(n) 的递推关系,然后尝试求解它或至少刻画它的渐近行为。

步骤 1:使问题准确

我们对最坏情况、最好情况还是平均情况感兴趣?输入参数是什么?

在这个例子中,让我们假设我们要分析最坏情况的行为,并且相关的输入参数是 n 方法中的 Solve

在递归算法中,找到一个从输入参数的值开始然后随着每次递归调用而减小直到达到基本情况的参数是有用的(尽管并非总是可行)。

在这个例子中,我们可以定义 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,row + 1)
     = k T(k-1) + O(k)

第四步:求解递推关系

对于这一步,了解一些递推关系的一般形式及其解决方案是很有用的。上面的关系是一般形式

T(n) = n T(n-1) + f(n)

精确解决方案

T(n) = n!(T(0) + Sum { f(i)/i!,for i = 1..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 中的任意多项式(因为它会变成这真的没关系)。使用 ratio test:

很容易看出级数收敛
L = lim { | (f(n+1)/(n+1)!) / (f(n)/n!) |,for n -> infinity }
  = lim { | f(n+1) / (f(n)(n+1)) |,for n -> infinity }
  = 0  // if f is a polynomial
  < 1,and hence the series converges

因此,对于n -> infinity

T(n) -> n!(T(0) + Sum { f(i)/i!,for i = 1..infinity })
      = T(0) n!,if f is a polynomial

步骤 5:结果

既然T(n)的极限是T(0) n!,我们可以写

T(n) ∈ Θ(n!)

这是算法最坏情况复杂度的严格界限。

此外,我们已经证明,除了递归调用之外,您在 for 循环中做了多少工作并不重要,只要它是多项式,复杂度就保持 Θ(n!) (对于这种形式的递推关系)。(粗体是因为有很多 SO 答案都弄错了。)

对于具有不同递推关系形式的类似分析,请参阅here


更新

我在代码的注释中犯了一个错误(我会留下它,因为它仍然具有指导意义)。实际上,循环和循环内完成的工作都不依赖于 k = n - row 而是依赖于 initialn(让我们称其为 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!)差一点。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-