[Leetcode] 回溯算法指南

前言

回溯是一种在搜索空间中对所有可能的排布进行迭代遍历的系统性方案。这些排布可能表示了若干个对象的所有安排(置换),或者构建若干元素所有可能的挑选方案(子集)。

问题形式

回溯类问题一般都可以用深度优先遍历来求解。
题目问法大致有这几种

  • 给一个字符串或者数组,求元素的排列、组合、子集
  • 给一个矩阵或者树,求可能的排列或两个点之间的路径集合
  • 给一个图,求所有的生成树
解题思路与模板

这些问题的共同点是我们必须让每个可能的排布恰好生成一次。要让排布不重不漏,则意味着必须定义一个具有条理性的生成次序。其本质是:走不通就回头。

/*参考模板*/
void backtracking(P node, vector<P> &path, vector<vector<P> >&paths){
	if(!node)  // invalid node
	    return;	
	path.push_back(node);
	
	bool success = ...;  // condition for success
	if (success)  
	    paths.push_back(vector<P>(path.begin(),path.end())); 
	    // don't return here
	
	for (P next: all directions)
	    backtracking(next, path, paths);
	path.pop_back();
	return;
}
具体问题
组合

给定两个整数 n n n k k k,返回 [ 1 , n ] [1,n] [1,n] 中所有可能的 k k k 个数的组合。

示例

输入:n = 4, k = 2
输出:[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4]]

解题思路在这里插入图片描述

我们仔细分析后可以发现,其中绿色的部分,其实是不能产生结果的分支。因为一旦已经选择到 [ 1 , 4 , 5 ] [1,4,5] [1,4,5] 或者 [ 2 , 4 , 5 ] [2,4,5] [2,4,5] 或者 [ 3 , 4 , 5 ] [3,4,5] [3,4,5] ,再进递归后,for 循环内也没有新的数字可用了,继续走也不能发现新的满足题意的组合。
如果我们在循环中每次都以 [ s t a r t , n ] [start,n] [start,n] 取数字的话,实际上却是会走到这一部分的。程序的运行情况形如:找到了 [ 1 , 4 , 5 ] [1,4,5] [1,4,5] 以后, 5 5 5 弹出成为 [ 1 , 4 ] [1,4] [1,4] 4 4 4 弹出成为 [ 1 ] [1] [1] ,然后 5 5 5 进来,成为 [ 1 , 5 ] [1,5] [1,5]。再进递归发现 for 循环都进不了(因为没有可选的元素),然后 5 5 5 又弹出,接着 1 1 1 弹出。
我们可以分析一下,比如, n = 5 n = 5 n=5 k = 3 k = 3 k=3 s u b . s i z e ( ) = = 1 sub.size( ) == 1 sub.size()==1,此时代表我们还需要 3 - 1 = 2 个数字,如果 i = 5 i = 5 i=5 的话,最多把 5 5 5 加入到 s u b sub sub 中,加入后 s u b sub sub 才有 2 个数字,不够 3 个。所以 i i i 没必要等于 5 5 5 i i i 循环到 4 4 4 就足够了。
所以 for 循环的结束条件可以改成, i <= n - (k - sub.size()) + 1k - sub.size() 代表我们还需要的数字个数。因为我们最后取到了 n n n,所以还要加 1。

class Solution {
public:
	vector<vector<int>> combine(int n, int k) {
		vector<vector<int>> result;
		vector<int> sub;
		combine(n, k, 1, sub, result);
		return result;
	}
	
private:
	void combine(int n, int k, int start, vector<int>& sub, vector<vector<int>>& result) {
		if (sub.size() == k) {
			result.push_back(sub);
			return;
		}
		for (int i = start; i <= n - (k - sub.size()) + 1; ++i) {
			sub.push_back(i);
			combine(n, k, i + 1, sub, result);
			sub.pop_back();
		}
	}
}
组合总和

给定一个数组 c a n d i d a t e s candidates candidates 和一个目标数 t a r g e t target target ,找出 c a n d i d a t e s candidates candidates 中所有可以使数字和为 t a r g e t target target 的组合。 c a n d i d a t e s candidates candidates 中的每个数字在每个组合中只能使用一次。
说明:

  • 所有数字(包括 t a r g e t target target)都是正整数。
  • 解集不能包含重复的组合。
示例

输入:candidates = [10,1,2,7,6,1,5], target = 8
输出:[[1, 7],[1, 2, 5],[2, 6],[1, 1, 6]]

解题思路

在这里插入图片描述
一个蓝色正方形表示的是“尝试将这个数到数组 c a n d i d a t e s candidates candidates 中找组合”,那么怎么找呢?挨个减掉那些数就可以了。
在减的过程中,会得到 0 和负数,也就是被标红色和粉色的结点:得到 0 是我们想要的,从 0 这一点向根结点走的路径(很可能只走过一条边,也算一个路径),就是一个组合,在这一点要做一次结算(把根结点到 0 所经过的路径,加入结果集);得到负数就说明这条路走不通,没有必要再走下去了。

class Solution {
public:
	vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> result;
        vector<int> sub;
        sort(candidates.begin(), candidates.end()); // 排序方便后面剪枝
        combine(candidates, target, 0, sub, result);
        return result;
    }
    
private:
	void combine(vector<int>& candidates, int target, int start, vector<int>& sub, vector<vector<int>>& result) {
		if (target < 0) return;
		if (target == 0) {
			result.push_back(sub);
			return;
		}
		for (int i = start; i < candidates.size(); ++i) {
			// 这一步剪枝操作基于 candidates 数组是排序数组的前提下,重复的数字经过排序肯定在相邻位置
			if (i > start && candidates[i] == candidates[i - 1]) continue;
            sub.push_back(candidates[i]);
            // 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
            combine(candidates, i + 1, target - candidates[i], sub, result);
            sub.pop_back();
        }
	}
}

类似的问题还有 组合总和组合总和 III因子的组合电话号码的字母组合

全排列 II

给定一个可包含重复数字的序列,返回所有不重复的全排列。

示例

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

解题思路在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> result;
        vector<int> sub;
        vector<bool> used(nums.size());
        sort(nums.begin(), nums.end());
        permute(nums, used, sub, result);
        return result;
    }
    
private:
    void permute(vector<int>& nums, vector<bool>& used, vector<int>& sub, vector<vector<int>>& result) {
        if (sub.size() == nums.size()) {
            result.push_back(sub);
            return;
        }
        for (int i = 0; i < nums.size(); ++i) {
        	// 不能使用重复数字
            if (used[i]) continue; 
            // 在进入一个新的分支之前,看一看这个数是不是和之前的数一样,
            // 如果这个数和之前的数一样,并且之前的数还未使用过,
            // 那接下来如果走这个分支,就会使用到之前那个和当前一样的数,就会发生重复,此时分支和之前的分支一模一样
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue; 
            used[i] = true;
            sub.push_back(nums[i]);
            permute(nums, used, sub, result);
            sub.pop_back();
            used[i] = false;
        }
    }
}

类似的题目还有 全排列字母大小写全排列

子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。

示例

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

解题思路在这里插入图片描述

由于原集合每一个数字只有两种状态,要么存在,要么不存在,那么在构造子集时就有选择和不选择两种情况,所以可以构造一棵二叉树,左子树表示选择该层处理的节点,右子树表示不选择,最终的叶节点就是所有子集合。

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> result;
        vector<int> sub;
        sort(nums.begin(), nums.end());
        subset(nums, 0, sub, result);
        return result;
    }
    
private:
    void subset(vector<int>& nums, int start, vector<int>& sub, vector<vector<int>>& result) {
        result.push_back(sub);
        for (int i = start; i < nums.size(); ++i) {
            sub.push_back(nums[i]);
            subset(nums, i + 1, sub, result);
            sub.pop_back();
        }
    }
}

类似的题目还有 子集 II

N皇后

n n n 皇后问题研究的是如何将 n n n 个皇后放置在 n ∗ n n*n nn 的棋盘上,并且使皇后彼此之间不能相互攻击。
在这里插入图片描述
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

示例

输入:4
输出:[
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],
["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解释:4 皇后问题存在两个不同的解法。

解题思路

对于这类问题,没有太简便的方法,只能使用穷举法,就是尝试所有的组合,每放置一个新的皇后的时候,必须要保证跟之前的所有皇后不能冲突,若发生了冲突,说明当前位置不能放,要重新找地方,这个逻辑非常适合用递归来做。
我们先建立一个长度为 n ∗ n n*n nn 的全是点的数组 q u e e n s queens queens,然后从第 0 行开始调用递归。在递归函数中,我们首先判断当前行数是否已经为 n n n,是的话,说明所有的皇后都已经成功放置好了,所以我们只要将 q u e e n s queens queens 数组加入结果中即可。
否则的话,我们遍历该行的所有列的位置,行跟列的位置都确定后,我们要验证当前位置是否会产生冲突,那么就需要使用一个子函数来判断了,首先验证该列是否有冲突,就遍历之前的所有行,若某一行相同列也有皇后,则冲突返回 false;再验证两个对角线是否冲突,就是一些坐标转换,主要不要写错了,若都没有冲突,则说明该位置可以放皇后,放了新皇后之后,再对下一行调用递归即可,注意递归结束之后要返回状态。

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> result;
        vector<string> queens(n, string(n, '.'));
        placeQueen(n, 0, queens, result);
        return result;
    }
    
private:
    void placeQueen(int n, int curRow, vector<string>& queens, vector<vector<string>>& result) {
        if (curRow == n) {
            result.push_back(queens);
            return;
        }
        for (int i = 0; i < n; ++i) {
            if (isValid(queens, curRow, i)) {
                queens[curRow][i] = 'Q';
                placeQueen(n, curRow + 1, queens, result);
                queens[curRow][i] = '.';
            }
        }
    }
    
    bool isValid(vector<string>& queens, int row, int col) {
        for (int i = 0; i < row; ++i) {
            if (queens[i][col] == 'Q') return false;
        }
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
            if (queens[i][j] == 'Q') return false;
        }
        for (int i = row - 1, j = col + 1; i >= 0 && j < queens.size(); --i, ++j) {
            if (queens[i][j] == 'Q') return false;
        }
        return true;
    }
}
解数独

编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:

  • 数字 1-9 在每一行只能出现一次。
  • 数字 1-9 在每一列只能出现一次。
  • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

空白格用 ‘.’ 表示。
在这里插入图片描述 在这里插入图片描述

解题思路

对于每个需要填数字的格子带入1到9,每代入一个数字都判定其是否合法,如果合法就继续下一次递归,结束时把数字设回 ‘.’,判断新加入的数字是否合法时,只需要判定当前数字是否合法,不需要判定这个数组是否为数独数组,因为之前加进的数字都是合法的,这样可以使程序更加高效一些。

class Solution {
public:
    void solveSudoku(vector<vector<char>>& board) {
        if (board.empty() || board.size() != 9 || board[0].size() != 9) return;
        solve(board, 0, 0);    
    }
    
private:
    bool solve(vector<vector<char>>& board, int i, int j) {
        if (i == 9) return true;
        if (j >= 9) return solve(board, i + 1, 0);
        if (board[i][j] == '.') {
            for (int k = 1; k <= 9; ++k) {
                board[i][j] = (char) (k + '0');
                if (isValid(board, i, j)) {
                    if (solve(board, i, j + 1)) return true;
                }
                board[i][j] = '.';
            }
        } else {
            return solve(board, i, j + 1);
        }
        return false;
    }
    
    bool isValid(vector<vector<char>>& board, int i, int j) {
        for (int col = 0; col < 9; ++col) {
            if (col != j && board[i][j] == board[i][col]) return false;
        }
        for (int row = 0; row < 9; ++row) {
            if (row != i && board[i][j] == board[row][j]) return false;
        }
        for (int row = i / 3 * 3; row < i / 3 * 3 + 3; ++row) {
            for (int col = j / 3 * 3; col < j / 3 * 3 + 3; ++col) {
                if ((row != i || col != j) && board[i][j] == board[row][col]) return false;
            }
        }
        return true;
    }
} 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值