回溯法排序树怎么画_回溯,入门

本文介绍了回溯法的概念,强调它是深度优先搜索(DFS)的一个应用场景,具有回溯和撤销选择的特点。通过全排列问题为例,讲解了如何使用递归和回溯法解决此类问题,并提供了代码模板。同时,提到了剪枝操作在优化回溯法效率中的重要性,以避免指数级别的计算复杂度。此外,还分享了相关题目和解题思路,帮助读者深入理解和实践回溯法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说回溯之前,得先知道dfs,说dfs得知道什么是搜索算法。
最常见,常用的搜素算法:

  • 广度优先搜索(bfs:Breadth First Search)
  • 深度优先搜索(dfs: Depth First Search)

这两种算法在经典的N叉树遍历算法上有最熟悉不过的应用。
比如使用队列层次遍历二叉树就基于bfs,递归方式的前中后序遍历就基于dfs。
当然dfs和bfs有除此之外更加复杂的应用场景。

对于回溯,我的理解就是基于dfs,深度优先搜索上的一种应用场景。
它的重要特点,也就是它的名字,回溯,回溯在搜索到某一层的某一分支之后,如果不满足条件后,就回退,撤销选择,尝试别的分支,直到找到符合结果。

回溯具体代码实现,主要通过递归来实现,因回溯的特点,其代码大部分有极高的对称性。
深度搜索的话,其实就相当于把所有可能的分支都要走一遍,所以朴素回溯,其时间复杂度一般是指数级别的,因为回退尝试,产生很多中间重复计算。这个时候,就需要合理的剪枝操作,来优化提高程序运行速度。但是剪枝一般不会影响时间复杂的大的量级。

回溯的关键字:回退,撤销选择,递归,dfs,剪枝。

分析回溯类题目,画出递归状态树就行,然后分析如何回退,如何剪枝。时间复杂度计算就分析总的递归的时间

回溯的代码模板:

public void dfs(当前路径, 当前层级,选择列表,结果集) {
    // 1. 终止条件
    if(符合条件) {
        当前路径加入结果集
        return;
    }
    // 2. 处理当前层
    cur.add(...);
    // 3. 下探,递归
    dfs(cur,level + 1, 选择列表,结果集);
    // 4. 回退,撤销选择
    cur.remove(...);
}

分享两篇我认为比较好的回溯文章:
https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/

https://leetcode-cn.com/problems/subsets/solution/hui-su-si-xiang-tuan-mie-pai-lie-zu-he-zi-ji-wen-t/

回溯的相关题目清单。

  1. 全排列(中等)   高频
  2. 全排列 II(中等) 高频
  3. 组合总和(中等)  高频
  4. 组合总和 II(中等) 高频
  5. 组合(中等) 高频
  6. 子集(中等) 高频
  7. 子集 II(中等)高频
  8. 第 k 个排列(中等)
  9. 复原 IP 地址(中等)
  10. 图像渲染(Flood Fill,中等)
  11. 岛屿数量(中等)
  12. 被围绕的区域(中等)
  13. 单词搜索(中等) 高频
  14. 电话号码的字母组合(中等) 高频
  15. 字母大小写全排列(中等) 高频
  16. 括号生成(中等) 高频
  17. N 皇后(困难
  18. 祖玛游戏(困难)
  19. 扫雷游戏(困难)

那么就从最基本的全排列题目(leetCode 46),开始,好好理解一下回溯吧。

题目描述:

给定一个 没有重复数字的序列,返回其所有可能的全排列。示例:
输入:[1,2,3]
输出: [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]

解题思路:

注意,题目已经约束了,待排列的数组没有重复元素。

如果没有思路,刚开始不太熟练的话,可以手画一下递归树,便于理解。后面写的多了,相信心中自有递归树。

盗图一波,图片来源上面的推荐贴。

8b1e34ffef21ad61a017243d16fca9ed.png

树的特定就是,每条路径中,已经选择过的元素,就不能再选择。我们可以维护一个布尔数组 visited 来表示 数组中的元素是否已经被访问。并把这个变量带到每一层的递归变量中去。

接下来,套用回溯模板。

dfs(boolean[] visited, List cur, int[] nums, List res) {
    1. terminal 终止条件: 当前路径的大小=排列数组的长度,就可以加入到结果集,然后返回backif (cur.size() == nums.length) {
        res.add(new ArrayList(cur));return;
    }
    nums长度的位置上,可以是任何一个数。所以对应递归层数就是nums的长度。for(int i = 0; i         // 剪枝,每条路径中,不能选择已选择过的元素if(visited[i] == true) {continue;
        }
        2. 处理当前层: 添加到当前路径,并标价已选择
        cur.add(nums[i]);
        visited[i] = true;
        3. drill down, 处理下一层
        dfs(visted, cur, nums, res);
        4. 回退,撤销选择,还原上一层的所有状态。
        visited[i] = false;
        cur.remove(cur.size() - 1);
    }
}

刚开始,很吃力,很正常,因为人脑可能理解递归比较吃力,但是这对计算机来说很容易。所以需要悟!做的多了,就悟了!
感觉像不像盗梦空间,每进入一层梦境,带着的那个陀螺,就像这里的变量,每一层的东西应该是不一样的,应该是副本。如果这块不是副本的话,就需要类似第4步的撤销处理,还原上一层的梦境!

写一遍完整的代码:

public List> permute(int[] nums) {
    List> res = new ArrayList<>();if (nums == null || nums.length <= 0) {return res;
    }
    boolean[] visited = new boolean[nums.length];
    dfs(nums, res, new ArrayList<>(), visited);return res;
}
private void dfs(int[] nums, List> res, List cur, boolean[] visited) {if (cur.size() == nums.length) {
        res.add(new ArrayList<>(cur));return;
    }for (int i = 0; i         if (visited[i]) {continue;
        }
        cur.add(nums[i]);
        visited[i] = true;
        dfs(nums, res, cur, visited);
        cur.remove(cur.size() - 1);
        visited[i] = false;
    }
}

接下来看全排列的进阶。全排列2(leetCode:47)

题目描述:

给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:[1,1,2],[1,2,1],[2,1,1]

解题思路:

这道题相比上面的,多了个条件:待排列的数组中会存在重复的数字。但是要返回所有不重复的全排列。
如果按照全排列1的解法,首先第二层会把1的路径再走一遍。这是不合适的,所以得想办法去剪枝。
大多数情况下,第一次接触题目,很难想出如何剪枝,剪枝是个技巧也是个经验。
此时画出状态树,在图上就能很更轻松的找出剪枝思路。

又盗图一波,图来源优秀题解:
https://leetcode-cn.com/problems/permutations-ii/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liwe-2/

ddc3f2dce596af05b4b5d4da111a1a80.png

图已经很清楚的标注了,应该剪除哪些分支。怎么对应到题解中呢?

还记得 N数之和,全歼灭 中那几道题吗?里面排序已经移动重复元素的手段完全可以学习借鉴。

1f6534df19e2223e6f025716cc7d1b81.png

是不是我们对 待排列的数组 排序后,保证当前层处理中的元素不等于前一个元素即可。

先上代码!

public List> permuteUnique(int[] nums) {
    List> res = new ArrayList<>();if (nums == null || nums.length <= 0) {return res;
    }
    Arrays.sort(nums);
    boolean[] visited = new boolean[nums.length];
    dfs(nums, res, new ArrayList(), visited);return res;
}
private void dfs(int[] nums, List> res, List cur, boolean[] visited) {if (cur.size() == nums.length) {
        res.add(new ArrayList<>(cur));return;
    }for (int i = 0; i         if (visited[i]) {continue;
        }if (i > 0 && nums[i-1] == nums[i] && !visited[i - 1]) {continue;
        }
        visited[i] = true;
        cur.add(nums[i]);
        dfs(nums, res, cur, visited);
        cur.remove(cur.size() - 1);
        visited[i] = false;
    }
}

你可能有疑惑,这个关键的剪枝条件 i > 0 && nums[i-1] == nums[i] && !visited[i - 1]  为什么还要i-1元素未被访问过呢?
这是因为同一层的i-1的元素因为回退操作,已经置为未访问了。
如果i-1的元素被访问过了,说明当前层在i-1的下一层,相等是不会重复的。
所以如果不加这个约束剪枝就错了。


本次通过最经典,最能直观表达回溯特点的全排列这两道题目,和大家入了个回溯的门。
如果通过这两道题悟了,趁热打铁,做下括号生成的题目吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值