1.DFS基础知识
题目中常用到的深度优先搜索范围:
1.矩阵中从当前节点往上下左右等方向搜索节点...
2.树状结构中沿着左右子节点搜索...
3.其余类型因题而异...
最后呈现的结果都是从一个切入点发散出来的dfs+回溯(全局恢复现场) ,有的题目不用恢复现场;dfs调用函数也是递归,可是一般来说没有返回值,每个节点执行到子节点最右侧叶子就表示该点的dfs完全结束了,
注意:对于复杂度有要求的题目需要有效的剪枝操作!
2.全排列题目
46. 全排列
class Solution {
public:
/*
1.切入点:
选择范围:每个位置可以填[0,n-1]个数
选择条件:该数没有被用过
剪枝条件:找到一种答案
恢复现场:path.pop_back()和状态st,便于这个数字在下一层使用
2.关键的全局变量:
每个排列的path;(因为在循环中做的选择,所以path是在变化的
1-n个数字是否使用过的bool状态数组st;
*/
vector<bool> st;
vector<int> path;
vector<vector<int>> res;
int n;
vector<vector<int>> permute(vector<int>& nums) {
n = nums.size();
st = vector<bool> (n, 0);
dfs(nums, 0);
return res;
}
void dfs(vector<int>& nums, int u)
{
if(u == n) {res.push_back(path); return;}
for(int i = 0; i < n; i ++)//选择范围
{
if(!st[i])//选择条件
{
st[i] = true;
path.push_back(nums[i]);
dfs(nums, u + 1);//递归
st[i] = false;//恢复现场
path.pop_back();
}
}
return;
}
};
内部剪枝:剑指 Offer 38. 字符串的排列(47. 全排列 II)
例如'122',以前的全排列会出现同一个位置取两次2的情况,可想而知这两种情况的子数都是一模一样的,也就是重复。所以只用对出现的第一个2进行dfs,先排序,让相同元素挨在一起,这样在同层的时候遍历到第二个2,发现和前面一个数相等,就可以剪枝了,前一个数st=0表示这两个2在同层,否则不一定是同层,不同层是不能直接剪枝的,
与上一题相比就多了剪枝的三个条件而已~
class Solution {
public:
/*
1.切入点:
选择范围:每个位置可以填[0,n-1]个数
选择条件:该数没有被用过
剪枝条件:(1)找到一种答案(2)内部剪枝:排序后的选择范围中前一个相同数已经用过了(st清0),这个数再用就会重复了
恢复现场:path.pop_back()和状态st,便于这个数字在下一层使用
2.关键的全局变量:
每个排列的path;(因为在循环中做的选择,所以path是在变化的
1-n个数字是否使用过的bool状态数组st;
*/
map<int,bool> st;
vector<string> res;
string tmp;
int n;
vector<string> permutation(string s) {
n = s.size();
if(!n) return {};
vector<char> news;
for(int i = 0; s[i]; i++) news.push_back(s[i]);
sort(news.begin(),news.end());
dfs(news, 0);
return res;
}
void dfs(vector<char> news,int u)
{
if (u == n) {res.push_back(tmp); return; }
for(int i = 0;i < n;i++)//选择范围
{
if(!st[i])//范围的条件+剪枝
{
//因为用到i-1所以需要i>0,同层需要st[i-1]=0
if(i>0 && news[i] == news[i-1] && !st[i-1]) continue;//剪枝,三个条件
tmp += news[i];//做选择
st[i] = true;//设置当前数已用
dfs(news, u+1); //进入下一层
st[i] = false;//撤销选择
tmp.pop_back();//撤销选择
}
}
return;
}
};
39. 组合总和
class Solution {
public:
/*
1.切入点:
选择范围:每个位置可以填[2,3,6,7]几个数,
选择条件:无
剪枝条件:这个答案不不行or找到一种答案
恢复现场:path.pop_back()
2.关键的全局变量:
每个排列的path;(因为在循环中做的选择,所以path是在变化的
*/
vector<vector<int>> ans;
vector<int> tmp;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
dfs(candidates,target,0);
return ans;
}
void dfs(vector<int>& candidates,int target,int u)
{
if(target == 0)//返回条件
{
ans.push_back(tmp);
return;
}
if(target < 0) return;//返回条件
for(int i = u; i < candidates.size(); i ++)//选择范围
{
tmp.push_back(candidates[i]);
dfs(candidates, target - candidates[i], i);//递归
tmp.pop_back();//恢复现场
}
}
};
216. 组合总和 III
class Solution {
public:
/*
1.切入点:
选择范围:每个位置可以填[u,9]几个数,
选择条件:无
剪枝条件:这个答案不不行or找到一种答案
恢复现场:path.pop_back()
2.关键的全局变量:
每个排列的path;(因为在循环中做的选择,所以path是在变化的
*/
class Solution {
public:
/*
1.切入点:
选择范围:每个位置可以填[u,9]几个数,
选择条件:无
剪枝条件:这个答案不不行or找到一种答案
恢复现场:path.pop_back()
2.关键的全局变量:
每个排列的path;(因为在循环中做的选择,所以path是在变化的
*/
vector<vector<int>> ans;
vector<int> tmp;
vector<vector<int>> combinationSum3(int k, int n) {
dfs(k, 1, n);
return ans;
}
void dfs(int k,int u,int n)//把需要的信息写到参数里即可
{
if(k == 0)//返回条件
{
if(n == 0) ans.push_back(tmp);
else return;
}
for(int i = u; i <=9; i ++)//选择范围
{
tmp.push_back(i);
dfs(k - 1, i + 1, n - i);//递归
tmp.pop_back();//恢复现场
}
}
};
22. 括号生成
class Solution {
public:
/*
1.切入点:
选择范围:'(',')'两种括号
选择条件:无,直接遍历
剪枝条件:个数超标or找到一种答案
2.关键的全局变量:
本题不用恢复现场,因为对每一层的两个dfs参数path都是一样的,不像以前在遍历过程中做选择!所以不用pop_back
*/
vector<string> ans;
int n;
vector<string> generateParenthesis(int _n) {
n = _n;
dfs("", 0, 0);
return ans;
}
void dfs( string path, int l,int r)
{
if(r > l || l > n || r > n) return;//返回条件
if(l == r && l == n)//返回条件
{
ans.push_back(path);
return;
}
dfs(path + '(' , l + 1, r );//选择范围
dfs(path + ')', l ,r + 1);
}
};
3.矩阵中的DFS
52. N皇后 II
class Solution {
public:
/*
1.切入点:
选择范围:每一行可以选择[0,n-1]个位置
选择条件:col d ud不攻击
剪枝条件:找到一种答案
恢复现场:修改col d ud状态
2.关键的全局变量:
col d ud的状态数组
*/
vector<bool> col, d, ud;
int n;
int ans;
int totalNQueens(int _n) {
n = _n;
col = vector<bool> (n, 0);
d = ud = vector<bool> (2 * n, 0);
dfs(0);
return ans;
}
void dfs(int u)
{
if(u == n)//返回条件
{
ans ++;
return;
}
for(int i = 0; i < n; i ++)//选择范围
{
if(!col[i] && !d[i + u] && !ud[i - u + n])//选择条件
{
col[i] = d[u + i] = ud[i - u + n] = true;
dfs(u + 1);//递归
col[i] = d[u + i] = ud[i - u + n] = false;//恢复现场
}
}
return;
}
};
79. 单词搜索(剑指 Offer 12. 矩阵中的路径)
class Solution {
public:
/*
1.切入点:
选择范围:每个点可以往4个方向搜索
选择条件:坐标满足
剪枝条件:值不相等or找到一种答案
恢复现场:修改该点状态st
2.关键的全局变量:
直接在矩阵中修改字母为'.',防止重复
*/
int n,m;
int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};//上右下左
bool exist(vector<vector<char>>& board, string word) {
//时间复杂度o(nm3^k)
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;//所有点都没搜到才是false
}
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;//返回条件
//如果等于,就从这个点开始dfs
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;//4个方向都不行的话
}
};
200. 岛屿数量
class Solution {
public:
/*
1.切入点:
选择范围:每个点可以往4个方向搜索
选择条件:坐标满足&&没被遍历过&&是岛屿
剪枝条件:无
恢复现场:
2.关键的全局变量:
本题为故意不恢复现场的一种应用
*/
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1 , 0, -1};
int cnt = 0;
int n, m;
vector<vector<char>> grid;
int numIslands(vector<vector<char>>& _grid) {
grid = _grid;
n = grid.size();
m = grid[0].size();
for(int i = 0; i < n; i ++)
for(int j = 0; j < m; j ++)
if(grid[i][j] == '1')
{
cnt ++;
dfs(i, j);
}
return cnt;
}
void dfs(int x, int y)//dfs把xy连着的点全部变成0
{
grid[x][y] = '0';
for(int i = 0; i < 4; i ++)
{
int a = dx[i] + x;
int b = dy[i] + y;
if(a >= 0 && a < n && b >= 0 && b < m && grid[a][b] == '1')
{
grid[a][b] = '0';
dfs(a, b);
}
}
}
};
4.挖坑
90. 子集 II
class Solution {
public:
/*
1.切入点:
选择范围:
选择条件:同层不与前一个元素重复
剪枝条件:到了最后一个dfs就停止
恢复现场:path.pop_back();
2.关键的全局变量:
path
*/
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
dfs(nums,0);
return res;
}
void dfs(vector<int>& nums, int u)
{
res.push_back(path);//全排列是只在u==n时加入,子集是在dfs过程中加入
//if(u == nums.size()) return ;//返回条件
for(int i = u; i < nums.size(); i ++)//选择范围
{
if(i > u && nums[i] == nums[i - 1]) continue;//剪枝
path.push_back(nums[i]);
dfs(nums, i + 1);//递归
path.pop_back();//恢复现场
}
}
};
37. 解数独
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;//标记已经出现了的点
}
}
dfs(board,0,0);//dfs行列
}
bool dfs(vector<vector<char>>& board, int x, int y)
{
if(y == 9) x ++, y = 0;//某一行已经填完了,跳到某一行起始位置
if(x == 9) return true;//返回条件
if(board[x][y] !='.') return dfs(board, x, y + 1);//返回条件
for(int i = 0; i < 9; i ++)//9个数字,选择范围
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;//找到一个就可以返回,最后的false是该点一个都不对的情况
row[x][i] =col[y][i]=cell[x/3][y/3][i] = false;//恢复现场
board[x][y] = '.';
}
return false;
}
};
剑指 Offer 13. 机器人的运动范围
class Solution {
public:
int get_single(int x)//算一个数的坐标位数和
{
int s = 0;
while(x) s += x % 10, x /= 10;
return s;
}
int get_sum(pair<int,int> p)//算横纵的
{
return get_single(p.first)+ get_single(p.second);
}
int res;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int n, m;
vector<vector<bool>> st;
int movingCount(int rows, int cols, int k) {
n = rows;
m = cols;
st = vector<vector<bool>> (n, vector<bool> (m, false));
res ++;
st[0][0] = true;
dfs(0, 0, k);
return res;
}
void dfs(int x, int y, int k)//遍历到每个点就好,对每个点判断
{
for(int i = 0; i < 4; i ++)
{
int a = dx[i] + x, b = dy[i] + y;
if(a < n && a >=0 && b < m && b >= 0 && get_sum({a,b}) <= k && !st[a][b])
{
res ++;
st[a][b] = true;
dfs(a, b, k);
}
}
}
};
3. 挖坑
473. 火柴拼正方形
class Solution {
public:
vector<bool> st;
bool makesquare(vector<int>& nums) {
//能否凑出4个相同数
//经典的剪枝问题
//优先枚举数字大的,这样分支少,会有更多的枝被剪掉
int sum = 0;
for(auto u : nums) sum += u;
if(!sum || sum % 4) return false;//sum为0或者不能被4整除
//1.排序,从大到小枚举
sort(nums.begin(), nums.end());
reverse(nums.begin(), nums.end());//从大到小排列
st = vector<bool> (nums.size());
return dfs(nums, 0, 0, sum / 4);//第几条边、当前长度、应有的长度
}
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;
if(!cur) return false;//2.是当前边的第一条,不满足时剪枝
if(cur + nums[i] == length) return false;//3.当前边的最后一条,不满足剪枝
while(i + 1 < nums.size() && nums[i + 1] == nums[i]) i ++;//4.跳过长度相同的木棍
}
return false;
}
};