广度优先搜索(BFS)一般由队列实现,其模板为:
void BFS(int s){
queue<int> q;
q.push(s);
while(!q.empty()){
取队首元素top;
访问队首元素top;
队首元素出队pop;
for(下一层未入队结点){
入队,并设置为已入队
}
}
}
深度优先搜索(DFS)可以用递归(调用系统栈实现)或栈实现。
栈模板
void DFS(int s){
stack<int> st;
st.push(s);
while(!st.empty()){
取栈顶元素top;
访问队顶元素top;
栈顶元素出栈pop;
for(下一层未入栈结点){
入队,并设置为已入栈
}
}
}
递归模板
void DFS(){
if(抵达边界条件) return;
修改某些边界条件或标志量;
//岔道口,剪枝减少计算量
if(满足某条件){//递归进入分支
DFS();
}
}
1、岛屿的最大面积(695)
搜索问题经典模板题,先介绍BFS思路:遍历二维数组每一个位置,如果是0跳过,如果为1,则判断其相邻4个位置是否为1(前提不能出界)。为防止重复访问,增加辅助数组标记,如果入过队则标记已访问。(当然本题可以直接把访问过的结点置为0更简单)直到所有数组被访问。
class Solution {
public:
//辅助方向数组,用于移动访问上下左右4个方向
vector<int> directions{-1,0,1,0,-1};
int BFS(vector<vector<int>>& grid,int x,int y){
if(grid[x][y]==0) return 0;
queue<pair<int,int>> q;
q.push({x,y});
grid[x][y] = 0;//置为0表示已访问过
int area = 0;
while(!q.empty()){
pair<int,int> top = q.front();
q.pop();
++area;
for(int i=0;i<4;i++){
int newX = top.first + directions[i];
int newY = top.second + directions[i+1];
if(newX>=0 && newX<grid.size() && newY>=0 && newY<grid[newX].size() && grid[newX][newY]==1){
grid[newX][newY] = 0;
q.push({newX,newY});
}
}
}
return area;
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int maxArea = 0;
for(int i=0; i<grid.size();i++){
for(int j=0; j<grid[i].size(); j++){
maxArea = max(maxArea,BFS(grid,i,j));
}
}
return maxArea;
}
};
pair和队列混合使用不太习惯,其实可以用两个queue分别存储x和y坐标。
DFS思路,可以用栈也可以用递归方式,递归更易于实现,但一般为了防止出现递归栈满的情况,用栈比较合适。
class Solution {
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
vector<int> directions{-1,0,1,0,-1};
int maxArea = 0, m = grid.size(), n = grid[0].size();
stack<pair<int,int>> st;
for(int x=0; x<m; x++){
for(int y=0; y<n; y++){
int area = 0;
if(grid[x][y]==1){
grid[x][y] = 0;
st.push({x,y});
while(!st.empty()){
auto [r,c] = st.top();
st.pop();
++area;
for(int i=0;i<4;i++){
int newx = r + directions[i];
int newy = c + directions[i+1];
if(newx>=0 && newx<m && newy>=0 && newy<n && grid[newx][newy]==1 ){
st.push({newx,newy});
grid[newx][newy] = 0;
}
}
}
}
maxArea = max(maxArea,area);
}
}
return maxArea;
}
};
语法:stack或queue中加入pair元素,使用.push({x,y}),读取auto[x,y] = .top()
递归方式
class Solution {
public:
vector<int> directions{-1,0,1,0,-1};
int DFS(vector<vector<int>>& grid,int r,int c){
//边界条件
if(grid[r][c]==0) return 0;
grid[r][c] = 0;
int area = 1;
for(int i=0; i<4; i++){
int x = r + directions[i], y = c + directions[i+1];
if(x>=0 && x<grid.size() && y>=0 && y<grid[0].size()){
area += DFS(grid,x,y);
}
}
return area;
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int maxArea = 0;
for(int i=0;i<grid.size();i++){
for(int j=0;j<grid[0].size();j++){
maxArea = max(maxArea,DFS(grid,i,j));
}
}
return maxArea;
}
};
2、省份数量(547)
本质上是求连通子图个数。
思路一:DFS\BFS。以DFS为例,对城市 i ,考察还未访问的城市 j 与之是否同省(对应值为1),若同省,则递归对j进行上述过程,并标记 j 被访问。本质上就是一次调用DFS函数把某一个省的城市都找出来并标记,则调用DFS次数就是省份数量。
递归写法
class Solution {
public:
void DFS(vector<vector<int>>& isConnected,int i,vector<bool>& isFind){
isFind[i] = true;
for(int j = 0; j<isConnected.size(); j++){
if(isConnected[i][j]==1 && !isFind[j]){
DFS(isConnected,j,isFind);
}
}
}
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size(),numProvices = 0;
//vector初始化方法为同一值
vector<bool> isFind(n,false);
for(int i=0; i<n; i++){
if(!isFind[i]){
DFS(isConnected,i,isFind);
++numProvices;
}
}
return numProvices;
}
};
栈实现方法
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size(),numProvices = 0;
//vector初始化方法为同一值
vector<bool> visited(n,false);
for(int i=0; i<n; i++){
if(!visited[i]){
stack<int> st;
st.push(i);
visited[i] =true;
while(!st.empty()){
int first = st.top();
st.pop();
for(int j=0; j<n; j++){
if(isConnected[first][j]==1 && !visited[j]){
st.push(j);
visited[j] = true;
}
}
}
++numProvices;
}
}
return numProvices;
}
};
方法二:使用并查集
并查集的核心思想是:用数组father[ i ]存储数组 i 的父节点,从而实现合并(集合的合并)和查找(判断两个元素是否在一个集合)的操作。我们可以进行路径压缩,使同一个集合元素共有一个父节点。
集合数量统计可以开辟一个bool数组,记录是否为根节点。当然由于进行了路径压缩,也可以直接看father[i] == i的数量。
class Solution {
public:
//查找x所在集合的根节点
int findFather(vector<int>& father,int x){
int a = x;
//找到父节点
while(x!=father[x]){
x = father[x];
}
//路径压缩
while(a!=father[a]){
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size(),numProvices = 0;
vector<int> father(n);// 存放父节点
vector<int> isRoot(n,false);//记录每个结点是否作为根节点,便于统计省份数目
// 父节点初始化
for(int i=0; i<n; i++){
father[i] = i;
}
for(int i=0; i<n; i++){
for(int j=0; j<i; j++){//合并同省份城市所在集合
if(isConnected[i][j]==1){
int faI = findFather(father,i);
int faJ = findFather(father,j);
if(faI != faJ){
father[faI] = faJ;
}
}
}
}
//i的跟结点为findFather[i]
for(int i=0;i<n;i++){
isRoot[findFather(father,i)] = true;
}
for(int i=0;i<n;i++){
numProvices += isRoot[i];
}
return numProvices;
}
};
语法:初始化vector数组为同一数组,vector<int> isRoot(n,false);
3、太平洋大西洋水流问题(417)
如果对每个位置进行搜索判断,不剪枝情况下时间复杂度非常高,因此反向思考,从4个边界向中间高处流,可以被抵达的点就是答案,这样只需要考察4个边界。
class Solution {
public:
vector<int> directions{-1,0,1,0,-1};
//判断(r,c)向高处流能到达哪些点
void dfs(vector<vector<int>>& heights,vector<vector<bool>>& can_reach,int r,int c){
if(can_reach[r][c]) return;
can_reach[r][c] = true;
for(int i=0; i<4; i++){
int x = r + directions[i], y = c + directions[i+1];
if(x>=0 && x<heights.size() && y>=0 && y<heights[0].size() && heights[r][c]<=heights[x][y]){
dfs(heights,can_reach,x,y);
}
}
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
if(heights.size()==0 || heights[0].size()==0) return {};
int m = heights.size(), n = heights[0].size();
//标记各点能否抵达太平洋或者大西洋
vector<vector<bool>> can_reach_p (m,vector<bool>(n,false));
vector<vector<bool>> can_reach_a (m,vector<bool>(n,false));
for(int i=0; i<m; i++){
//考察哪些点能抵达左右边界
dfs(heights,can_reach_p,i,0);
dfs(heights,can_reach_a,i,n-1);
}
for(int i=0; i<n; i++){
//考察哪些点能抵达上下边界
dfs(heights,can_reach_p,0,i);
dfs(heights,can_reach_a,m-1,i);
}
vector<vector<int>> ans;
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(can_reach_p[i][j] && can_reach_a[i][j]){
ans.push_back(vector<int>{i,j});
}
}
}
return ans;
}
};
语法:初始化二维vector,如vector<vector<bool>> can_reach_p (m,vector<bool>(n,false));
4、全排列(46)
全排列过程可以这样递归实现。递归过程:让n个元素轮流作为首位,对剩余n-1个元素进行全排列,我们可以用交换的方法,把每个元素交换到首位。递归边界:当只有1个元素,全排列即为本身。
对n-1个结点完成全排列后,我们需要恢复之前的状态,把后面的元素交换到首位,这就涉及到回溯法。回溯法比起DFS多了一个递归后修改回原结点状态的过程(使用引用传递状态)。
class Solution {
public:
void backtracking(vector<int>& nums,int level,vector<vector<int>>& ans){
//边界,交换到最后一个元素
if(level == nums.size()-1){
ans.push_back(nums);
return;
}
//level位元素和之后所有元素逐一交换,然后递归,之后回溯法修改回原状态
for(int i=level; i<nums.size(); i++){
swap(nums[i],nums[level]);
backtracking(nums,level+1,ans);
swap(nums[i],nums[level]);//回溯,修改回原状态
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;
backtracking(nums,0,ans);
return ans;
}
};
5、组合(77)
递归链条:从区间[st, n]中选出k个整数,如果选中st,则在[st+1, n]中选择k-1个整数;没选中st,则在[st+1, n]中选择k个整数。
递归边界:如果n-st+1<k,无法实现组合;如果k=0,已经找到k个数组合了。
回溯法运用:用nums数组存储已经选中的数字,用pop_back维持未选中的状态。
class Solution {
public:
//参数k表示还需选中的数字个数
void backtracking(int st, int n, int k,vector<int>& nums,vector<vector<int>>& ans){
if(n-st+1 < k) return;//剩余元素不足k个,无法实现组合
if(k == 0) {//已经本组组合找齐元素了
ans.push_back(nums);
return;
}
//选中st
nums.push_back(st);
backtracking(st+1,n,k-1,nums,ans);
//没选中st
nums.pop_back();
backtracking(st+1,n,k,nums,ans);
}
vector<vector<int>> combine(int n, int k) {
vector<int> nums;
vector<vector<int>> ans;
backtracking(1,n,k,nums,ans);
return ans;
}
};
6、单词搜索(79)
递归过程:如果当前字符board[r][c]与目标单词的字母word[idx]相同,则对其上下左右(不出界)且没有被访问的结点,进行递归判断,看其是否是word[idx+1]。
递归边界:当idx == word.size()-1,且当前字符board[r][c]与目标单词的字母word[idx]相同,则找到了该单词。
回溯:如果子递归找不到该单词,重新设置board[r][c]为未访问。
class Solution {
public:
vector<int> directions{-1,0,1,0,-1};
void backtracking(vector<vector<char>>& board,string word,vector<vector<bool>>& visited,int r, int c, bool& flag,int idx){
if(visited[r][c]==false && board[r][c] == word[idx]){
if(idx = word.length()-1){
flag = true;
return;
}
visited[r][c] = true;//(r,c)已被选用
for(int i=0;i<4;i++){
int x = r + directions[i], y = c + directions[i+1];
if(x>=0 && x<board.size() && y>=0 && y<board[0].size()){
backtracking(board,word,visited,x,y,flag,idx+1);
}
}
visited[r][c] = false;
}
}
bool exist(vector<vector<char>>& board, string word) {
int m = board.size(), n = board[0].size();
bool flag = false;
vector<vector<bool>> visited(m,vector<bool>(n,false));
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
backtracking(board,word,visited,i,j,flag,0);
if(flag) return true;
}
}
return false;
}
};
7、N 皇后(51)
经典N皇后问题,同样采用回溯法。
递归过程:N皇后排列结果必定是每行和每列仅有1个皇后,因此我们可以按行填入。当我们在第row行第 i 个位置放入一个皇后时,则第i个位置对应的竖行、左、右斜行都不可再填入(用对应数组标识),然后对第row+1行递归,寻找合适位置填入下一个皇后。
递归边界:当放置第N行时(编号0~N-1),已经找到一种摆法,保存结果。
回溯:递归后,把第row行第 i 个位置摆放的皇后,第i个位置对应竖行、左、右斜行修改回来。
N皇后的问题的难点主要在于选中一个位置放入皇后,要如何把其对应的行,列,左右斜行给修改,使之不能再填入别的皇后。特别是要注意到同一斜行与其行、列号的关系。
class Solution {
public:
void backtracking(vector<vector<string>>& ans,vector<string>& board, vector<bool>& columns, vector<bool>& ldiag, vector<bool>& rdiag,int row,int n){
//已经摆放了n个皇后了
if(row == n){
ans.push_back(board);
return;
}
//在第row行寻找合适位置放入皇后
for(int i=0; i<n; i++){
//判断第i个位置对应的列、左、右斜行是否已有皇后
//以左上为原点,从左上到右下给左斜行编号[0~2n-1],其左斜行号 = 行号 + 列号
//从右上到左下给右斜行编号[0~2n-1],其右斜行号 = 行号 - 列号 + (n-1)
if(columns[i] || ldiag[row+i] || rdiag[row - i + n -1]){
continue;
}
//在找到的合适位置放入皇后,并修改对应列、左、右斜行
board[row][i] = 'Q';
columns[i] = ldiag[row+i] = rdiag[row - i + n -1] = true;
//对第row+1行递归
backtracking(ans,board,columns,ldiag,rdiag,row+1,n);
//回溯
board[row][i] = '.';
columns[i] = ldiag[row+i] = rdiag[row - i + n -1] = false;
}
}
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> ans;
if(n==0) return ans;
//存储摆放结果
vector<string> board(n,string(n,'.'));
//ldiag和rdiag为左右斜行,n*n方格有2n-1个左、右斜行
vector<bool> columns(n,false), ldiag(2*n-1,false),rdiag(2*n-1,false);
backtracking(ans,board,columns,ldiag,rdiag,0,n);
return ans;
}
};
8、最短的桥(934)
本题首先要认识到,从一个岛到另一岛的最短距离 = 从一个岛进行宽度优先搜索找到另一个岛扩展层数 -1。具体思路:
(1)先用深度优先搜索,把第一个岛的结点全部找出加入队列;
(2)bfs向外拓展,直到找到另一个岛,所有入队的结点把值改为2,标记为已访问。
class Solution {
public:
queue<pair<int,int>> q;
vector<int> directions{-1,0,1,0,-1};
void dfs(vector<vector<int>>& grid,int r,int c){
grid[r][c] = 2;
q.push({r,c});
for(int i=0; i<4; i++){
int x = r + directions[i], y = c + directions[i+1];
if(x>=0 && x<grid.size() && y>=0 && y<grid.size() && grid[x][y]==1){
dfs(grid,x,y);
}
}
}
int shortestBridge(vector<vector<int>>& grid) {
int n = grid.size();
//先用dfs找出一个岛的所有结点入队,并把1改为2用于标识已访问
int flag = false;
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
if(grid[i][j]==1){
dfs(grid,i,j);
flag = true;
break;
}
}
if(flag) break;
}
//用bfs向外搜索,到碰见1时向外搜索的层数-1即为桥数
int layer = 0;
while(!q.empty()){
++layer;
//把本层全部取出
int num = q.size();
while(num--){
auto [r,c] = q.front();
q.pop();
for(int k=0; k<4; k++){
int x = r + directions[k], y = c + directions[k+1];
if(x>=0 && x<n && y>=0 && y<n){
if(grid[x][y]==1) return layer-1;//拓展到另一个岛
else if(grid[x][y]==2) continue;//已入队结点
else if(grid[x][y]==0) {//0结点入队,并修改为2标记已入过队
grid[x][y] = 2;
q.push({x,y});
}
}
}
}
}
return 0;
}
};
9、被围绕的区域(130)
本题类似于前面的找岛问题,不同在于不考虑有边界点的岛。我的思路是:用数组存储岛的结点,再用一个标志变量flag存储该岛是否存在边界结点,如果不存在,则把数组中的结点修改为X。
class Solution {
public:
vector<int> direction{-1,0,1,0,-1};
void dfs(vector<vector<char>>& board, vector<vector<bool>>& visited, vector<pair<int,int>>& pos, int r,int c,bool& flag){
visited[r][c] = true;
if(r==0 || r==board.size()-1 || c==0 || c==board[0].size()-1){//边界上的结点
flag = false;
}
pos.push_back({r,c});
for(int i=0;i<4;i++){
int x = r + direction[i], y = c + direction[i+1];
if(x>=0 && x<board.size() && y>=0 && y<board[0].size() && board[x][y]=='O' && !visited[x][y]){
dfs(board,visited,pos,x,y,flag);
}
}
}
void solve(vector<vector<char>>& board) {
int m = board.size(), n = board[0].size();
vector<vector<bool>> visited(m,vector<bool>(n,false));
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
if(board[i][j]=='O' && !visited[i][j]){
bool flag = true;//判断岛是否有边界结点
vector<pair<int,int>> pos;//存储岛的结点
dfs(board,visited,pos,i,j,flag);
if(flag){
for(auto[x,y]: pos){
board[x][y] = 'X';
}
}
}
}
}
}
};
参考题解发现更优思路:可以先对边界点进行dfs,把包含边界点的岛修改为‘A’。之后对所有结点遍历,若结点为'A',则改回‘O’,若结点为'O'则修改为'X'。
class Solution {
public:
int n, m;
void dfs(vector<vector<char>>& board, int x, int y) {
if (x < 0 || x >= n || y < 0 || y >= m || board[x][y] != 'O') {
return;
}
board[x][y] = 'A';
dfs(board, x + 1, y);
dfs(board, x - 1, y);
dfs(board, x, y + 1);
dfs(board, x, y - 1);
}
void solve(vector<vector<char>>& board) {
n = board.size();
if (n == 0) {
return;
}
m = board[0].size();
for (int i = 0; i < n; i++) {
dfs(board, i, 0);
dfs(board, i, m - 1);
}
for (int i = 1; i < m - 1; i++) {
dfs(board, 0, i);
dfs(board, n - 1, i);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (board[i][j] == 'A') {
board[i][j] = 'O';
} else if (board[i][j] == 'O') {
board[i][j] = 'X';
}
}
}
}
};
10、二叉树的所有路径(257)
采用回溯法。
递归过程:一个结点左子树不为空时,把左子树值加入str,向左子树递归,递归完回溯,把插入值去掉,恢复str;右子树同理。
递归边界,一个结点左右子树都为空时,到达了叶子结点,把str加入ans,返回。
class Solution {
public:
void dfs(vector<string>& ans, string& str,TreeNode* root){
if(root->left == nullptr && root->right == nullptr){//抵达叶子结点
ans.push_back(str);
return;
}
if(root->left != nullptr){
int pos = str.size();
str += "->";
str += to_string(root->left->val);//to_stirng函数把int转化为string
dfs(ans,str,root->left);
//回溯,把插入的左子树部分删除
str.erase(str.begin()+pos,str.end());
}
if(root->right != nullptr){
int pos = str.size();
str += "->";
str += to_string(root->right->val);
dfs(ans,str,root->right);
str.erase(str.begin()+pos,str.end());
}
}
vector<string> binaryTreePaths(TreeNode* root) {
if(root == nullptr) return {};
//因为根节点前面没有->,所以把把根节点值先加入
string str = to_string(root->val);
vector<string> ans;
dfs(ans,str,root);
return ans;
}
};
语法:to_string()把数值型数据(如int\double\long等)转化为string。
在仔细思考,其实这题中存储某条路径的str不需要引用传递,直接用形参就复制了一份副本,不需要删除回溯这个操作了。
class Solution {
public:
void construct_paths(TreeNode* root, string path, vector<string>& paths) {
if (root != nullptr) {
path += to_string(root->val);
if (root->left == nullptr && root->right == nullptr) { // 当前节点是叶子节点
paths.push_back(path); // 把路径加入到答案中
} else {
path += "->"; // 当前节点不是叶子节点,继续递归遍历
construct_paths(root->left, path, paths);
construct_paths(root->right, path, paths);
}
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> paths;
construct_paths(root, "", paths);
return paths;
}
};
11、全排列 II(47)
本题是全排列的变形题,区别在于本题存在重复的元素,在回溯法的过程中会产生重复结果。这些重复结果产生的原因是,重复元素背交换到pos位,因此可以建立一个集合,存储pos中出现过的元素,只有没出现在pos位上的元素才能执行后续操作和递归。
class Solution {
public:
void backtracking(vector<int>& nums,vector<vector<int>>& ans,int pos){
if(pos == nums.size()-1){
ans.push_back(nums);
return;
}
unordered_set<int> s;
for(int i=pos; i<nums.size(); i++){
if(!s.count(nums[i])){
s.insert(nums[i]);
swap(nums[i],nums[pos]);
backtracking(nums,ans,pos+1);
swap(nums[i],nums[pos]);
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> ans;
backtracking(nums,ans,0);
return ans;
}
};
12、组合总和(39)
组合数的变形题,要求从组合出k个数变成了组合数字的和为target。仍然可以用回溯法,设candidates共有n个数。
递归过程:若选中第k个数时,则对[k,n-1]继续递归,使[k,n-1]中选中的数字和等于sum - candidates[k];若不选中第k个数时,回溯,去掉选中candidates[k],的则对[k+1,n-1]继续递归,使[k+1,n-1]中选中的数字和等于sum 。
递归边界:若当前sum=0,则保存答案返回;若sum<0或是k==n,则直接返回。
class Solution {
public:
void backtracking(vector<vector<int>>& ans,vector<int>& candidates,vector<int>& nums,int sum,int k){
if(sum == 0){//这一边界条件要放在上面,不然可能会在sum=0,而candidates.size()时直接返回
ans.push_back(nums);
return;
}
if(sum<0 || k==candidates.size()){
return;
}
//选中candidates[k]
nums.push_back(candidates[k]);
backtracking(ans,candidates,nums,sum - candidates[k],k);
//未选中candidates[k]
nums.pop_back();
backtracking(ans,candidates,nums,sum,k+1);
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> nums;
backtracking(ans,candidates,nums,target,0);
return ans;
}
};
13、组合总和 II(40)
变式题,这一题里元素本身就包含重复元素,每个元素只能用一次。难点在于如何产生不重复的结果。如下例:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
candiates中有两个1,但是只能产生一个[1,7]的结果,因此需要改进递归条件。为了不让重复的元素执行相同的递归过程,得到相同的递归结果,对相同元素,只取一个进行递归即可。为此,我们先把candiates进行排序,相同元素会连续排列,选中第一个进行递归,不选中时跳到下一个不同元素进行递归。
class Solution {
public:
void backtracking(vector<vector<int>>& ans,vector<int>& candidates,vector<int>& nums,int sum,int k){
if(sum == 0){//这一边界条件要放在上面,不然可能会在sum=0,而candidates.size()时直接返回
ans.push_back(nums);
return;
}
if(sum<0 || k==candidates.size()){
return;
}
//选中candidates[k]
nums.push_back(candidates[k]);
backtracking(ans,candidates,nums,sum - candidates[k],k+1);
//未选中candidates[k],为了不产生重复结果,跳到下一个不同的元素递归
nums.pop_back();
int i=k+1;
for(i=k+1;i<candidates.size();i++){
if(candidates[i]!=candidates[k]) break;
}
backtracking(ans,candidates,nums,sum,i);
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
vector<vector<int>> ans;
vector<int> nums;
backtracking(ans,candidates,nums,target,0);
return ans;
}
};
14、解数独(37)
这题看成是N皇后问题的扩展题,都是不同方格间填入元素的存在制约关系,需要利用这一关系剪枝,搜索出最终结果。N皇后是行、列、斜行间的制约关系,数独是行、列、九宫格内的制约关系。所以思路也类似。维系row, col, block的辅助数组,记录(r, c)点所在行、列、块内已经使用过的数字。
递归思路:如果(r, c)点是'.’,则根据对应row, col, block,选出所有可能值填入,并修改row, col, block辅助状态,之后移动到下一个点递归,然后回溯,把填入值、辅助数组值改回来,;如果(r, c)点已经有数字,直接递归下一个点。
递归边界:按行优先移动点,当递归到(9,0)点时,说明已经得到答案,保存结果后返回。
class Solution {
public:
//记录每一行、列、九宫格已经使用的数字
bool row[9][10] = {false}, col[9][10] = {false}, block[3][3][10] = {false};
char ans[9][9];//记录最终答案
void backtracking(vector<vector<char>>& board,int r,int c){
if(r==9 && c==0){
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
ans[i][j] = board[i][j];
}
}
return;
}
int x,y;//下一个递归点
if(c==8) {
x = r+1;
y = 0;
}else{
x = r;
y = c+1;
}
if(board[r][c]=='.'){
//寻找1-9间的合适值填入
for(int i=1;i<10;i++){
if(row[r][i] || col[c][i] || block[r/3][c/3][i]){
//(r,c)所在行、列或九宫格已经有数字i
continue;
}
row[r][i] = col[c][i] = block[r/3][c/3][i] = true;
board[r][c] = '0' + i;
backtracking(board,x,y);
//回溯
row[r][i] = col[c][i] = block[r/3][c/3][i] = false;
board[r][c] = '.';
}
}else{
backtracking(board,x,y);
}
}
void solveSudoku(vector<vector<char>>& board) {
//更新记录表
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]!='.'){
int num = board[i][j] - '0';
row[i][num] = true;
col[j][num] = true;
block[i/3][j/3][num] = true;
}
}
}
backtracking(board,0,0);
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
board[i][j] = ans[i][j];
}
}
}
};
15、最小高度树(310)
本题最直接的思路是,把边读入,按邻接表存储图。以每个结点为根结点,进行dfs或bfs,求出当前树的高度,并更新最小树高,保存结果。但这样的方法时间复杂度较高,在结点很多时会超过时间界限。
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
vector<vector<int>> G(n);
for(int i=0;i<n-1;i++){
int a = edges[i][0], b = edges[i][1];
G[a].push_back(b);
G[b].push_back(a);
}
vector<int> ans;
int min_depth = n;
for(int i=0; i<n; i++){
int depth = -1;
vector<bool> visited(n,false);
queue<int> q;
q.push(i);
visited[i] = true;
while(!q.empty()){
int size = q.size();
++depth;
for(int j=0; j<size;j++){
int root = q.front();
q.pop();
for(int k=0; k<G[root].size(); k++){
int node = G[root][k];
if(!visited[node]){
q.push(node);
visited[node] = true;
}
}
}
}
if(min_depth>depth){
min_depth = depth;
ans.clear();
ans.push_back(i);
}else if(min_depth == depth){
ans.push_back(i);
}
}
return ans;
}
};
参考大佬解答,注意发现以图中心的结点作为根,树高会更小,而用边缘结点(如叶子)树高会较大。如下图示例:0、2、3都是边缘叶子结点,所以树高都较大,而中心的1作为根,则树高较小。
既然度为1的边缘结点会是不可能作为最小树高结点,那么我们可以逐步剥去剩余图中度为1的边缘结点,在剥完所有结点时,最后被剥去的那层结点就是答案。
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
if(n==1) return {0};
vector<vector<int>> G(n);
for(int i=0;i<n-1;i++){//邻接表存储图
int a = edges[i][0], b = edges[i][1];
G[a].push_back(b);
G[b].push_back(a);
}
vector<int> ans,degree(n);
queue<int> q;
//统计结点的度,并把度为1的结点存进队列
for(int i=0;i<n;i++){
degree[i] = G[i].size();
if(G[i].size()==1) {
q.push(i);
}
}
//向内层结点进行bfs搜索
while(!q.empty()){
//还有内层结点,把存储的上一层结点清空
ans.clear();
int size = q.size();
for(int i=0; i<size; i++){
int root = q.front();
ans.push_back(root);//存储本层结点
--degree[root];//该结点被剥去,度减少为0,避免重复入队
q.pop();
//bfs搜索,把剥去外层结点后度变为1的结点加入队列
for(int j=0; j<G[root].size(); j++){
int node = G[root][j];
--degree[node];
if(degree[node]==1){
q.push(node);
}
}
}
}
return ans;
}
};