回溯算法将排列问题一网打尽

139 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

回溯算法将排列问题一网打尽

1. 全排列

1.1 问题描述

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

1.2 要求

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

1.3 思路

以数组 [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.png****

说明:

  • 每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的 不同的值 体现,这些变量的不同的值,称之为状态
  • 使用深度优先遍历有 回头 的过程,在 回头 以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为 状态重置
  • 深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
  • 深度优先遍历通过 回溯 操作,实现了全局使用一份状态变量的效果。

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

1.4 代码

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
  const length = nums.length;
  const paths = [];
  const used = Array(length).fill(false);
​
  const backtracking = (path = []) => {
    
    if(path.length === length) {
      paths.push([...path]);
      return;
    }
​
    for(let i = 0; i < length; i++) {
      if (used[i]) continue;
      used[i] = true;
      path.push(nums[i]);
      backtracking(path);
      path.pop();
      used[i] = false;
    }
  }
  backtracking();
  return paths;
};

2. 全排列 II

2.1 问题描述

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

2.2 要求

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

2.3 思路

这一题在 全排列 的基础上增加了 序列中的元素可重复 这一条件,但要求:返回的结果又不能有重复元素。

重点在于:在遍历的过程中,一边遍历一遍检测,在一定会产生重复结果集的地方剪枝

一个比较容易想到的办法是在结果集中去重。但是问题来了,这些结果集的元素是一个又一个列表,对列表去重不像用哈希表对基本元素去重那样容易。

如果要比较两个列表是否一样,一个容易想到的办法是对列表分别排序,然后逐个比对。既然要排序,我们就可以 在搜索之前就对候选数组排序,一旦发现某个分支搜索下去可能搜索到重复的元素就停止搜索,这样结果集中不会包含重复列表。

画出树形结构如下:重点想象深度优先遍历在这棵树上执行的过程,哪些地方遍历下去一定会产生重复,这些地方的状态的特点是什么? 对比图中标注 ① 和 ② 的地方。相同点是:这一次搜索的起点和上一次搜索的起点一样。不同点是:

  • 标注 ① 的地方上一次搜索的相同的数刚刚被撤销;
  • 标注 ② 的地方上一次搜索的相同的数刚刚被使用。

image.png

产生重复结点的地方,正是图中标注了剪刀,且被绿色框框住的地方。

大家也可以把第 2 个 1 加上 ' ,即 [1, 1', 2] 去想象这个搜索的过程。只要遇到起点一样,就有可能产生重复。这里还有一个很细节的地方:

  • 在图中 ② 处,搜索的数也和上一次一样,但是上一次的 1 还在使用中;
  • 在图中 ① 处,搜索的数也和上一次一样,但是上一次的 1 刚刚被撤销,正是因为刚被撤销,下面的搜索中还会使用到,因此会产生重复,剪掉的就应该是这样的分支。

2.4 代码

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permuteUnique = function(nums) {
  const length = nums.length;
  const paths = [];
  const used = Array(length).fill(false);
  nums.sort((a, b) => a - b);
​
  const backtracking = (path = []) => {
    
    if(path.length === length) {
      paths.push([...path]);
      return;
    }
​
    for(let i = 0; i < length; i++) {
      if (used[i]) continue;
      // 小剪枝:同一层相同数值的结点,从第 2 个开始,结果一定发生重复,因此跳过
      if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue;
      used[i] = true;
      path.push(nums[i]);
      backtracking(path);
      path.pop();
      used[i] = false;
    }
  }
  backtracking();
  return paths;
};

3. 字母大小写全排列

3.1 问题描述

给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。

返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。

3.2 要求

示例 1:

输入:s = "a1b2"
输出:["a1b2", "a1B2", "A1b2", "A1B2"]

示例 2:

输入: s = "3z4"
输出: ["3z4","3Z4"]

3.3 思路

image-20220530144823455.png

3.4 代码

/**
 * @param {string} s
 * @return {string[]}
 */
var letterCasePermutation = function(s) {
  const strArr = s.split('');
  const paths = [];
  const indexList = strArr.reduce((pre, cur, curIndex) => {
    if (isNaN(cur)) pre.push(curIndex);
    return pre;
  }, []);
  const length = indexList.length;
​
  const backtracking = (startIndex, path = []) => {
    if (startIndex === length) {
      paths.push(path.join(''));
      return;
    }
​
    path[indexList[startIndex]] = path[indexList[startIndex]].toUpperCase();
    backtracking(startIndex + 1, path);
    path[indexList[startIndex]] = path[indexList[startIndex]].toLowerCase();
    backtracking(startIndex + 1, path);
  }
  
  backtracking(0, strArr);
  return paths;
};