目录
一、图论常用方法
1.深度优先搜索(dfs)
dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯,再往黄河走)。
深搜三部曲如下:
- 确认递归函数,参数
- 确认终止条件
- 处理目前搜索节点出发的路径
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
和整数 n
、source
和 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的结点,就需要去讨论正常连接下使得图变成环的边,在并查集中遍历寻找出构成有向环的边(边的两个点根节点相同,再连接就会出现边闭合起来的环)
注意,因为每次判断是否要删除,或者说是否出现闭合起来的环,都需要重新讨论,所以需要每次讨论之前重新初始化一下并查集,以免重复讨论