解法一:回溯法
有关递归的算法,都离不开“树”的遍历这一抽象模型,只不过对于不同的算法,在前(中)后序遍历的时候,所做的事不同而已。
实际上回溯算法就是一个 N 叉树的前序遍历加上后序遍历而已。这个 N 等于当前可做的选择(choices)的总数,同时,在前序遍历的位置作出当前选择(choose 过程),然后开始递归,最后在后序遍历的位置撤销当前选择(unchoose 过程)。
// 二叉树遍历框架
def traverse(root):
if root is None: return
# 前序遍历代码写在这
traverse(root.left)
# 中序遍历代码写在这
traverse(root.right)
# 后序遍历代码写在这
// N 叉树遍历框架
def traverse(root):
if root is None: return
for child in root.children: # 多个孩子,遍历递归
# 前序遍历代码写在这
traverse(child)
# 后序遍历代码写在这
/*
回溯伪代码
choiceList:当前可以进行的选择列表
track:可以理解为决策路径,即已经做出一系列选择
res:用来储存答案,即符合条件的决策路径
*/
def backtrack(choiceList, track, res):
if track is OK:
res = append(res, track)
else:
for choice in choiceList:
# choose:选择一个 choice 加入 track
backtrack(choices, track, answer)
# unchoose:从 track 中撤销上面的选择,该选择又重新加回选择列表
每向下走一层,就是在「选择列表」中挑一个「选择」加入「决策路径」,然后把这个选择从「选择列表」中删除(以免之后重复选择);当一个决策分支探索完成后,我们就要向上回溯,要把该分支的「选择」从「决策列表」中取出,然后把这个「选择」重新加入「选择列表」(供其他的决策分支使用)。以上,就是模板中 choose 和 unchoose 的过程,choose 过程是向下探索,进行选择;unchoose 过程是向上回溯,撤销刚才的选择。
可以理解为,回溯算法相当于一个决策过程,递归地遍历一棵决策树,穷举所有的决策,同时把符合条件的决策挑出来。
写 backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。
回到本题,那么这棵决策树如下:
对照着写成代码
func permute(nums []int) [][]int {
var res [][]int
if len(nums) == 0{
return res
}
used := make([]bool, len(nums))
path := make([]int, 0, len(nums))
backtrack(nums, used, path, &res)
return res
}
// 由于切片不方便随机删除和恢复某个元素,这里借助一个标记数组
// 可选择列表choiceList即为输入nums数组中未被标记为used的元素
func backtrack(nums []int, used []bool, path []int, res *[][]int) {
if len(path) == len(nums){
tmp := make([]int, len(path))
copy(tmp, path)
*res = append(*res, tmp)
return
}
for idx, num := range nums{
if used[idx]{
continue
}
path = append(path, num)
used[idx] = true
backtrack(nums, used, path, res)
path = path[:len(path)-1]
used[idx] = false
}
}