图论C++详解(知识点与LeetCode题目解析)

目录

一、图论常用方法

1.深度优先搜索(dfs)

2.广度优先搜索(bfs)

3.并查集

二、图论常见题型LeetCode

1.所有可能的路径 797

2.岛屿数量 200

3.岛屿的最大面积 695

4.飞地的数量 1020

5.被围绕的区域 130

6.太平洋大西洋水流问题417

7.最大人工岛 827

8.单词接龙 127

9.钥匙和房间 841

10.岛屿的周长 463

11.寻找图中是否存在路径 1971

12.冗余连接 684

13.冗余连接2  685


一、图论常用方法

1.深度优先搜索(dfs)

dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯,再往黄河走)。

深搜三部曲如下:

  1. 确认递归函数,参数
  2. 确认终止条件
  3. 处理目前搜索节点出发的路径

2.广度优先搜索(bfs)

bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。

3.并查集

并查集是树形结构。不过,不是二叉树。

可以查询两个节点的根节点,不断向上递归。这样的结构很适合用于判断两个节点是否连接起来会构成环,或者是否存在有效路径。

二、图论常见题型LeetCode

1.所有可能的路径 797

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序

 graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。

示例 1:

输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;//一条路径
    void GetResult(vector<vector<int>>& graph,int x){
        //终止条件--x始终传输的是新数组的索引
        if(x == graph.size()-1){
            //递归到最远的节点
            result.push_back(path);
            return;
        }

        //处理路径--从一个新的数组开始
        for(int i = 0;i < graph[x].size();i++){
            path.push_back(graph[x][i]);
            GetResult(graph,graph[x][i]);
            path.pop_back();//弹出以继续进行递归
        }
    }

public:
    vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
        path.push_back(0);//始终从零节点出发
        GetResult(graph,0);
        return result;
    }
};

题解:

本题采用的就是dfs深度优先搜索。思想就是从0节点开始,不断向之后寻找,直到找到数组的长度限制为止。本题的思路是,从零开始,每次递归先存入当前值入path一维数组,然后将当前值作为索引(有向无环图的性质),再次进行递归。终止条件是当前传入的值(索引)已经超过列表的范围,那么就存入path,向上递归。递归条件上,使用中序遍历。


2.岛屿数量 200

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
class Solution {
private:
    //连接更多形成岛屿
    void MoreConnected(vector<vector<char>>& grid,vector<vector<bool>>& isconnected,int n,int m){
        int dir[4][2]  = {0,-1,0,1,1,0,-1,0};//向下向上向右向左
        for(int i = 0;i<4;i++){
            int movex = n+dir[i][0];//高度
            int movey = m+dir[i][1];//宽度
            if(movex >= grid.size() || movex <0 || movey >= grid[0].size() || movey <0){
                continue;//正确剪枝
            }
            if(isconnected[movex][movey] == false && grid[movex][movey] == '1'){
                //未被连接,并且存在--孤岛
                isconnected[movex][movey] = true;
                MoreConnected(grid,isconnected,movex,movey);
            }
        }

    }
public:
    int numIslands(vector<vector<char>>& grid) {
        int n = grid.size();//高度
        int m = grid[0].size();//宽度
        int result = 0;

        vector<vector<bool>> isconnected(n,vector<bool>(m,false));//初始化都没有连接上

        for(int i=0;i< n;i++){
            for(int j=0;j<m;j++){
                if(isconnected[i][j] == false && grid[i][j] == '1'){
                    //未被连接,并且存在--孤岛
                    isconnected[i][j] = true;
                    result++;
                    MoreConnected(grid,isconnected,i,j);
                }
            }
        }

        return result;
    }
};

题解:

本题需要求岛屿的数量,可以看到岛屿只能水平或竖直方向上构成,所以可以采用递归的思路,建立一个记忆数组,记忆是否走过该岛屿,然后顺序遍历,一旦找到1的元素并且没有被记忆到,说明该为孤岛,就从该元素开始进行水平竖直方向上的4种一步尝试,根据题意去拓展记忆数组。这就是一种深度搜索,本题的递归终止条件由于方向四种是固定的,所以不用特意设置。寻找路径的判断方式,就是是否记忆过和是否为“1”的岛屿板块。


3.岛屿的最大面积 695

给你一个大小为 m x n 的二进制矩阵 grid 。

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

示例 1:

输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1
class Solution {
private:
    int count = 0;
    int dir[4][2] = {0,1,0,-1,1,0,-1,0};
    void GetCount(int x,int y,vector<vector<int>>& grid,vector<vector<bool>>& isVisited){
        for(int i =0;i<4;i++){
            int finalx = x + dir[i][0];//高度
            int finaly = y + dir[i][1];//宽度

            if(finalx <0 || finaly <0 || finalx >= grid.size() || finaly >= grid[0].size()){
                continue;
            }

            if(!isVisited[finalx][finaly] && grid[finalx][finaly] == 1){
                isVisited[finalx][finaly] = true;
                count++;//对当前的岛屿面积进行累加
                GetCount(finalx,finaly,grid,isVisited);
            }
        }
    }
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int n = grid.size();//高度
        int m = grid[0].size();//宽度
        vector<vector<bool>> isVisited(n,vector<bool>(m,false));//定义布尔访问数组
        int result = 0;

        for(int i=0;i<n;i++){
            for(int j = 0;j<m;j++){
                if(!isVisited[i][j] && grid[i][j] == 1){
                    isVisited[i][j] = true;
                    count = 1;
                    GetCount(i,j,grid,isVisited);
                    result = max(result,count);//获得以改地方为起点的岛屿的面积,并且比较出最大的
                }
            }
        }

        return result;
    }        
};

题解:

本题求最大面积,其逻辑还是与岛屿数量相似,仍然是设置一个访问数组来保存点位上的是否访问过,然后从四方向进行访问探索。不同的是需要在每一次开始探索时,重新计数,以来确定岛屿的面积,同时在探索结束后,比较出当前的最大面积,最终返回最大值


4.飞地的数量 1020

给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。

一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。

返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。

示例 1:

输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]
输出:3
解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。
class Solution {
private:
    int count =0;
    int dir[4][2] = {1,0,-1,0,0,-1,0,1};
    void GetCount(vector<vector<int>>& grid,int x,int y){
        grid[x][y]=0;//清空当前的岛屿块
        count++;
        for(int i=0;i<4;i++){
            int finalx = x+dir[i][0];
            int finaly = y+dir[i][1];

            if(finalx < 0||finaly <0 ||finalx >= grid.size()||finaly >= grid[0].size()){
                continue;
            }

            if(grid[finalx][finaly] == 1){
                GetCount(grid,finalx,finaly);
            }
        }
    }
public:
    int numEnclaves(vector<vector<int>>& grid) {
        int n = grid.size();//高
        int m = grid[0].size();//宽

        for(int i=0;i<n;i++){
            if(grid[i][0] == 1){
                //最左侧列
                GetCount(grid,i,0);
            }
            if(grid[i][m-1] == 1){
                //最右侧列
                GetCount(grid,i,m-1);
            }
        }

        for(int i=0;i<m;i++){
            if(grid[0][i] == 1){
                //最上行
                GetCount(grid,0,i);
            }
            if(grid[n-1][i] == 1){
                //最下行
                GetCount(grid,n-1,i);
            }
        }

        count=0;//在去除了四边上的岛屿后,才开始计数
        for(int i =0;i<n;i++){
            for(int j =0;j<m;j++){
                if(grid[i][j] == 1){
                    GetCount(grid,i,j);
                }
            }
        }

        return count;
    }
};

题解:

本题仍然采用深度优先算法,不同的是本题是要求内部的岛屿数量,那么就可以先从四条外边开始遍历,如果找到值为1的岛屿,就向四个方向继续遍历,并且每次遍历前,清空该值为1的岛屿为0,这样就清空掉不用的岛屿。遍历完四边后,剩下的就是内部岛屿,此时将计数器值置为0,则此时开始计数,在全局遍历过后,此时的count就是内部岛屿块的数量


5.被围绕的区域 130

给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。

示例 1:

输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的
class Solution {
private:
    int dir[4][2] = {1,0,-1,0,0,1,0,-1};
    void ChangeBoard(vector<vector<char>>& board,int x,int y){
        board[x][y] = 'A';//先特别赋值为A
        for(int i=0;i<4;i++){
            int finalx = x + dir[i][0];
            int finaly = y + dir[i][1];

            if(finalx <0 || finaly <0 || finalx >= board.size() | finaly >= board[0].size()){
                continue;
            }

            if(board[finalx][finaly] == 'O'){
                ChangeBoard(board,finalx,finaly);
            }
        }
    }
public:
    void solve(vector<vector<char>>& board) {
        int n = board.size();
        int m = board[0].size();

        for(int i=0;i<n;i++){
            if(board[i][0] == 'O'){
                ChangeBoard(board,i,0);
            }
            if(board[i][m-1] == 'O'){
                ChangeBoard(board,i,m-1);
            }
        }

        for(int j=0;j<m;j++){
            if(board[0][j] == 'O'){
                ChangeBoard(board,0,j);
            }
            if(board[n-1][j] == 'O'){
                ChangeBoard(board,n-1,j);
            }
        }

        //在将边上的岛屿O都变成A后
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(board[i][j] == 'O'){
                    board[i][j]='X';
                }
                if(board[i][j] == 'A'){
                    board[i][j]='O';
                }
            }
        }


    }
};

题解:

这题其实也是岛屿问题,不同的在于,该题是需要保留边上的岛屿,去除中间的岛屿。那么可以采用先遍历边上的岛屿,登上岛屿探索,并且把这些岛屿的值赋值为A,这样遍历完四边后,剩下来的岛屿若值还为O,说明一定是内陆岛屿,这时候给这些到岛屿赋值为X即可,这就是剔除了内部岛屿,同时将A值恢复为O即可,这样就实现了有保留有去除的简单岛屿问题解法。

注:

C++不会对X==Y报错,一定记得赋值时候的等号,别写成==了


6.太平洋大西洋水流问题417

有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。

岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。

示例 1:

输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
class Solution {
private:
    int dir[4][2] = {1,0,-1,0,0,1,0,-1};
    void GetVisited(vector<vector<int>>& heights,vector<vector<bool>>& isvisited,int x,int y){
        if(isvisited[x][y]){
            return;//减少消耗,遍历过就不用再次遍历
        }
        isvisited[x][y] =true;


        for(int i =0;i<4;i++){
            int finalx = x+dir[i][0];
            int finaly = y+dir[i][1];
            
            if(finalx <0 || finaly <0 || finalx >= heights.size() || finaly >= heights[0].size()){
                continue;
            }

            if(heights[finalx][finaly] >= heights[x][y]){
                //满足条件,可以由周围向其流
                GetVisited(heights,isvisited,finalx,finaly);
            }
        }

        return;//向上返回
    }
    
public:
    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
        vector<vector<int>> result;//结果数组
        int n = heights.size();
        int m = heights[0].size();
        
        vector<vector<bool>> isPacific(n,vector<bool>(m,false));
        vector<vector<bool>> isAtlantic(n,vector<bool>(m,false));

        for(int i =0;i<n;i++){
            GetVisited(heights,isPacific,i,0);//左列
            GetVisited(heights,isAtlantic,i,m-1);//右列
        }

        for(int i =0;i<m;i++){
            GetVisited(heights,isPacific,0,i);//上行
            GetVisited(heights,isAtlantic,n-1,i);//下行
        }


        for(int i=0;i < n;i++){
            for(int j=0;j<m;j++){
                if(isPacific[i][j] && isAtlantic[i][j]){
                    result.push_back({i,j});
                }
            }
        }

        return result;
    }
};

题解:

本题也可以转换为对边界的讨论(这样会减少内存消耗运行时长)就是从边界向内讨论,这样只需要对边界的每一个点讨论一次就可以,明显减少内存消耗。思路是建立太平洋和大西洋的布尔数组,数组用来保存讨论的结果(就是该位置是否是流向太平洋或大西洋的路线的一部分)。讨论的方式是将传入的点,进行判断,首先判断该节点是否讨论过,接着给对应的数组值赋值为真,如果周围的点比本身要大,那就以周围的点继续进行讨论(这就是在逆流向上),那么只要同时为真的点,就说明其存在顺流向下向到太平洋和大西洋的路径(因为是逆流上来的,一步步赋值为真的)。这样也就构成了最后的判断条件。 


7.最大人工岛 827

给你一个大小为 n x n 二进制矩阵 grid 。最多 只能将一格 0 变成 1 。

返回执行此操作后,grid 中最大的岛屿面积是多少?

岛屿 由一组上、下、左、右四个方向相连的 1 形成。

示例 1:

输入: grid = [[1, 0], [0, 1]]
输出: 3
解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。
class Solution {
private:
    int count;//计数当前的面积大小
    int dir[4][2] = {1,0,-1,0,0,1,0,-1};
    void GetCount(vector<vector<int>>& grid,vector<vector<bool>>& isvisited,int x,int y,int mark){
        //判断还可以继续扩张
        if(isvisited[x][y] || grid[x][y] == 0){
            return;
        }    
    
        isvisited[x][y] = true;
        grid[x][y] = mark;//加上标签
        count++;//面积扩张
    
        for(int i=0;i<4;i++){
            int finalx = x+dir[i][0];
            int finaly = y+dir[i][1];

            if(finalx <0||finaly<0||finalx >= grid.size() ||finaly >=grid[0].size()){
                continue;
            }
            
            GetCount(grid,isvisited,finalx,finaly,mark);
        }
    }
public:
    int largestIsland(vector<vector<int>>& grid) {
        int n = grid.size();
        int m = grid[0].size();

        vector<vector<bool>> isvisited(n,vector<bool>(m,false));
        unordered_map<int,int> gridNum;//岛屿编号和大小
        int mark = 2;//编号由2开始--因为后面会赋值mark

        bool isAll = true;//判断岛屿是不是矩阵大小
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(grid[i][j] == 0){
                    isAll = false;//说明不是矩阵大小
                }
                //对可拓展的岛屿进行拓展
                if(!isvisited[i][j] && grid[i][j] == 1){
                    count =0;
                    GetCount(grid,isvisited,i,j,mark);
                    gridNum[mark] = count;//保存面积
                    mark++;//计数加一
                }
            }
        }

        if(isAll){
            return n*m;//直接返回整个面积
        }

        //遍历0的点,确定哪个点添加更好
        int result = 0;
        unordered_set<int> VisitedMark;
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                //以该点构成岛屿面积,首先是1
                int areaSize = 1;
                VisitedMark.clear();//情空
                if(grid[i][j] == 0){
                    for(int k=0;k<4;k++){
                        int finalx = i+dir[k][0];
                        int finaly = j+dir[k][1];
                        if(finalx <0||finaly<0||finalx >= grid.size() ||finaly >=grid[0].size()){
                            continue;
                        }
                        if(VisitedMark.count(grid[finalx][finaly])){
                            continue;//之前已经存储过该岛屿--mark编号
                        }

                        areaSize += gridNum[grid[finalx][finaly]];//加上该块岛屿的面积
                        VisitedMark.insert(grid[finalx][finaly]);//存储编号
                    }
                }
                result = max(result,areaSize);
            }
        }

        return result;
    }
};

题解:

本题虽然在关键逻辑上与常见岛屿上相似,但是整体上的处理是不同的。题目要求最大的岛屿面积,所以对于该点的选择上一定是遍历,那么可以在遍历该点(值为0)的时候,仍然向四周扩展,如果扩展到存在岛屿的一个点,那么就通过加上这个点所对应整体岛屿的面积,作为将该点作为岛屿点的岛屿面积。(可能存在该点连接不止一个岛屿的情况)

那么就需要使得,岛屿上任意一点的数值对应该岛屿的面积,在这里可以给岛屿标上编号。具体实现方式是给岛屿点的值赋值为编号(编号大于1,为了不和1、0的讨论冲突),然后再用无序表存储编号对应的面积,这里面积就用岛屿问题常见的套路,从该点向四周扩张岛屿面积,扩张到已经扩张过或者该点不可能为岛屿点(0)的时候,保存面积到编号。之后只需要用无序表的索引为该点的值,就可以得到对应的面积。

这里还要讨论一下,给的数组就是一整个岛屿的情况,也就是没有一个0,如果这样的话,就无需讨论,直接返回数组的横纵长度积即可。


8.单词接龙 127

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  •  对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
  • sk == endWord

给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。

示例 1:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        unordered_set<string> wordSet(wordList.begin(),wordList.end());//转化为无序集合,便于查找
        if(wordSet.find(endWord) == wordSet.end()){
            return 0;//在转换序列中都没有,则不可能找到了
        }

        unordered_map<string,int> wordMap;//无序表存储当前单词和长度
        wordMap.insert(pair<string,int>(beginWord,1));
        queue<string> que;//初始队列
        que.push(beginWord);

        while(!que.empty()){
            string word = que.front();
            que.pop();//先进先出
            int path = wordMap[word];//获得其长度
            for(int i=0;i<word.size();i++){
                string newWord = word;//每次重新替换单个单词
                for(int j =0;j<26;j++){
                    newWord[i] = 'a'+j;//替换
                    if(newWord == endWord){
                        return path+1;
                    }
                    if(wordMap.find(newWord) == wordMap.end() && wordSet.find(newWord) != wordSet.end()){
                        //当长度无序表中不存在该元素,但是转换无序集合中存在时
                        wordMap.insert(pair<string,int>(newWord,path+1));//长度无序表加上元素,并且扩充长度
                        que.push(newWord);
                    }
                }
            }
        }

        return 0;
    }
};

题解:

此题需要找到一个序列,满足一个完整的转换过程。由于转换的多变性,优先使用广度搜索,就是将每一层的每个节点遍历一遍,并且保存每一种可能,以寻找到最优的路径。

因为需要返回长度,所以可以设置无序表,保存单词和长度的关系。在寻找的思路上,用队列去存储单词,然后比较单词。只要队列不为空,就去获得当前队列的第一个元素,然后弹出(遍历该长度下的所有可能),为什么说是该长度下呢,因为每一次存入队列中的元素,无序表对应的长度是相同的,并且会在该次广度搜索中,弹出掉当前长度的所有元素。然后以此元素,去对单词的每一个位置进行26次字母替换的讨论,如果替换后与最终目标单词相同就直接返回长度+1(+1的原因是,当前长度是上一次的长度,这里替换过后,是使用了新的转换),如果不同观察该元素是否在提供的转化序列中,如果存在存入队列和无序表,一个是便于下次遍历,一个是便于获得长度。

如果本个元素没有满足条件,并且队列不为空,就会继续使用与其同长度(同层)的元素,继续讨论,如果该长度的元素全部遍历完了,还存在元素(其长度一定是该长度加1)就继续下一层的讨论,如果不存在元素了,说明没有找到转换序列,则返回0,告知无解


9.钥匙和房间 841

有 n 个房间,房间按从 0 到 n - 1 编号。最初,除 0 号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。

当你进入一个房间,你可能会在里面找到一套不同的钥匙,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。

给你一个数组 rooms 其中 rooms[i] 是你进入 i 号房间可以获得的钥匙集合。如果能进入 所有 房间返回 true,否则返回 false

示例 1:

输入:rooms = [[1],[2],[3],[]]
输出:true
解释:
我们从 0 号房间开始,拿到钥匙 1。
之后我们去 1 号房间,拿到钥匙 2。
然后我们去 2 号房间,拿到钥匙 3。
最后我们去了 3 号房间。
由于我们能够进入每个房间,我们返回 true。
class Solution {
private:
    void ForDfs(vector<vector<int>>& rooms, vector<bool>& isVisited,int key){
        if(isVisited[key]){
            return;//遍历过就不再遍历
        }

        //因为不是求路径,只是求是否可以通过钥匙之间的组合到达目的地
        isVisited[key] = true;
        vector<int> keys = rooms[key];//到下一层

        for(int i : keys){
            ForDfs(rooms,isVisited,i);
        }
    }
public:
    bool canVisitAllRooms(vector<vector<int>>& rooms) {
        vector<bool> isVisited(rooms.size(),false);
        ForDfs(rooms,isVisited,0);

        for(int  i : isVisited){
            if(i == false){
                return false;//有节点没有被遍历到
            }
        }
        return true;
    }
};

题解:

该题的题意需要仔细阅读,因为本题并不是求最优路径,只是通过钥匙之间的组合去一个个开房间,那么只需要确认每一个房间都被访问过即可。那么就可以使用深度搜索,思路是设置布尔数组以真假保存每个房间的访问情况,然后设置深度搜索的函数,每次对当前房间访问情况判断,如果访问过(说明已经对该情况讨论过--即从该房间向别的房间走过),就没有必要再次访问。如果没有,获取该房间提供的钥匙集合,一个个使用钥匙递归,同样因为不需要求最短路径,所以不需要回溯。


10.岛屿的周长 463

给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。

网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。

岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

示例 1:

输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
输出:16
解释:它的周长是上面图片中的 16 个黄色的边
class Solution {
public:
    int islandPerimeter(vector<vector<int>>& grid) {
        int sum = 0;//总个数
        int cover = 0;//相邻的陆地
        for(int i=0;i<grid.size();i++){
            for(int j=0;j<grid[0].size();j++){
                if(grid[i][j] == 1){
                    sum++;
                    if(i-1>=0 && grid[i-1][j] == 1){
                        //与上边相邻
                        cover++;
                    }
                    if(j-1>=0 && grid[i][j-1] == 1){
                        //与左边相邻
                        cover++;
                    }
                }
            }
        }

        return sum*4 - cover*2;
    }
};

题解:

本题是通过阅读图,得出如果要求周长,则可以通过将每个方块的四边长相加,最后再减去各个方块与左边上边相邻的边乘2即可。因为重合一条,就会减少两条边,并且只需要讨论左上,就不需要讨论右下了,可以保证各个节点讨论的时候,不会重叠获得不必要的左上边的数量。


11.寻找图中是否存在路径 1971

有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。

请你确定是否存在从顶点 source 开始,到顶点 destination 结束的 有效路径 。

给你数组 edges 和整数 nsource 和 destination,如果从 source 到 destination 存在 有效路径 ,则返回 true,否则返回 false 。

示例 1:

输入:n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
输出:true
解释:存在由顶点 0 到顶点 2 的路径:
- 0 → 1 → 2 
- 0 → 2
class Solution {
private:
    int n = 200005;
    vector<int> father = vector<int>(n,0);
    //并查集初始化
    void init(){
        for(int i=0;i<n;i++){
            father[i]=i;//默认根节点为自己
        }
    }
    //并查集寻找根节点
    int Find(int x){
        return x == father[x] ? x :father[x] = Find(father[x]);
        //如果相同,就直接返回该值,不同就递归去寻找当前根节点的根节点
    }

    void MakeConnect(int a,int b){
        int af = Find(a);
        int bf = Find(b);
        if(af==bf){
            return;
        }else{
            father[bf] = af;//连接,根节点赋值根节点
        }
    }

    bool isSame(int a,int b){
        int af = Find(a);
        int bf = Find(b);
        if(af==bf){
            return true;
        }else{
            return false;
        }
    }
public:
    bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
        //由于每个顶点最多一条边相连,所以不用考虑覆盖的问题,可以使用并查集,保存根与节点的关系
        init();
        for(int i =0;i<edges.size();i++){
            MakeConnect(edges[i][0],edges[i][1]);
        }

        return isSame(source,destination);
    }
};

题解:

由于题目中强调,每个顶点只会以至多一条边连接,并且还要求两点是否存在路径,那么就可以用并查集来寻找两个节点是否存在同一个根节点==是否可以连接。思路是,并查集初始化的时候,每一个元素对应根节点为自身,然后遍历目标数组进行连接操作,连接操作是只要两个传入节点的根节点不相同,就将传入节点的任意一个的根节点被另外一个覆盖,那么这样在寻找该节点的根节点时,就会发现其根节点和另一个一致,那么两者就是可连接也就是存在路径的。(寻找的操作,是一个递归过程,如果当前传入值和数组对应值不一致,那么就以数组对应值作为值再次传入,不断向后寻找根节点,当传入值和对应值相同的时候,说明找到了根节点,并且也会同时刷新该传入值的根节点值,优化查找效率)其实可以抽象为,如果该节点被连接过,那么find根节点的时候一定会经过find该节点被连接过的节点的根节点的过程。所以只需要遍历数组,一个个连接起来,最后判断两个目标值的根节点是否相同即可


12.冗余连接 684

树可以看成是一个连通且 无环 的 无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的那个。

示例 1:

输入: edges = [[1,2], [1,3], [2,3]]
输出: [2,3]
class Solution {
private:
    //构建并查集
    int n = 1002;
    vector<int> father = vector(n,0);

    void init(){
        for(int i=0;i<n;i++){
            father[i] = i;//根节点为自身
        }
    }

    int Find(int x){
        return  x == father[x] ? x : father[x] = Find(father[x]);
    }

    bool isSame(int a,int b){
        int af = Find(a);
        int bf = Find(b);
        if(af == bf){
            return true;
        }else{
            return false;
        }
    }

    void MakeConnect(int a,int b){
        int af = Find(a);
        int bf = Find(b);
        if(af == bf){
            return;
        }else{
            father[bf] = af;//根节点赋值
        }
    }
public:
    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        init();//初始化并查集
        for(int i =0;i<edges.size();i++){
            if(!isSame(edges[i][0],edges[i][1])){
                MakeConnect(edges[i][0],edges[i][1]);
            }else{
                //如果两者已经有同一个根节点了,会造成环
                return edges[i];
            }
        }
        return {};
    }
};

题解:

本题注意题目中的目标条件 ,是形成树,并且树如题中所说,是无环的无向图。那么只需要保存好有向图,并且时刻判断当前两个结点连接后,是否会构成环即可。构成环其实就是两个结点的根节点相同--在并查集中,这是两者连接的方式。所以本题使用并查集是较优的办法。需要注意的是,并查集的大小一般比题目给出的范围大些,防止溢出。


13.冗余连接2  685

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

示例 1:

输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]
class Solution {
private:
    static const int N = 1002;
    int father[N];
    int n;

    void init(){
        for(int i = 0;i<n;i++){
            father[i] = i;
        }
    }

    int Find(int x){
        return x==father[x] ? x :father[x] = Find(father[x]);
    }

    bool isSame(int a, int b){
        int af = Find(a);
        int bf = Find(b);
        if(af == bf){
            return true;
        }else{
            return false;
        }
    }

    void MakeConnect(int a,int b){
        int af = Find(a);
        int bf = Find(b);
        if(af == bf){
            return;
        }else{
            father[bf] = af;//根节点赋值
        }
    }

    bool initAndDelete(vector<vector<int>>& edges,int deleteIndex){
        init();//再次初始化
        for(int i = 0;i<n;i++){
            if(i == deleteIndex){
                continue;
            }
            if(isSame(edges[i][0],edges[i][1])){
                return false;//构成了有向环,在删除了该边的情况下
            }else{
                MakeConnect(edges[i][0],edges[i][1]);
            }  
        }

        return true;
    }

    vector<int> GetSpecial(vector<vector<int>>& edges){
        init();
        for(int i = 0;i< n;i++){
            if(isSame(edges[i][0],edges[i][1])){
                return edges[i];//构成了有向环,则返回该边
            }else{
                MakeConnect(edges[i][0],edges[i][1]);
            } 
        }

        return {};//没有要删除的
    }


public:
    vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
        //1:记录入度为2的点  从0指向1-看图
        int inCount[N] = {0};
        n = edges.size();
        for(int i =0;i<n;i++){
            inCount[edges[i][1]]++;//记录好该点的入度
        }

        //2:记录入度为2的边
        vector<int> inIndex;
        for(int i = n-1;i >= 0;i--){
            if(inCount[edges[i][1]] == 2){
                inIndex.push_back(i);//入度最多为2,因为原来是树,只是加了一条有向边
            }
        }

        //3:处理入度为2的边(其实只会有两条,如果有的话)
        if(inIndex.size() > 0){
            //决定删除哪条边
            if(initAndDelete(edges,inIndex[0])){
                return edges[inIndex[0]];
            }else{
                return edges[inIndex[1]];
            }
        }

        //4:收尾--返回有向环
        return GetSpecial(edges);
        
    }
};

题解:

首先要关注到题目中的“该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成”,这说明了,目标数组是由一个树和一条有向边组成,那么要返回n个节点的树,就一定要删除一条有向边。什么时候要删除有向边,就是有向图变成环的时候。环出现的可能性,有结点入度为2,就一定会导致变成环,还有一种可能入度虽然为1,但是指向了一个元素,使得图变成了环。所以本题的解就要围绕此展开。

首先定义好并查集,然后寻找入度为2的边的结点,接着保存这两条边的索引,去分别判断是否删除该节点后,剩下的不为环(遍历到目标索引continue即可),如果不为环就返回该边。如果没有入度为2的结点,就需要去讨论正常连接下使得图变成环的边,在并查集中遍历寻找出构成有向环的边(边的两个点根节点相同,再连接就会出现边闭合起来的环)

注意,因为每次判断是否要删除,或者说是否出现闭合起来的环,都需要重新讨论,所以需要每次讨论之前重新初始化一下并查集,以免重复讨论

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花火の云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值