DFS+回溯:
题目简介 | LeetCode题号 |
---|---|
1-电话号码的字母组合 | LeetCode第17题 |
2-单词搜索 | LeetCode第79题 |
3-全排列 | LeetCode第46题 |
4-全排列Ⅱ | LeetCode第47题 |
5-子集 | LeetCode第78题 |
6-子集Ⅱ | LeetCode第90题 |
7-组合总和Ⅲ | LeetCode第216题 |
8-N皇后Ⅱ | LeetCode第52题 |
9-解数独 | LeetCode第37题 |
10-火柴拼正方形 | LeetCode第473题 |
1.LeetCode-17-电话号码的字母组合
题目描述:
题目分析:
LeetCode里面深度搜索的题目试比较多的,宽度搜索就比较少,所以这里也是重要讲深度搜索。搜索的话,可以用循环来实现,也可以用递归实现,所以深度搜索!=递归。这个题目,yy想实现的是用循环而不是用递归的形式来做。
代码:
class Solution {
public:
// 首先要把每个数字代表
string chars[8] = {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"}; // 下标是从 0 开始的,chars[0]是数字2下面的字母
vector<string> letterCombinations(string digits) {
if(digits.empty()) return vector<string>(); // 这里的返回值为什么加了(),不是很理解;
vector<string> state(1, ""); //表示盛放字符串的数组中只有一个字符串,是空;
for(auto u : digits){ // 1. 首先要循环每一个数字
vector<string> now; // 2. 用于保存新组合的字符串
for(auto c : chars[u - '2']){ // 3. 将当前数字下的字母取出来
for(auto s : state){ // 4. 遍历原来的字符串
now.push_back(s + c); // 加入新的字母
}
}
state = now; // 更新字符串
}
return state;
}
};
2-LeetCode-79-单词搜索
题目描述:
题目分析:
上面是题目的基本含义。该题是一个搜索题,搜索题最重要的是一个顺序问题。需要注意一点的是,我们在搜索过程中,只能往前或者说往下去搜索,而不能回头搜索。
- 枚举起点;
- 起点确定之后,从起点开始依次搜索下一个节点的位置(第二个点在搜索的时候就只能向后、向左、向右进行,不能向前进行)时间复杂度:nm * 3^k :其中nm分别代表行数和列数,路径的平均长度是k(也就是我们要搜索的单词的长度),3指的是每次有三个方向是可以选择的。在搜索的过程中主要是判断这个路径是不是合法的,是否合法就是要判断当前的路径是不是和我们要搜索的字母是一致的。
代码:
class Solution {
public:
int n , m ; // 全局变量,分别表示行和列
// 下面是枚举四个方向的技巧:隐约觉得这个方向数组用的很巧妙,实际上是用1 和 -1 来分别表示前进或者后退,然后用 0 表示不动,不太明白的是这个顺序:【0:左】【1:下】【2:右】【3:上】
int dx[4] = {-1 , 0 , 1 , 0} , dy[4] = {0 , 1 , 0 , -1 };
bool exist(vector<vector<char>>& board, string word) {
// 特别判断一下,如果这个盒子为空,或者列为空,返回false
if(board.empty() || board[0].empty()) return false;
n = board.size() , m = board[0].size();
for(int i = 0 ; i < n ; i++)
for(int j = 0 ; j < m ; j++)
if(dfs(board , i , j , word , 0))
return true;
return false;
}
// 写一个深度搜索的函数,其中x,y表示我们当前走到了那个格子了,word是要找的目表单词,u是我们找到目标单词的第几位了,这里传递参数board的时候传的是取地址的,因为这样的话,直接去地址找到这个数组,不需要将数组一遍又一遍的复制了
bool dfs(vector<vector<char>>& board , int x , int y , string& word , int u){
// 如果不匹配的话:
if(board[x][y] != word[u]) return false;
if(u == word.size() - 1) return true; //如果寻找到了单词的最后一个位置,那么就成功了;
board[x][y] = '.'; // 如果我们当前这个格子我们用过了,那么就不能再用了,这里就需要做一个标记;
// 这里的大部分代码都是需要回溯的,所谓的回溯就是恢复初始状态
for(int i = 0 ; i < 4 ; i++){
int a = x + dx[i] , b = y + dy[i];
if(a >= 0 && a < n && b >= 0 && b < m)
if(dfs(board , a , b , word , u + 1))
return true;
}
board[x][y] = word[u]; //这个应该就是所谓的回溯,恢复了初试状态,在进行初试状态之前还递归了很多次;这里恢复现场就是要保证在走不同的路径的时候,看到的初试状态是一致的;
return false;
}
};
3-LeetCode-46-全排列
题目描述:
题目分析:
3 – 5 是一个类型的题目,排列组合的枚举。一直在强调怎么样才能不遗漏枚举出来?关键就是顺序!枚举每一个位置上该放那个数;或者是枚举每一个数放在那个位置上。下面是这两种方式的分析,体会一下:
代码:
class Solution {
public:
// 实现的是枚举每一个位置:
int n ;
vector<bool> st ; // 用来保存当前这个分支用的数字是那些
vector<vector<int>> ans; // 用来存所有的方案
vector<int> path ; // 用来存当前的方案的
vector<vector<int>> permute(vector<int>& nums) {
n = nums.size() ;
st = vector<bool>(n) ;
dfs(nums , 0);
return ans;
}
void dfs(vector<int>& nums , int u){
if(u == n){ // 判断一下边界,如果都遍历完了,就插入到最终的结果中
ans.push_back(path);
return ;
}
// 否则枚举一下当前这个格子可以填进去那个数:
for(int i = 0 ; i < n; i++)
if(!st[i]){ // 当st为false的时候,表示可以填
st[i] = true;
path.push_back(nums[i]);
dfs(nums , u + 1);
path.pop_back(); // 这个是恢复现场
st[i] = false;
}
}
};
4-LeetCode-47-全排列Ⅱ
题目描述:
题目分析:
全排列的这个题目的话,是有两种方式:枚举每个位置上存放那个数;枚举每个数存放在哪个位置上。全排列的这个第Ⅱ个题目,用枚举每个数存放在那个位置(这里有重复的元素)。
- 这个全排列一个很重要的事情就是:判重!
- 解决这个问题的时候首先将要处理的是将这个int类型的数组里,相同的数放到一起——用“排序sort来实现”;
- 在全排列的过程中,因为有相同的数,而这个相同的数在前和在后是同样的顺序,为了避免相同的数被排两次,就要认为的规定一个顺序,因为我们之前用sort进行了排序,那么我们现在就要将这个顺序定为:不变,按照原来我们排好的顺序;
- 顺序不变的话,就要在dfs()函数里面多设置一下状态,以前用u来表示每个位置,现在用u来表示每个字母,并且还要dfs(u , start):start来表示当前可以从哪个数开始搜索,简单的来说:如果是1123的话,假设将第一个1放到了第三个位置,那么第2个1就要从第四个位置进行搜。
代码:
class Solution {
public:
// 首先把位置开辟出来:
int n ;
vector<vector<int>> ans;
vector<int> path;
vector<bool> st ; // 状态,应该是用来表示每个数是否被用了
vector<vector<int>> permuteUnique(vector<int>& nums) {
// 先开辟了这些空间
n = nums.size();
st = vector<bool>(n);
path = vector<int>(n);
sort(nums.begin() , nums.end());
dfs(nums , 0 , 0);
return ans;
}
// u 代表当前要存储第 u 个数,start表示当前可以枚举的位置的开始
void dfs(vector<int>& nums , int u , int start){ // start 表示当前从哪个位置开始枚举
// 判断一下是否所有的数字都放下了
if(u == n){
ans.push_back(path);
return ;
}
// 如果有数字还没放下,从start位置开始枚举
for(int i = start ; i < n ; i++){
if(st[i] == false){ // 开始看看每一个位置是否被用了,没有用,继续
st[i] = true;
path[i] = nums[u];
// 在进行继续枚举下一个数放到哪里的时候,需要判断一下,下一个数是否和当前数相同:
dfs(nums , u + 1 , u + 1 < n && nums[u + 1] == nums[u] ? i + 1 : 0);
st[i] = false; // 恢复现场
}
}
}
};
5-LeetCode-78-子集
题目描述:
题目分析:
做法有两种,一个是递归,一个是循环。递归的话最重要的就是顺序。这里用一个比较新的方法,迭代的写法,利用到了2进制的写法;是一个特别巧妙的写法,如下面展示的,先假设数字只有三个,那么它所有的子集合如下面的所示,一共有8个,是2^n – 1 (n = 3)个,那么可以用二进制表示,如下面所展示的,需要注意的点是:001表示个位数是1,各位是1,2,3中的1;这里需要注意的一点就是,如何找到每一位上的0或者是1?实现:
i >> j & 1
代码:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
for(int i = 0 ; i < 1 << nums.size() ; i++){ // nums.size()是一个数,左移1变成
vector<int> now; // 定义一个暂时的数组来存放一个子集合;
for(int j = 0 ; j < nums.size() ; j ++)
if(i >> j & 1) // i 先右移 j 位后再做与
now.push_back(nums[j]);
res.push_back(now);
}
return res;
}
};
这里用了一个左移一个右移,我必须整明白这两的的用法
6-LeetCode-90-子集Ⅱ
题目描述:
题目分析:
在上一个题目中,我们需要考虑的是这个数在集合中选择还是不选择,90这个题目里面有重复的数字,我们就需要考虑 每个数字选择几次,这里列举了一下 1,2,2,2,3,3这么几位数,首先是统计每个数的状态(出现的次数)
- 1:0(不出现),1(出现);
- 2:0(两个1都不出现),1(第一个1出现),2(第二个1出现),3(两个1都出现);
- 3:0,1,2;
总共的选择:2 * 4 * 3
代码:
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
// 为了实现把相同的数放到一起,须先排序一下
sort(nums.begin(), nums.end());
dfs(nums, 0); // 排序之后需要从第0个位置开始枚举
return ans;
}
void dfs(vector<int>& nums, int u){
// 当把所有的数都枚举一遍之后,把当前的路径加入进去
if(u == nums.size()){
ans.push_back(path);
return;
}
// 计算当前数字的个数:很简单的双指针算法
int k = 0 ;
while(k + u < nums.size() && nums[k + u] == nums[u]) k++;
// 有 k 个数与nums[u]相同
for(int i = 0; i <= k; i++)
{
dfs(nums, u + k);
path.push_back(nums[u]);
}
// 恢复现场:
for(int i = 0 ; i <= k ; i++) path.pop_back();
}
};
7-LeetCode-216-组合总和Ⅲ
题目描述:
题目分析:
到216题之前,全排列和组合数以及求所有集合,这个是求组合数。具体的做法就是枚举所有组合数的和,看看是否等于给定的数忽然明白了,这里所说的顺序,很多情况下是指的,我们枚举的顺序,比如枚举每个数放到哪个位置或者枚举每个位置放那些数。首先要讲的是,假如可选的数有:1,2,3,4,那么当我们选择了首先选择了1,那么第2个数只从后面的2,3,4中选,这样避免了重复;在一个就要考虑dfs的参数:
代码:
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> combinationSum3(int k, int n) {
// 1. 这里是倒着来的,首先是枚举第 k 个数,然后和是 n,再去枚举k-1个数,和是 n-k
dfs(k , 1 , n); // 开始枚举的位置是从1枚举到9
return ans;
}
void dfs(int k , int start, int n){
// 如果我们枚举完所有的数之后,因为枚举n个,每次选一个,最后选满了k个
if(k == 0){
// 如果k个数的和是n
if(n == 0) ans.push_back(path);
return;
}
for(int i = start ; i <= 9 ; i++){
path.push_back(i);
dfs(k-1 , i + 1, n - i);
path.pop_back(); //恢复现场
}
}
};
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> combinationSum3(int k, int n) {
// 1. 这里是倒着来的,首先是枚举第 k 个数,然后和是 n,再去枚举k-1个数,和是 n-k
dfs(k , 1 , n); // 开始枚举的位置是从1枚举到9
return ans;
}
void dfs(int k , int start, int n){
// 如果我们枚举完所有的数之后,因为枚举n个,每次选一个,最后选满了k个
if(k == 0){
// 如果k个数的和是n
if(n == 0) ans.push_back(path);
return;
}
// 这个地方是可以做一个优化的,就是我们每次不需要i从一个点开始,一直枚举到9,因为在这一步枚举到一个大的数(比如8,9)那么后面还不到k个数,没法继续了,这时候要保证从i到9起码要有k个数才可以;需要9 - i + 1 >= k,移项:i <= 10 -k
for(int i = start ; i <= 10 - k ; i++){
path.push_back(i);
dfs(k-1 , i + 1, n - i);
path.pop_back(); //恢复现场
}
}
};
8-LeetCode-52-N皇后Ⅱ
题目描述:
题目分析:
皇后可以沿着横线走,也可以沿着竖线走,还可以沿着对角线的形式走。既然用DFS的形式来解决这个N皇后的问题,首先要考虑的就是顺序。需要注意的一点是每一行和每一列都只能放一个皇后。yy讲解的时候说,因为在做这个N皇后的时候每一行上都会放上皇后,所以需要枚举每一行上皇后的位置(只需要保证在当前行不要和上一行皇后的位置冲突了),这非常类似与全排列。需要做的:1. 依次枚举每一行皇后的位置;2. 每一列只能有一个皇后;3. 每条斜线上只能有一个皇后;其中满足条件2需要有个状态:col[N],是bool类型的;在一个就是对角线上,对角线有正对角线和反对角线。这道题目还需要判断的一个点是:如何正确快速的判断坐标(x,y)在那个斜线上,需要注意的一点是,不需要精确 地判断出来,坐标(x,y)在那条斜线上,只需要判断两个点是否在同一个斜线上。这里只需要判断一下截距,而且两条直线只有一个符号。所以直接就判断:x + y = b 和x-y = b,因为这里x-y可能是负数,所以这里加上一个N。(这里的话去看代码比较清晰)
代码:
class Solution {
public:
int ans = 0 , n;
vector<bool> col , d , ud;
int totalNQueens(int _n) {
n = _n;
// 初试化状态:
col = vector<bool>(n);
d = ud = vector<bool>(n * 2);
dfs(0); // 从0行开始递归
return ans;
}
void dfs(int u){
if(u == n){
ans++;
return;
}
for(int i = 0 ; i < n ; i++ ){
if(!col[i] && !d[u + i] && !ud[u - i + n] ){ // false表示里面没有东西,true表示里面放了东西
col[i] = d[u + i] = ud[u - i + n] = true;
dfs(u + 1);
col[i] = d[u + i] = ud[u - i + n] = false; // 恢复现场
}
}
}
};
题意很长,做起来也不是很容易,但是想到这个方法的话,就太容易了
9-LeetCode-37-解数独
题目描述:
题目分析:
这个题目算是比较简单的,首先需要考虑的是枚举的顺序,顺序就是枚举每个空格该填什么数。需要维护的状态有:row[9][9],bool类型的,每一行下的每一个数字是否已经填过了,col[9][9]每一列的每一个数字是否填过了;cell[3][3][9]每一个九宫格的每一个数是否填过了。(N皇后和数独问题可以归纳为精确覆盖问题【dancing links】)
代码:
class Solution {
public:
bool row[9][9] = {0},col[9][9] = {0},cell[3][3][9] = {0};
void solveSudoku(vector<vector<char>>& board) {
// 首先将填过的数字更新一下他们的状态;
for(int i = 0 ; i < 9 ;i++){
for(int j = 0 ; j < 9 ; j ++){
char c = board[i][j];
if(c != '.'){
int t = c - '1'; // 将字符转化为数字
row[i][t] = col[j][t] = cell[i / 3][j / 3][t] = true; // 这个地方很难想到要除3
}
}
}
dfs(board , 0 , 0); // 从左上角开始
}
bool dfs(vector<vector<char>> &board , int x , int y){
// 首先要判断一下,如果是出界了,就要拐到下一行的开头,行数x要加一,列从0开始
if(y == 9) x++ , y = 0;
if(x == 9) return true; //遍历完成的话,就返回true,否则返回false,返回之前需要判断一下;
if(board[x][y] != '.') return dfs(board , x , y + 1); // 如果当前位置上已经填了别的数,递归下一个位置
// 需要填写:
for(int i = 0; i < 9 ; i++){
if(!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i]){
board[x][y] = '1' + i;
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
if(dfs(board , x , y + 1)) return true;
row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;
board[x][y] = '.';
}
}
return false;
}
};
10-LeetCode-473-火柴拼正方形
题目描述:
题目分析:
很经典的题目。它涉及了剪枝的过程。搜索顺序就是依次构造正方形的每一条边。剪枝的过程是:
- 从大到小枚举所有边,每次剪枝去掉的分支会更多。(为什么要从大到小:首先,选择边总的方式是一定的,只是顺序不一样。这里举一个列子:2,3,4,5,7 [说的是分支的数量] 先枚举7的分支再枚举2的分支就会先砍掉2下面的分支。)
- 每条边内部的木棒长度规定成从大到小(主要是为了防止枚举重复,认为规定一个顺序)
- 如果当前木棒拼接失败,则跳过接下来所有长度相同的木棒(如下面的图,假设一个木棒在拼接第2条边的时候失败了,而另一个相同长度的木棒在的拼接第3条边的时候成功了,那么这是矛盾的[这个不是很明白])
- 如果当前木棒拼接失败,且是当前边的第一个,则直接减掉当前分支假设在拼接第3条边的开头的时候就失败了,如果说将其放到第4个边的位置‘2’上成功了,那么就可以将其换到位置‘3’上面,这与前面矛盾了。
- 如果当前木棒拼接失败,且是当前边的最后一个,则直接减掉当前分支(与4很相似);
代码:
class Solution {
public:
vector<bool> st; // 用来盛放每个木棒是否被用过,没有用过是false,用过了true;
bool makesquare(vector<int>& nums) {
// 首先要判断一下给的木棒的长度是否能拼接个正方形
int sum = 0 ;
for(auto u : nums)
sum += u;
if(!sum || sum % 4) return false;
// 一定要排序,而且是是从大到小,所以这里就要翻转一下
sort(nums.begin() , nums.end());
reverse(nums.begin() , nums.end());
st = vector<bool>(nums.size());
return dfs(nums , 0 , 0 , sum / 4);
}
//dfs函数的参数,nums是所有边,u当前拼接到了第几条边,cur当前边的长度,每条边总的长度
bool dfs(vector<int>& nums , int u , int cur , int length){
// 特判一下:
if(cur == length) u ++ , cur = 0;
// 如果四条边都拼接完成了,那么就返回:
if(u == 4) return true;
for(int i = 0 ; i < nums.size() ; i++){
if(!st[i] && cur + nums[i] <= length) // 判断是否被用过,并且满足当前长度还不够
{
st[i] = true;
if(dfs(nums , u , cur + nums[i], length)) return true;
st[i] = false; // 恢复现场
// 第4个剪枝:位于第一个位置上
if(!cur) return false;
// 第5个剪枝:位于最后一个位置上
if(cur + nums[i] == length) return false;
// 现在开始实现第3个剪枝:
while(i + 1 < nums.size() && nums[i + 1] == nums[i]) i++;
}
}
return false;
}
};
NOTE: 标上黄色的都是比较重要的剪枝策略,能直接影响程序运行的速度。可以注释每一个剪枝的代码去看看效果。而且,这些剪枝是一些超级牛的人经过很长时间的研究才得出的结论,所以记住就行了,不用纠结。