算法题总结(十三)——回溯算法(下)
子集问题
78、子集问题
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:
从图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。
每次递归都需要吧path加入结果集中。
如果要写终止条件,注意:result.add(new ArrayList<>(path));要放在终止条件的上面,如下:
class Solution {
List<List<Integer>> result =new ArrayList<>();
List<Integer> path =new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums,0);
return result;
}
private void backtrack(int[] nums,int index)
{
result.add(new ArrayList<>(path));
//把树的所有结点都记录下来,就是整个子集
if(index>=nums.length)
return; //终止条件也可以不加
for(int i=index;i<nums.length;i++)
{
path.add(nums[i]);
backtrack(nums,i+1);
path.removeLast();
}
}
}
90、子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
因为包含重复元素,所以要排序+去重。
去重的时候一定要排序!
class Solution {
List<List<Integer>> result =new ArrayList<>();
LinkedList<Integer> path =new LinkedList<>();
private void backtrack(int[] nums,int index)
{
result.add(new ArrayList<>(path)); //第一次调用的时候是把空的加入
if(index >= nums.length)
return; //终止条件 也可以不加
for(int i=index;i<nums.length;i++)
{
//去重,i>index保证是同一层的去重,而i=index是同一个树枝的
if(i>index && nums[i-1]==nums[i])
continue;
path.add(nums[i]);
backtrack(nums,i+1);
path.removeLast();
}
}
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); //排序
backtrack(nums,0);
return result;
}
}
使用set去重,也要进行排序,否则也会重复:
class Solution {
List<List<Integer>> reslut = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtracking(nums,0);
return reslut;
}
public void backtracking(int[] nums,int startIndex){
reslut.add(new ArrayList<>(path));
if(startIndex >= nums.length)
return;
HashSet<Integer> hashSet = new HashSet<>(); //同一层使用的是同一个hashset
for(int i = startIndex; i < nums.length; i++){
if(hashSet.contains(nums[i])){
continue;
}
hashSet.add(nums[i]);
path.add(nums[i]);
backtracking(nums,i+1);
path.removeLast();
}
}
}
491、 非递减子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1]
输出:[[4,4]]
由于含有重复的元素,因此需要进行去重,在之前的子集问题中,是通过排序后进行去重的,但本题是要求递增的子序列,因此不能对原数组进行排序,如果排序那么数组都是自增的了。所以不能使用之前的去重逻辑。
所以本题需要判断有两处,一个是保证递增,一个是同一层是否重复使用。
保证递增:把每个插入的元素和末尾的相比较,相当于尾插法。
使用哈希表来去重
由于不能排序,所以只能使用hash来进行去重,不能使用used数组,或者使用数组来代替hash表。
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex){
if(path.size() >= 2) //只要长度大于2就要放入结果中
{
result.add(new ArrayList<>(path));
return ;
}
HashSet<Integer> set = new HashSet<>(); //每一层使用同一个hash表
for(int i = startIndex; i < nums.length; i++){
if(!path.isEmpty() && path.get(path.size() -1 ) > nums[i] || hs.contains(nums[i])) //如果不是递增或者同一层出现过 那就continue
continue;
set.add(nums[i]);
path.add(nums[i]);
backTracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
排列问题
46、全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 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]]
处理排列问题就不用使用startIndex了
解法一:通过判断path中是否存在数字,排除已经选择的数字
因为题目说明了不含重复数字,所以可以使用这种方式来进行跳过选过的元素。
class Solution {
List<List<Integer>> result =new ArrayList<>();
List<Integer> path =new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
backtrack(nums);
return result;
}
private void backtrack(int [] nums)
{
if(path.size()==nums.length)
{
result.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++) //排列问题,每次要从0开始,所以要跳过已选取过的元素
{
if(path.contains(nums[i]))
continue;
path.add(nums[i]);
backtrack(nums);
path.removeLast();
}
}
}
解法二:使用used数组来记录数字的使用情况
使用used数组的方式比较好
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
used = new boolean[nums.length];
permuteHelper(nums);
return result;
}
private void permuteHelper(int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
if (used[i]){
continue; //防止同一树枝选取同一个元素,uesd为true,同一树枝使用过
}
used[i] = true;
path.add(nums[i]);
permuteHelper(nums);
path.removeLast();
used[i] = false;
}
}
}
47、全排列二
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 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]]
这道题目和46.全排列 (opens new window)的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列。
这里又涉及到去重了。
在40.组合总和II (opens new window)、90.子集II (opens new window)我们分别详细讲解了组合问题和子集问题如何去重。
那么排列问题其实也是一样的套路。
还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重用used[i - 1] == true。
对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
来来来,我就用输入: [1,1,1] 来举一个例子。
树层上去重(used[i - 1] == false),的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
树层去重效率更高!
//排序后使用used数组进行去重
//组合问题可以不使用used数组来去重,因为递归的时候下一个startIndex是i+1而不是0,我们可以使用i与index的关系来判断是同一层还是同一个树枝上的,但是排列问题是从0开始的,所以不能用这种方式。
//如果要是全排列的话,每次要从0开始遍历,为了跳过已入栈的元素,需要使用used
class Solution {
List<List<Integer>> result =new ArrayList<>();
List<Integer> path =new LinkedList<>();
boolean[] used; //used定义为全局变量,就不用传入到backtrack中
public List<List<Integer>> permuteUnique(int[] nums) {
used =new boolean[nums.length];
//判断是否使用过,具体来说是判断同一个树枝上是否使用过
Arrays.fill(used,false);
Arrays.sort(nums);
backtrack(nums);
return result;
}
private void backtrack(int[] nums)
{
if(path.size()==nums.length)
{
result.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++)
{
if(i>0 && nums[i-1]==nums[i] && used[i-1]==false)
continue; //同一层去重,相同并且前一个使用过
//被选过的不能在选
if(used[i]==false) //说明同一个树枝上没有使用过,也可以使用上一题等于true就continue的方式
{
used[i]=true;
path.add(nums[i]);
backtrack(nums);
path.removeLast();
used[i]=false;
}
}
}
}
也可以使用set来进行层去重,要注意set定义的位置
使用hash来进行同一层去重
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
private boolean[] used = null;
public List<List<Integer>> permuteUnique(int[] nums) {
used = new boolean[nums.length];
Arrays.sort(nums);
backtracking(nums);
return res;
}
public void backtracking(int[] nums) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
HashSet<Integer> hashSet = new HashSet<>();//层去重,这里的每一层都是一个新的hashset
for (int i = 0; i < nums.length; i++) {
if (hashSet.contains(nums[i])) //树层去重,同一层有重复元素了就跳过
continue;
if (used[i] == true)//枝去重,防止同一个树枝使用同一个元素
continue;
hashSet.add(nums[i]);//记录元素
used[i] = true;
path.add(nums[i]);
backtracking(nums);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
去重问题
如果包含重复元素了就需要进行去重,比如组合总和二,子集二,全排列二等。
我们可以使用used数组和set来进行排序后去重,但是使用set去重的版本相对于used数组的版本效率都要低很多而使用used数组在时间复杂度上几乎没有额外负担!
使用set去重,不仅时间复杂度高了,空间复杂度也高了,因为每一层递归都有一个set集合,而used数组是可以定义为全局变量共用的
所以使用used数组的方式就好了!
重新安排行程
332、重新安排行程
给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
- 例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出:["JFK","MUC","LHR","SFO","SJC"]
因为是只要找到一条路径就可以了,所以使用boolean返回值
之前说过,有递归的地方就有回溯,深度优先搜索也是用递归来实现的,所以往往伴随着回溯。
在回溯算法:重新安排行程 (opens new window)其实也算是图论里深搜的题目,但是我用回溯法的套路来讲解这道题目,算是给大家拓展一下思路,原来回溯法还可以这么玩!
和排序问题类似!
就是先按字典序排序,然后根据终点来进行判断,直到找完路径。
class Solution {
private LinkedList<String> res; //用一个全局变量记录,就不用返回了
private LinkedList<String> path = new LinkedList<>();
public List<String> findItinerary(List<List<String>> tickets) {
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));//先排序
path.add("JFK");
boolean[] used = new boolean[tickets.size()]; //也可以定义为一个全局变量
backTracking((ArrayList) tickets, used);
return res;
}
public boolean backTracking(ArrayList<List<String>> tickets, boolean[] used) {
if (path.size() == tickets.size() + 1) {
res = new LinkedList(path);
return true;
}
for (int i = 0; i < tickets.size(); i++) {
//
if (!used[i] && tickets.get(i).get(0).equals(path.getLast())) {
path.add(tickets.get(i).get(1));
used[i] = true;
if (backTracking(tickets, used)) {
return true;
}
used[i] = false;
path.removeLast();
}
}
return false;
}
}
棋盘问题
51、N皇后问题
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
回溯法解决N皇后问题
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
这里我明确给出了棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了。
检查的时候只需要检查列,不需要检查行,因为在for循环中,每次只会选取一行中的一个位置。
for循环就是棋盘的宽度,递归的深度就是棋盘的高度
class Solution {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board =new char[n][n]; //先定义一个二维数组代表棋盘,方便操作
for(char[] c : board)
{
Arrays.fill(c,'.');
}
backtrack(n,0,board);
return result;
}
private void backtrack(int n,int row,char[][] board) //从第几行开始递归!
{
if(row==n)
{
List<String> list =new ArrayList<>(); //存放一个棋盘的结果
for(char[] c:board)
{
list.add(new String(c)); //和sb一样,直接使用new String()
}
result.add(list);
return;
}
for(int col=0;col<n;col++) //for循环就是棋盘的宽度,递归的深度就是棋盘的高度
{
if(valid(row,col,n,board))
{
board[row][col]='Q';
backtrack(n,row+1,board);
board[row][col]='.';
}
}
}
private boolean valid(int row,int col,int n,char[][] board)
{
//检查前row行,也就是检查同一列是否有相同的元素!
for(int i=0;i<row;i++) //因为是一行一行填入的,所以只需要检查前row行
{
if(board[i][col]=='Q')
return false;
}
//检查左上对角线
for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--)
{
if(board[i][j]=='Q')
return false;
}
//右上角对角线
for(int i=row-1,j=col+1;i>=0 && j<=n-1;i--,j++)
{
if(board[i][j]=='Q')
return false;
}
return true;
}
}
37、解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例 1:
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,所以返回值是bool类型。
填满了就终止了,所以不需要终止条件。
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
一个二维循环遍历找到要填入数字的位置,然后一个一维循环,尝试从1-9填入数字看是否合适。
class Solution {
public void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
private boolean solveSudokuHelper(char[][] board){
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < 9; i++){ // 遍历行
for (int j = 0; j < 9; j++){ // 遍历列
if (board[i][j] != '.'){ // 跳过原始数字
continue;
}
for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适
if (isValidSudoku(i, j, k, board)){
board[i][j] = k;
if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
return true;
}
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
private boolean isValidSudoku(int row, int col, char val, char[][] board){
// 同行是否重复
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
}
其他问题
22、括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
- 1 <= n <= 8
使用递归:
如果左括号数量不大于 nnn,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。
class Solution {
List<String> result =new ArrayList<>();
StringBuilder path =new StringBuilder(); //存放每组括号
public List<String> generateParenthesis(int n) {
backtrack(0,0,n);
return result;
}
public void backtrack(int left,int right,int n){ //左右括号的个数
if(path.length()==2*n)
{
result.add(new String(path));
return;
}
if(left<n){j
path.append('(');
backtrack(left+1,right,n);
path.deleteCharAt(path.length()-1);
}
if(right<left)
{
path.append(')');
backtrack(left,right+1,n);
path.deleteCharAt(path.length()-1);
}
}
}
79、单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false
class Solution {
boolean[][] visited; //标记每个位置是否被访问过,防止在一次递归中,一个位置被重复访问
int[][] dir={{0,1},{0,-1},{1,0},{-1,0}};
public boolean exist(char[][] board, String word) {
int h=board.length;
int w=board[0].length;
visited=new boolean[h][w];
for(int i=0;i<h;i++)
{
for(int j=0;j<w;j++)
{
if(backtrack(board,i,j,word,0))
return true;
}
}
return false;
}
//从word的第k个字母开始递归
//只判断能不能找到,所以有boolean返回值
public boolean backtrack(char[][] board,int i,int j,String word,int k)
{
if(word.charAt(k)!=board[i][j])
return false;
else if(k==word.length()-1)
return true;
//说明匹配
visited[i][j]=true;
//四个方向递归
for(int t=0;t<4;t++)
{
int newi=i+dir[t][0];
int newj=j+dir[t][1];
if(newi>=0 && newi<board.length && newj>=0 && newj<board[0].length)
{
if(visited[newi][newj]==false)
{
if(backtrack(board,newi,newj,word,k+1))
return true; //如果找到了,直接返回true
}
}
}
visited[i][j]=false; //没找到符合条件的,回溯,把visited置为false
return false;
}
}
总结
原文地址:https://blog.csdn.net/maruofan666/article/details/142766551
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!