1. BFS
广度优先搜索一层一层地进行遍历,每层遍历都是以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
1.1. 二进制矩阵中的最短路径(NO.1091)
题目描述:0 表示可以经过某个位置,求解从左上角到右下角的最短路径长度。
解题思路:使用BFS,设置一个队列保存节点,访问过将其在矩阵中对应的值改为1.每次处理一层中所有的节点。
class Solution {
public:
int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0)
return -1;
int m = grid.size(), n = grid[0].size();
if (grid[0][0] == 1)//开始节点就走不通
return -1;
vector<vector<int>> adj = {
{
-1,-1},{
-1,0},{
-1,1},{
0,-1},{
0,1},{
1,-1},{
1,0},{
1,1}};
queue<pair<int,int>> qu;
qu.push(make_pair(0,0));
grid[0][0] = 1;
int length = 1;
while (!qu.empty()){
//队列不为空时循环
int len = qu.size();//每层的节点数
for (int i = 0; i < len; i++){
//出队一个元素
int x = qu.front().first;
int y = qu.front().second;
qu.pop();
if (x == m-1 && y == n-1)
return length;
//将该元素周围可通过的元素进队
for (int j = 0; j < 8; j++){
int x1 = x + adj[j][0];
int y1 = y + adj[j][1];
if (x1 < 0 || x1 >= m || y1 < 0 || y1 >= n || grid[x1][y1] == 1)
continue;//越界或者不通
else{
qu.push(make_pair(x1,y1));
grid[x1][y1] = 1;
}
}
}
length++;
}
return -1;
}
};
2. DFS
2.1 查找最大的连通面积–岛屿的最大面积
题目描述:给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
class Solution {
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0)
return 0;
int maxArea = 0;
//遍历所有的点,得出以该点开始的岛屿面积
int m = grid.size(), n = grid[0].size();
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
int tmp = dfs(grid,i,j);
if (tmp > maxArea)
maxArea = tmp;
}
}
return maxArea;
}
//从(i,j)点进行深度优先遍历,判断岛屿面积
int dfs(vector<vector<int>>& grid, int i, int j){
if (i >= grid.size() || i < 0)
return 0;
if (j >= grid[0].size() || j < 0)
return 0;
if (grid[i][j] == 1){
grid[i][j] = 0;
return 1 + dfs(grid,i-1,j) + dfs(grid,i+1,j) + dfs(grid,i,j-1) + dfs(grid,i,j+1);
}
return 0;
}
};
2.2 矩阵中的连通分量数目–岛屿数量(NO.200)
题目描述:给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
解题思路:遍历矩阵的所有位置,若当前位置值为1,则进行深度优先遍历,遍历的过程中将遍历过的点值为0.
class Solution {
private:
vector<vector<int>> around = {
{
-1,0},{
1,0},{
0,-1},{
0,1}};
void dfs(vector<vector<char>>& grid, int i, int j){
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] == '0')
return;
grid[i][j] = '0';
for (auto d : around)
dfs(grid,i+d[0],j+d[1]);
}
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0)
return 0;
int cnt = 0;
for (int i = 0; i < grid.size(); i++){
for (int j = 0; j < grid[0].size(); j++){
if (grid[i][j] != '0'){
dfs(grid,i,j);
cnt++;
}
}
}
return cnt;
}
};
2.3 无向图连通块的个数–朋友圈数量(NO.547)
题目描述:给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
解题思路:给定的矩阵可以看成图的邻接矩阵。这样我们的问题可以变成无向图连通块的个数。visited[i] 表示第 i 个元素是否被深度优先搜索访问过。从第一个节点开始,访问其任一相邻的节点。然后再访问这一节点的任一相邻节点。这样不断遍历到没有未访问的相邻节点时,回溯到之前的节点进行访问。
class Solution {
private:
void dfs(vector<vector<int>>& M, int i, vector<int>& visited){
if (visited[i] == 1)
return;
visited[i] = 1;
for (int j = 0; j < M.size(); j++){
if (M[i][j] == 1 && visited[j] == 0)
dfs(M, j, visited);
}
}
public:
int findCircleNum(vector<vector<int>>& M) {
if (M.size() == 0)
return 0;
int m = M.size(), cnt = 0;
vector<int> visited(m,0);
for (int i = 0; i < m; i++){
if (visited[i] == 0){
dfs(M,i,visited);
cnt++;
}
}
return cnt;
}
};
3. 回溯
普通 DFS 主要用在可达性问题,这种问题只需要执行到特点的位置然后返回即可。而 Backtracking 主要用于求解排列组合问题,例如有 { ‘a’,‘b’,‘c’ } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
3.1 数字的全排列(NO.46)
题目描述:定一个 没有重复 数字的序列,返回其所有可能的全排列。
输入: [1,2,3];输出:[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> res;
if (n == 0)
return res;
vector<bool> visited(n,false);//用于标记数字是否已经用过
vector<int> path;//保存遍历的路径结果
dfs(nums,path,visited,res);
return res;
}
void dfs(vector<int>& nums, vector<int>& path, vector<bool>& visited, vector<vector<int>>& res){
if (path.size() == nums.size()){
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++){
if (!visited[i]){
//第i个数字没有访问过
path.push_back(nums[i]);//将数字i加入路径中
visited[i] = true;
dfs(nums,path,visited,res);//继续遍历
visited[i] = false;//回溯,将该数弹出并置为没有访问过
path.pop_back();
}
}
}
};
3.2 含有重复数字的数组全排列(NO.47)
题目描述:给定一个可包含重复数字的序列,返回所有不重复的全排列。
输入: [1,1,2];输出:[ [1,1,2], [1,2,1], [2,1,1]]
分析:上题中数组中数字不重复,本题数字可以重复。
当遍历到nums[i]时,如果nums[i]等于nums[i-1],则要判断是否进行剪枝。
如果visited[i-1]为真,则nums[i-1]访问过了,表示i与i-1是在同一条路径上;如果visited[i-1]为假,表示nums[i-1]刚刚被撤销选择,不在一条路径上(nums[i]与nums[i-1]在同一层,两个数对应的情况是一样的),要进行剪枝。
所以与上题相比,本题多了一个剪枝条件:
if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1])
continue;
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> res;
if (n == 0)
return res;
sort(nums.begin(),nums.end());//先进行排序,剪枝的前提
vector<bool> visited(n,false);//用于标记数字是否已经用过
vector<int> path;//保存遍历的路径结果
dfs(nums,path,visited,res);
return res;
}
void dfs(vector<int>& nums, vector<int>& path, vector<bool>& visited, vector<vector<int>>& res){
if (path.size() == nums.size()){
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++){
if (visited[i])
continue;
//第i个数字没有访问过
//剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
//写 !visited[i-1]是因为nums[i-1]在深度优先遍历的过程中刚刚被撤销选择
if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1])
continue;
path.push_back(nums[i]);//将数字i加入路径中
visited[i] = true;
dfs(nums,path,visited,res);//继续遍历
visited[i] = false;//回溯,将该数弹出并置为没有访问过
path.pop_back();
}
}
};
3.3 数组中数字组合的和为给定值(NO.39)
题目描述:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
输入:candidates = [2,3,6,7], target = 7;所求解集为:[ [7], [2,2,3]]
解题思路:使用回溯法+剪枝。
如果不剪枝,那么遍历到叶子节点的结果如上,会产生3个等效的结果,[2,2,3], [2,3,2], [3,2,2]。
遇到这一类相同元素不计算顺序的问题,我们在搜索的时候就需要按某种顺序搜索。具体的做法是:每一次搜索的时候设置 下一轮搜索的起点 begin。即:从每一层的第 2 个结点开始,都不能再搜索产生同一层结点已经使用过的 candidate 里的元素。
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
int n = candidates.size();
vector<vector<int>> res;
if (n == 0)
return res;
vector<int> path;
dfs(candidates,0,target,path,res);
return res;
}
void dfs(vector<int>& candidates, int begin, int target, vector<int>& path, vector<vector<int>>& res){
if (target < 0)
return;
if (target == 0){
res.push_back(path);
return;
}
for (int i = begin; i < candidates.size(); i++){
path.push_back(candidates[i]);
dfs(candidates,i,target-candidates[i],path,res);
path.pop_back();
}
}
};
3.4 组合总和II (NO.40)
题目描述:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
输入: candidates = [10,1,2,7,6,1,5], target = 8;所求解集为:[ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6]]
解题思路:与上一题的区别在于:本题数组中含有重复元素,且每个数字在每个组合中只能使用一次。因为含有重复元素,所以要采用类似于47题的剪枝方法;每个数字只能使用一次,所以begin = i + 1,即在下一层遍历中不能再用nums[i]。
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> res;
int n = candidates.size();
if (n == 0)
return res;
sort(candidates.begin(),candidates.end());
vector<int> path;
int begin = 0;
dfs(candidates,begin,target,path,res);
return res;
}
void dfs(vector<int>& candidates, int begin, int target, vector<int>& path, vector<vector<int>>& res){
if (target == 0){
res.push_back(path);
return;
}
if (target < 0)//剪枝
return;
for (int i = begin; i < candidates.size(); i++){
if (i > begin && candidates[i] == candidates[i - 1])//剪枝
continue;
path.push_back(candidates[i]);
// 因为元素只能使用一次,这里递归传递下去的是 i + 1 而不是 i
dfs(candidates,i+1,target-candidates[i], path, res);
path.pop_back();
}
}
};
3.5 组合总和III(NO.216)
题目描述:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
注意:所有数字都是正整数。解集不能包含重复的组合。
输入: k = 3, n = 9;输出: [[1,2,6], [1,3,5], [2,3,4]]
解题思路:本题有两个条件:(1)数字不能重复使用,所以在继续进行深度遍历的时候,下一次从i+1开始;(2)不能包含重复的集合,所以使用begin按顺序搜索。
class Solution {
public:
vector<vector<int>> combinationSum3(int k, int n) {
vector<vector<int>> res;
vector<int> path;
dfs(k,n,1,path,res);
return res;
}
void dfs(int k, int n, int begin, vector<int>& path, vector<vector<int>>& res){
if (k == 0 && n == 0){
//结束条件
res.push_back(path);
return;
}
if (k <= 0 || n <= 0)//剪枝
return;
for (int i = begin; i <= 9; i++){
path.push_back(i);
dfs(k-1, n-i, i+1, path, res);//继续深度遍历
path.pop_back();//回溯
}
}
};
3.6 子集(NO.78)
t题目描述:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。
输入: nums = [1,2,3];输出:[ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], []]
解题思路:使用回溯法。因为相同元素不考虑顺序,所以使用39题的剪枝方法。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
if (n == 0)
return res;
for (int len = 0; len <= nums.size(); len++){
vector<int> path;
dfs(nums,0,len,path,res);
}
return res;
}
void dfs(vector<int>& nums, int begin, int len, vector<int>& path, vector<vector<int>>& res){
if (path.size() == len){
res.push_back(path);
return;
}
for (int i = begin; i < nums.size(); i++){
path.push_back(nums[i]);
dfs(nums,i+1,len,path,res);
path.pop_back();
}
}
};
3.7 子集II(NO.90)
题目描述:给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。
输入: [1,2,2];输出:[ [2], [1], [1,2,2], [2,2], [1,2], []]
解题思路: 与上一题的区别在于数组中含有重复元素。所以使用类似于47题的方法进行了剪枝。
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
if (n == 0)
return res;
sort(nums.begin(),nums.end());
for (int len = 0; len <= n; len++){
vector<int> path;
dfs(nums,0,len,path,res);
}
return res;
}
void dfs(vector<int>& nums, int begin, int len, vector<int>& path, vector<vector<int>>& res){
if (path.size() == len){
res.push_back(path);
return;
}
for (int i = begin; i < nums.size(); i++){
//剪枝
if (i > begin && nums[i] == nums[i-1])
continue;
path.push_back(nums[i]);
dfs(nums,i+1,len,path,res);
path.pop_back();
}
}
};
3.8 分割回文串(NO.131)
题目描述:给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
输入: “aab”; 输出:[ [“aa”,“b”], [“a”,“a”,“b”]]
解题思路:使用回溯法。start表示所截取子串的开始下标,在每一个分支中,截取结束下标可以取start到s.size()-1中的任意值,表示截取了不同长度的子串。判断这个子串是否为回文串,如果不是,进行剪枝,如果是,则继续截取。
class Solution {
public:
vector<vector<string>> res;
void backtrack(string s,vector<string>&path,int start){
string temp;
if(start == s.size()){
//判断结束条件
res.push_back(path);
return;
}
for(int i=start;i<s.size();i++){
bool flag=true;
//从start截取到i得到的子串
temp=s.substr(start, i-start+1);
//判断是否为回文
int wide=temp.size();
for(int j=0;j<wide;j++){
if(temp[j]!=temp[wide-1-j]){
flag=false;
break;
}
}
//不是回文子串,剪枝
if(flag==false) continue;
//是回文子串
path.push_back(temp);
backtrack(s,path,i+1);//下一次从i+1开始截取,start=i+1
path.pop_back(); //还原
}
}
vector<vector<string>> partition(string s) {
vector<string> path;
backtrack(s,path,0);
return res;
}
};
3.9 电话号码的字母组合(NO.17)
题目描述:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
解题思路:
class Solution {
private:
unordered_map<char,string> myMap =
{
{
'2',"abc"},{
'3',"def"},{
'4',"ghi"},{
'5',"jkl"},
{
'6',"mno"},{
'7',"pqrs"},{
'8',"tuv"},{
'9',"wxyz"}};
vector<string> res;
string current;
void dfs(int index, string digits){
if (index == digits.size()){
res.push_back(current);
return;
}
string tmp = myMap[digits[index]];
for (int i = 0; i < tmp.size(); i++){
current.push_back(tmp[i]);//添加一个字符
dfs(index+1, digits);//继续递归
current.pop_back();//回溯
}
}
public:
vector<string> letterCombinations(string digits) {
if (digits.size() == 0)
return vector<string>();
dfs(0, digits);
return res;
}
};
3.10 字符串与矩阵路径(剑指offer)
题目描述:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径。
[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
int rows = board.size();
int cols = board[0].size();
bool res = false;
vector<vector<bool>> visited(rows,vector<bool>(cols,false));
for (int i = 0; i < rows; i++){
for (int j = 0; j < cols; j++){
string path;
res = res || dfs(board,word,path,visited,rows,cols,i,j);
}
}
return res;
}
bool dfs(vector<vector<char>>& board, string word, string& path, vector<vector<bool>>& visited, int rows, int cols, int row, int col ){
if (path.size() == word.size())
return true;
bool hasPath = false;
if (row >= 0 && col >= 0 && row < rows && col < cols && board[row][col] == word[path.size()] && !visited[row][col]){
path.push_back(board[row][col]);
visited[row][col] = true;
hasPath = dfs(board,word,path,visited,rows,cols,row-1,col)
|| dfs(board,word,path,visited,rows,cols,row,col-1)
|| dfs(board,word,path,visited,rows,cols,row+1,col)
|| dfs(board,word,path,visited,rows,cols,row,col+1);
if (!hasPath){
visited[row][col] = false;
path.pop_back();
}
}
return hasPath;
}
};
3.11 括号生成(NO.22)
题目描述:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
输入:n = 3输出:[ “((()))”, “(()())”, “(())()”, “()(())”,"()()()" ]
解题思路:可以使用暴力解法,往长度为2n的字符串中添加左括号或者右括号,然后判断是否有效。也可以使用回溯法。
class Solution {
public:
vector<string> generateParenthesis(int n) {
string str;
vector<string> res;
dfs(n,n,str,res);
return res;
}
void dfs(int left, int right, string& str, vector<string>& res){
if (left == 0 && right == 0){
res.push_back(str);
return;
}
if (left > 0){
//左括号只要还有,就可以随便放
str.push_back('(');
dfs(left-1, right, str, res);
str.pop_back();
}
if (right > left){
//添加右括号时,字符串中左括号的个数要大于右括号的个数
str.push_back(')');
dfs(left, right-1, str, res);
str.pop_back();
}
}
};