【算法】回溯法四步走 (16)

将元素一个一个填入排列结果中,如果元素填过了,那就做个记号,下次不填了,填其他的。

import java.util.ArrayList; import java.util.List; class Solution { /* 本题采用回溯法 */ public List<List<Integer>> permute(int[] nums) { // 使用lists来存放全排列结果 List<List<Integer>> lists = new ArrayList<>(); // 使用list来存放一次排列的结果 List<Integer> list = new ArrayList<>(); function(nums, lists, list, 1); return lists; } /* count为排列中的数字个数 */ public void function(int[] nums, List<List<Integer>> lists, List<Integer> list, int count) { int temp = 0; // 临时变量,用来记录初始数据,方便还原现场 // 递归出口,当排列中的数字个数大于所有数字个数时,即 排列完成 if (count > nums.length) { lists.add(new ArrayList<Integer>(list)); return; } // 分支路径 for (int i = 0; i < nums.length; i++) { if (nums[i] == Integer.MAX_VALUE) { continue; } // 加入排列 list.add(nums[i]); // 存放原始数据,方便还原现场 temp = nums[i]; // 已经排过的元素标为Integer.MAX_VALUE nums[i] = Integer.MAX_VALUE; // 递归函数,其前后操作对称 function(nums, lists, list, count + 1); // 还原现场 nums[i] = temp; // 与标为Integer.MAX_VALUE对称 list.remove(list.indexOf(nums[i])); // 与add加入排列对称 } } } 答案

从全排列问题开始理解回溯算法
我们尝试在纸上写 3 个数字、4 个数字、5 个数字的全排列,相信不难找到这样的方法。以数组 [1, 2, 3] 的全排列为例。

先写以 1 开头的全排列,它们是:[1, 2, 3], [1, 3, 2],即 1 + [2, 3] 的全排列(注意:递归结构体现在这里);

再写以 2 开头的全排列,它们是:[2, 1, 3], [2, 3, 1],即 2 + [1, 3] 的全排列;

最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1],即 3 + [1, 2] 的全排列。

总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示。

看到这里的朋友,建议先尝试自己画出「全排列」问题的树形结构。

image

说明:

每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为「状态」;

使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」;

深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;

深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果。

使用编程的方法得到全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列。

设计状态变量

首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即:在已经选择了一些数的前提下,在剩下的还没有选择的数中,依次选择一个数,这显然是一个 递归 结构;

递归的终止条件是: 一个排列中的数字已经选够了 ,因此我们需要一个变量来表示当前程序递归到第几层,我们把这个变量叫做 depth,或者命名为 index ,表示当前要确定的是某个全排列中下标为 index 的那个数是多少;

布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 \(O(1)\) 的时间复杂度判断这个数是否被选择过,这是一种「以空间换时间」的思想。

这些变量称为「状态变量」,它们表示了在求解一个问题的时候所处的阶段。需要根据问题的场景设计合适的状态变量。

思路和算法

这个问题可以看作有 n 个排列成一行的空格,我们需要从左往右依此填入题目给定的 n 个数,每个数只能使用一次。那么很直接的可以想到一种穷举的算法,即从左往右每一个位置都依此尝试填入一个数,看能不能填完这 n 个空格,在程序中我们可以用「回溯法」来模拟这个过程。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwwszw.html