LeetCode刷题笔记(5):搜索

广度优先搜索(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;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值