文章目录
- ~~~~~~~~~~~~图~~~~~~~~~~~~
- 1034. 边界着色
- **** 图:有向无环图DAG ****
- 207. 课程表
- 210. 课程表 II
- 851. 喧闹和富有
- 802. 找到最终的安全状态
- 797. 所有可能的路径
- 743. 网络延迟时间
- ******** 图:无向图 ********
- 547. 省份数量
- 200. 岛屿数量
- 相似题目:695. 岛屿的最大面积
- 相似题目:463. 岛屿的周长
- 相似题目:130. 被围绕的区域
- 2039. 网络空闲的时刻
- 684. 冗余连接
- 329. 矩阵中的最长递增路径
- ~~~~~~~~~~~~~~~~~~~~~~~~
- 剑指 Offer 13. 机器人的运动范围
- 79. 单词搜索
- 相似题目:212. 单词搜索 II
- 1162. 地图分析
- 相似题目:1765. 地图中的最高点
- 相似题目:994. 腐烂的橘子
- 迷宫问题
图
1034. 边界着色
给你一个大小为 m x n 的整数矩阵 grid ,表示一个网格。另给你三个整数 row、col 和 color 。网格中的每个值表示该位置处的网格块的颜色。
当两个网格块的颜色相同,而且在四个方向中任意一个方向上相邻时,它们属于同一 连通分量 。
连通分量的边界 是指连通分量中的所有与不在分量中的网格块相邻(四个方向上)的所有网格块,或者在网格的边界上(第一行/列或最后一行/列)的所有网格块。
请你使用指定颜色 color 为所有包含网格块 grid[row][col] 的 连通分量的边界 进行着色,并返回最终的网格 grid 。
示例 1:
输入:grid = [[1,1],[1,2]], row = 0, col = 0, color = 3
输出:[[3,3],[3,2]]
示例 2:
输入:grid = [[1,2,2],[2,3,2]], row = 0, col = 1, color = 3
输出:[[1,3,3],[2,3,3]]
示例 3:
输入:grid = [[1,1,1],[1,1,1],[1,1,1]], row = 1, col = 1, color = 2
输出:[[2,2,2],[2,1,2],[2,2,2]]
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 50
1 <= grid[i][j], color <= 1000
0 <= row < m
0 <= col < n
解法1:DFS
思路:
深度优先搜索。使用二维数组 visited 标记访问过的节点。遍历上、下、左、右四个方向上的点。如果下一个点位置越界,或者当前位置与下一个点位置颜色不一样,则对该节点进行染色。
在遍历的过程中注意使用 visited 标记访问过的节点,以免重复遍历。
代码:
class Solution {
public:
int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
vector<vector<int>> colorBorder(vector<vector<int>>& grid, int row, int col, int color) {
vector<vector<int>> result = grid;
vector<vector<int>> visited(grid.size(),vector<int>(grid[0].size(),0));
visited[row][col] = 1;
dfs(grid,result,visited,row,col,color);
return result;
}
void dfs(vector<vector<int>>& grid, vector<vector<int>>& result, vector<vector<int>>& visited,int r, int c, int color){
for(int i = 0;i<4;i++){
int x = dir[i][0] + r, y = dir[i][1] + c;
//下一个位置越界,则当前点在边界,对其进行作色
if(!(x >= 0 && y >= 0 && x <= grid.size() - 1 && y <= grid[0].size() - 1)) {
result[r][c] = color;
continue;
}
//如果访问过,则跳过
if(visited[x][y]) continue;
//如果下一个位置颜色与当前颜色相同,则继续搜索
if(grid[x][y] == grid[r][c]){
visited[x][y] = 1;
dfs(grid,result,visited,x,y,color);
}
//如果下一个位置颜色与当前颜色不相同,则当前位置为连通区域边界,对其进行作色
else{
result[r][c] = color;
}
}
}
};
复杂度分析:
时间复杂度:O(mn),其中 m 和 n 分别是 grid 的行数和列数。在最坏情况下,我们需要访问到grid 中的每个点。
空间复杂度:O(mn)。我们用一个与grid 相同大小的矩阵来存储每个点是否被遍历过,而其他的空间消耗,比如递归均不超过 O(mn)。
**** 图:有向无环图DAG ****
207. 课程表
力扣链接
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
提示:
1 <= numCourses <= 105
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i] 中的所有课程对 互不相同
解法1:拓扑排序(广度优先遍历)
思路:
代码:
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> inDegree(numCourses);//入度数组
unordered_map<int,vector<int>> umap;//邻接表
for(int i = 0;i<prerequisites.size();i++){
inDegree[prerequisites[i][0]]++;//当前课程入度+1
umap[prerequisites[i][1]].push_back(prerequisites[i][0]);//添加依赖他的后续课程
}
queue<int> Que;
//所有入度为0的课程入列
for(int i = 0;i<numCourses;i++){
if(inDegree[i] == 0) Que.push(i);
}
int count = 0;
while(Que.size()){
int selected = Que.front();
Que.pop();
count++;
vector<int> next = umap[selected];
if(next.size()>0){
for(int i = 0;i<next.size();i++){
inDegree[next[i]]--;
if(inDegree[next[i]]==0) Que.push(next[i]);
}
}
}
//if (count == numCourses) return true;
return count == numCourses;
}
};
总结:拓扑排序问题
1.根据依赖关系,构建邻接表、入度数组。
2.选取入度为 0 的数据,根据邻接表,减小依赖它的数据的入度。
3.找出入度变为 0 的数据,重复第 2 步。
4.直至所有数据的入度为 0,得到排序,如果还有数据的入度不为 0,说明图中存在环。
复杂度分析:
时间复杂度 O(N+M): 遍历一个图需要访问所有节点和所有临边,N 和 M 分别为节点数量和临边数量;
空间复杂度 O(N+M): 为建立邻接表所需额外空间,umap长度为 N ,并存储 M 条临边的数据。
解法2:DFS
思路:
代码:
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> flags(numCourses,0);//存储每个节点的状态
vector<vector<int>> vec2(numCourses);//存储每个课程的邻接节点
for(vector<int>& pre:prerequisites){
vec2[pre[1]].push_back(pre[0]);
}
for(int i = 0;i<numCourses;i++){
if(!dfs(vec2,flags,i)) return false;
}
return true;
}
//判断是否有环 有环:false 无环:true
bool dfs(vector<vector<int>>& vec2,vector<int>& flags,int i ){
if(flags[i] == 1) return false;
if(flags[i] == -1) return true;
flags[i] = 1;
for(int course:vec2[i]){
if(!dfs(vec2,flags,course)){
return false;
}
}
flags[i] = -1;//说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索
return true;
}
};
210. 课程表 II
力扣链接
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
示例 3:
输入:numCourses = 1, prerequisites = []
输出:[0]
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
所有[ai, bi] 互不相同
解法1:BFS
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
//BFS
vector<int> inDegree(numCourses,0);//入度数组
unordered_map<int,vector<int>> umap;//邻接表
for(int i = 0;i<prerequisites.size();i++){
inDegree[prerequisites[i][0]]++;
umap[prerequisites[i][1]].push_back(prerequisites[i][0]);
}
queue<int> Que;
vector<int> result;
int count = 0;
for(int i = 0;i<numCourses;i++){
if(inDegree[i] == 0) Que.push(i);
}
while(!Que.empty()){
int course = Que.front();
Que.pop();
count++;
result.push_back(course);
vector<int> nexVec = umap[course];
if(nexVec.size()>0){
for(int i = 0;i<nexVec.size();i++){
inDegree[nexVec[i]]--;
if(inDegree[nexVec[i]] == 0) Que.push(nexVec[i]);
}
}
}
if (count == numCourses) return result;
else result.clear();
return result;
}
};
解法2:DFS
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
//DFS
vector<int> flag(numCourses,0);//储存每个节点的状态
vector<vector<int>> vec2(numCourses);//邻接表
for(vector<int>& pre:prerequisites){
vec2[pre[1]].push_back(pre[0]);
}
vector<int> result;
for(int i = 0;i<numCourses;i++){
if(!dfs(vec2,flag,i,result)){
result.clear();
return result;
}
}
reverse(result.begin(),result.end());
return result;
}
bool dfs(vector<vector<int>>& vec2,vector<int>& flag,int i,vector<int>& result){
if(flag[i] == 1) return false;
if(flag[i] == -1) return true;
flag[i] = 1;
for(int course:vec2[i]){
if(!dfs(vec2,flag,course,result)) return false;
}
flag[i] = -1;
result.push_back(i);
return true;
}
};
851. 喧闹和富有
力扣链接
有一组 n 个人作为实验对象,从 0 到 n - 1 编号,其中每个人都有不同数目的钱,以及不同程度的安静值(quietness)。为了方便起见,我们将编号为 x 的人简称为 "person x "。
给你一个数组 richer ,其中 richer[i] = [ai, bi] 表示 person ai 比 person bi 更有钱。另给你一个整数数组 quiet ,其中 quiet[i] 是 person i 的安静值。richer 中所给出的数据 逻辑自恰(也就是说,在 person x 比 person y 更有钱的同时,不会出现 person y 比 person x 更有钱的情况 )。
现在,返回一个整数数组 answer 作为答案,其中 answer[x] = y 的前提是,在所有拥有的钱肯定不少于 person x 的人中,person y 是最安静的人(也就是安静值 quiet[y] 最小的人)。
示例 1:
输入:richer = [[1,0],[2,1],[3,1],[3,7],[4,3],[5,3],[6,3]], quiet = [3,2,5,4,6,1,7,0]
输出:[5,5,2,5,4,5,6,7]
解释:
answer[0] = 5,
person 5 比 person 3 有更多的钱,person 3 比 person 1 有更多的钱,person 1 比 person 0 有更多的钱。
唯一较为安静(有较低的安静值 quiet[x])的人是 person 7,
但是目前还不清楚他是否比 person 0 更有钱。
answer[7] = 7,
在所有拥有的钱肯定不少于 person 7 的人中(这可能包括 person 3,4,5,6 以及 7),
最安静(有较低安静值 quiet[x])的人是 person 7。
其他的答案也可以用类似的推理来解释。
示例 2:
输入:richer = [], quiet = [0]
输出:[0]
提示:
n == quiet.length
1 <= n <= 500
0 <= quiet[i] < n
quiet 的所有值 互不相同
0 <= richer.length <= n * (n - 1) / 2
0 <= ai, bi < n
ai != bi
richer 中的所有数对 互不相同
对 richer 的观察在逻辑上是一致的
解法1:拓扑排序(BFS)
思路:
代码:
class Solution {
public:
vector<int> loudAndRich(vector<vector<int>>& richer, vector<int>& quiet) {
int n = quiet.size();
vector<int> inDegree(n,0);//入度数组
vector<vector<int>> vec2(n);//领接表
for(vector<int>& rich:richer){
inDegree[rich[1]]++;
vec2[rich[0]].push_back(rich[1]);
}
vector<int> result(n);
queue<int> Que;
for(int i = 0;i<n;i++){
result[i] = i;
if(inDegree[i] == 0) Que.push(i);
}
while(!Que.empty()){
int x = Que.front();
Que.pop();
vector<int> next = vec2[x];
if(next.size()>0){
for(int i = 0;i<next.size();i++){
if(quiet[result[x]] < quiet[result[next[i]]]) {
result[next[i]] = result[x];
}
inDegree[next[i]]--;
if(inDegree[next[i]] == 0) Que.push(next[i]);
}
}
}
return result;
}
};
解法2:DFS
思路:
代码:
class Solution {
public:
vector<int> loudAndRich(vector<vector<int>>& richer, vector<int>& quiet) {
//DFS
vector<int> result(quiet.size(),-1);
vector<vector<int>> vec2(quiet.size());//邻接表
for(vector<int> & rich:richer){
vec2[rich[1]].push_back(rich[0]);
}
for(int i = 0;i<quiet.size();i++){
if(result[i] == -1) dfs(vec2,quiet,result,i);
}
return result;
}
//深度遍历寻找大于等于当前点的财富且比当前点安静的节点
int dfs(vector<vector<int>>& vec2,vector<int>& quiet,vector<int>& result,int i){
if(result[i] != -1) return result[i];
if(vec2[i].size() == 0){
result[i] = i;
return result[i];
}
result[i] = i;
for (int j:vec2[i]){
int id = dfs(vec2,quiet,result,j);
if(quiet[result[i]] > quiet[id]) result[i] = id;
}
return result[i];
}
};
802. 找到最终的安全状态
力扣链接
在有向图中,以某个节点为起始节点,从该点出发,每一步沿着图中的一条有向边行走。如果到达的节点是终点(即它没有连出的有向边),则停止。
对于一个起始节点,如果从该节点出发,无论每一步选择沿哪条有向边行走,最后必然在有限步内到达终点,则将该起始节点称作是 安全 的。
返回一个由图中所有安全的起始节点组成的数组作为答案。答案数组中的元素应当按 升序 排列。
该有向图有 n 个节点,按 0 到 n - 1 编号,其中 n 是 graph 的节点数。图以下述形式给出:graph[i] 是编号 j 节点的一个列表,满足 (i, j) 是图的一条有向边。
示例 1:
Illustration of graph
输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]]
输出:[2,4,5,6]
解释:示意图如上。
示例 2:
输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
输出:[4]
提示:
n == graph.length
1 <= n <= 104
0 <= graph[i].length <= n
graph[i] 按严格递增顺序排列。
图中可能包含自环。
图中边的数目在范围 [1, 4 * 104] 内。
解法1:拓扑排序(BFS)
思路:
(1) 题目解析
题目中关键的一句:对于一个起始节点,如果从该节点出发,无论每一步选择沿哪条有向边行走,最后必然在有限步内到达终点,则将该起始节点称作是安全的。
也就是说,对于某一个节点,如果它当前在某个环内,或者有可能走到某个环上,那么它就是不安全的,因为如果遇到环,就无法在有限步内到达终点。
我们结合样例一来看:
输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]]
输出:[2,4,5,6]
其中输入的graph数组中的元素代表了各点的指向情况,例如第一个元素[1,2]就表示以节点0为起点的边有两条,分别指向节点1和节点2。
输出为[2,4,5,6],首先图中节点5和6都是出度为 0 的节点,他们本身就是终点,而2和4的情况相同,他们的出度都为 1,且都指向节点5,所以他们只能通过这条边走向终点5。
(2)为什么要用拓扑排序,以及什么是拓扑排序?
代码:
class Solution {
public:
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
int n = graph.size();
//反图,邻接表存储
vector<vector<int>> vec2(n);
//入度数组 原图记录的节点出度,在反图中就是入度
vector<int> inDegree(n,0);
for(int i = 0;i<n;i++){
for(int x:graph[i]){
vec2[x].push_back(i);
}
inDegree[i] = graph[i].size();
}
queue<int> Que;
for(int i = 0;i<n;i++){
if(inDegree[i] == 0) Que.push(i);
}
while(!Que.empty()){
int selected = Que.front();
Que.pop();
vector<int> next = vec2[selected];
for(int i = 0;i<next.size();i++){
inDegree[next[i]]--;
if(inDegree[next[i]] == 0) Que.push(next[i]);
}
}
vector<int> res;
for(int i = 0;i<n;i++){
if(inDegree[i] == 0) res.push_back(i);
}
return res;
}
};
复杂度分析:
时间复杂度:O(n+m),n为节点个数,m为边的条数。
空间复杂度:O(n+m),用于存图。
解法2:DFS
思路:
这题就是一个 dfs,然后将可以到达顶点的加入结果集,不能到达的(有环)的放弃加入结果集即可。
代码:
class Solution {
public:
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
//DFS
int n = graph.size();
//0没有被访问 -1已经被其他节点启动的DFS访问且安全 1已经被当前节点启动的DFS访问
vector<int> flags(n,0);
vector<vector<int>> vec2(n);
for(int i=0;i<n;i++){
for(int x:graph[i]){
vec2[i].push_back(x);
}
}
vector<int> result;
for(int i = 0;i<n;i++){
if(dfs(vec2,flags,i)){
result.push_back(i);
}
}
return result;
}
//判断是否有环
bool dfs(vector<vector<int>>& vec2,vector<int>& flags,int i){
if(flags[i] == 1) return false;
if(flags[i] == -1) return true;
flags[i] = 1;
for(int j:vec2[i]){
if(!dfs(vec2,flags,j)) return false;
}
flags[i] = -1;
return true;
}
};
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
示例 2:
输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
提示:
n == graph.length
2 <= n <= 15
0 <= graph[i][j] < n
graph[i][j] != i(即不存在自环)
graph[i] 中的所有元素 互不相同
保证输入为 有向无环图(DAG)
解法1:回溯+DAG
思路:
这是一道基本的深度优先搜索的题目,由于题目说该图是一个有向无环图,因此从起始顶点开始,按照某条路径遍历下去不可能回到某个已经遍历过的节点,于是我们在遍历的时候并不需要记录已经遍历过的节点。
具体步骤:
从0号节点开始,进行深度优先搜索,并且将访问顶点加入列表,如果当前节点为n-1号节点,将该列表加入结果集。
代码:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void dfs(vector<vector<int>>& graph, int startIndex, int n){
if(startIndex == n){
path.push_back(startIndex);
res.push_back(path);
return;
}
path.push_back(startIndex);
for(int i = 0;i<graph[startIndex].size();i++){
dfs(graph,graph[startIndex][i],n);
path.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
dfs(graph,0,graph.size()-1);
return res;
}
};
743. 网络延迟时间
力扣链接
有 n 个网络节点,标记为 1 到 n。
给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。
示例 1:
输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2
示例 2:
输入:times = [[1,2,1]], n = 2, k = 1
输出:1
示例 3:
输入:times = [[1,2,1]], n = 2, k = 2
输出:-1
提示:
1 <= k <= n <= 100
1 <= times.length <= 6000
times[i].length == 3
1 <= ui, vi <= n
ui != vi
0 <= wi <= 100
所有 (ui, vi) 对都 互不相同(即,不含重复边)
解法1:迪杰斯特拉+最短路径
- 什么是迪杰斯特拉算法
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,它用于计算一个顶点到其他顶点的最短路径。它的主要特点是:以起始点为中心向外层层扩展(广度优先搜索思想), 直到扩展到终点。
迪杰斯特拉的基本思想如下:
把图的顶点集合划分为两个集合 S 和 V-S。第一个集合 S 表示距源点最短距离已经确定的顶点集,即一个顶点如果属于集合 S 则说明从源点 s 到该顶点的最短路径已知。其余的顶点放在另一个集合 V-S 中。
初始时,集合 S 只包含源点,即 S = { s },这时只有源点到自己的最短距离是已知的。设 v 是 V 中的某个顶点,把从源点 s 到顶点 v 且中间只经过集合 S 中顶点的路径称为从源点到 v 的特殊路径,并用数组 D 来记录当前所找到的从源点 s 到每个顶点的最短特殊路径长度。
从尚未确定最短路径长度的集合 V-S 中取出一个最短特殊路径长度最小的顶点 u,将 u 加入集合 S,同时修改数组 D 中由 s 可达的最短路径长度。
代码:
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
typedef pair<int, int> pair;//距离-当前终点
vector<vector<pair>> graph(n+1);//邻接表
vector<int> dis(n+1,INT_MAX);//距离数组
priority_queue<pair,vector<pair>,greater<pair>> pq;//最小堆来寻找「未确定节点」中与起点距离最近的点
for(auto& t:times) graph[t[0]].push_back({t[2],t[1]});
dis[k] = 0;
pq.push({0,k});
while(!pq.empty()){
auto [dist,endpoint] = pq.top();
pq.pop();
//if(dist > dis[endpoint]) continue;
for(auto& e:graph[endpoint]){
int d = dist + e.first;
int point = e.second;
if(d < dis[point]){
dis[point] = d;
pq.push({d,point});
}
}
}
int res = 0;
for(int i = 1;i<=n;i++){
if(dis[i] == INT_MAX){
return -1;
}
res = max(res,dis[i]);
}
return res;
}
};
******** 图:无向图 ********
1.并查集介绍
基本概念
- 并查集是一种数据结构
- 并查集这三个字,一个字代表一个意思。
- 并(Union),代表合并
- 查(Find),代表查找
- 集(Set),代表这是一个以字典为基础的数据结构,它的基本功能是合并集合中的元素,查找集合中的元素
- 并查集的典型应用是有关连通分量的问题
- 并查集解决单个问题(添加,合并,查找)的时间复杂度都是O(1)
- 因此,并查集可以应用到在线算法中
并查集跟树有些类似,只不过她跟树是相反的。在树这个数据结构里面,每个节点会记录它的子节点。在并查集里,每个节点会记录它的父节点。
合并:
如果节点是相互连通的(从一个节点可以到达另一个节点),那么他们在同一棵树里,或者说在同一个集合里,或者说他们的祖先是相同的。
如果选中的一条边(2,4)的两个顶点在同一个集合里面,说明存在环
2.并查集模板
class UnionFind{
public:
int find(int x){//查找祖先节点
int root = x;//初始化祖先为自己
while(father[root] != -1){//如果节点的父节点不为空,那就不断迭代。
root = father[root];
}
while(x != root){//路径压缩
int original_father = father[x];
father[x] = root;
x = original_father;
}
return root;
}
bool is_connected(int x,int y){
return find(x) == find(y);
}
void merge(int x,int y){
int root_x = find(x);
int root_y = find(y);
if(root_x != root_y){
father[root_x] = root_y;
}
}
void add(int x){
if(!father.count(x)){
father[x] = -1;
}
}
private:
// 记录父节点
unordered_map<int,int> father;
};
547. 省份数量
力扣链接
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
提示:
1 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j] 为 1 或 0
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
解法1:并查集
思路:
计算连通分量数的另一个方法是使用并查集。初始时,每个城市都属于不同的连通分量。遍历矩阵isConnected,如果两个城市之间有相连关系,则它们属于同一个连通分量,对它们进行合并。
代码:
class UnionFind{
public:
int find(int x){
int root = x;
while(father[root] != -1){
root = father[root];
}
//路径压缩
while(x!=root){
int original_father = father[x];
father[x] = root;
x = original_father;
}
return root;
}
void merge(int x, int y){
int root_x = find(x);
int root_y = find(y);
if(root_x != root_y){
father[root_x] = root_y;
num_of_sets--;
}
}
void add(int x){
if(!father.count(x)){
father[x] = -1;
num_of_sets++;
}
}
int get_num_of_sets(){
return num_of_sets;
}
private:
//记录父节点
unordered_map<int,int> father;//son:father
//记录集合数量
int num_of_sets = 0;
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
UnionFind uf;
for(int i = 0;i<isConnected.size();i++){
uf.add(i);
for(int j = 0;j<i;j++){
if(isConnected[i][j]){
uf.merge(i,j);
}
}
}
return uf.get_num_of_sets();
}
};
解法2:BFS
思路:
对于每个城市,如果该城市尚未被访问过,则从该城市开始广度优先搜索,直到同一个连通分量中的所有城市都被访问到,即可得到一个省份。
代码:
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size();
int result = 0;
vector<int> visited(n,0);
for(int i = 0;i<n;i++){
if(!visited[i]){
queue<int> Que;
Que.push(i);
result++;
while(!Que.empty()){
int x = Que.front();
Que.pop();
visited[x] = 1;
for(int k = 0;k<n;k++){
if(isConnected[x][k] == 1 && !visited[k]) Que.push(k);
}
}
}
}
return result;
}
};
复杂度分析:
时间复杂度:O(n^2),其中 n 是城市的数量。需要遍历矩阵 isConnected 中的每个元素。
空间复杂度:O(n),其中 n 是城市的数量。需要使用数组 visited 记录每个城市是否被访问过,数组长度是 n,广度优先搜索使用的队列的元素个数不会超过 n。
解法3:DFS
思路:
深度优先搜索的思路是很直观的。遍历所有城市,对于每个城市,如果该城市尚未被访问过,则从该城市开始深度优先搜索,通过矩阵isConnected 得到与该城市直接相连的城市有哪些,这些城市和该城市属于同一个连通分量,然后对这些城市继续深度优先搜索,直到同一个连通分量的所有城市都被访问到,即可得到一个省份。遍历完全部城市以后,即可得到连通分量的总数,即省份的总数。
代码:
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size();
vector<int> visited(n,0);
int result = 0;
for(int i=0;i<n;i++){
if(!visited[i]){
dfs(isConnected,visited,i);
result++;
}
}
return result;
}
//是否被访问过,深度优先
void dfs(vector<vector<int>>& isConnected,vector<int>& visited,int i){
for(int j = 0;j<isConnected.size();j++){
if(isConnected[i][j] == 1 && !visited[j]){
visited[j] = 1;
dfs(isConnected,visited,j);
}
}
}
};
复杂度分析:
时间复杂度:O(n^2),其中 n 是城市的数量。需要遍历矩阵 n 中的每个元素。
空间复杂度:O(n),其中 n 是城市的数量。需要使用数组visited 记录每个城市是否被访问过,数组长度是 n,递归调用栈的深度不会超过 n
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
示例 2:
输入:grid = [
[“1”,“1”,“0”,“0”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“1”,“0”,“0”],
[“0”,“0”,“0”,“1”,“1”]
]
输出:3
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] 的值为 ‘0’ 或 ‘1’
解法1:DFS
思路:
我们可以将二维网格看成一个无向图,竖直或水平相邻的 1 之间有边相连。
为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。
最终岛屿的数量就是我们进行深度优先搜索的次数。
代码:
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
//DFS
int result = 0;
int n = grid.size();
int m = grid[0].size();
vector<vector<bool>> visited(n,vector<bool>(m,false));
for(int i = 0;i<n;i++){
for(int j = 0;j<m;j++){
if(!visited[i][j] && grid[i][j] == '1'){
dfs(grid,visited,i,j);
result++;
}
}
}
return result;
}
void dfs(vector<vector<char>>& grid,vector<vector<bool>>& visited,int i ,int j){
if(i<0||j<0||i>=grid.size()||j>=grid[0].size()||grid[i][j]=='0'||visited[i][j]) return;
visited[i][j] = true;
dfs(grid,visited,i+1,j);
dfs(grid,visited,i,j+1);
dfs(grid,visited,i-1,j);
dfs(grid,visited,i,j-1);
}
};
复杂度分析:
时间复杂度:O(MN),其中 M 和 N 分别为行数和列数。
空间复杂度:O(MN),在最坏情况下,整个网格均为陆地,深度优先搜索的深度达到 MN。
解法2:BFS
思路:
同样地,我们也可以使用广度优先搜索代替深度优先搜索。
为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则将其加入队列,开始进行广度优先搜索。在广度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。直到队列为空,搜索结束。
最终岛屿的数量就是我们进行广度优先搜索的次数。
代码:
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
//BFS
int result = 0;
int m = grid.size();
int n = grid[0].size();
for(int r = 0;r<m;r++){
for(int c= 0;c<n;c++){
if(grid[r][c] == '1'){
result++;
grid[r][c] = '0';
queue<pair<int,int>> Que;
Que.push({r,c});
while(!Que.empty()){
auto x = Que.front();
Que.pop();
int row = x.first, col = x.second;
if(row-1>=0 && grid[row-1][col] == '1'){
Que.push({row-1,col});
grid[row-1][col] = '0';
}
if(col-1>=0 && grid[row][col-1] == '1'){
Que.push({row,col-1});
grid[row][col-1] = '0';
}
if(row+1<m && grid[row+1][col] == '1'){
Que.push({row+1,col});
grid[row+1][col] = '0';
}
if(col+1<n && grid[row][col+1] == '1'){
Que.push({row,col+1});
grid[row][col+1] = '0';
}
}
}
}
}
return result;
}
};
相似题目:695. 岛屿的最大面积
力扣链接
给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
示例 1:
- 岛屿的最大面积
给你一个大小为 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 。
示例 2:
输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 50
grid[i][j] 为 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 。
示例 2:
输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 50
grid[i][j] 为 0 或 1
解法1:DFS
class Solution {
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
//vector<vector<int>> isVisited(m,vector<int>(n,0));
int res = 0;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(grid[i][j] == 1 ){
res = max(dfs(grid,i,j),res);
}
}
}
return res;
}
int dfs(vector<vector<int>>& grid,int i, int j){
if(i < 0 || j<0 || i>=grid.size() || j >= grid[0].size() || grid[i][j] == 0) return 0;
grid[i][j] = 0;
int cnt = 1;
cnt += dfs(grid,i+1,j);
cnt += dfs(grid,i-1,j);
cnt += dfs(grid,i,j+1);
cnt += dfs(grid,i,j-1);
return cnt;
}
};
解法2:BFS
class Solution {
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
//BFS
int result = 0;
int m = grid.size(), n = grid[0].size();
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(grid[i][j] == 1){
queue<pair<int,int>> que;
que.push({i,j});
int area = 1;
grid[i][j] = 0;
while(!que.empty()){
auto x = que.front();
que.pop();
int row = x.first, col = x.second;
if(row-1>=0 && grid[row-1][col] == 1){
que.push({row-1,col});
grid[row-1][col] = 0;
area++;
}
if(row+1<m && grid[row+1][col] == 1){
que.push({row+1,col});
grid[row+1][col] = 0;
area++;
}
if(col-1>=0 && grid[row][col-1] == 1){
que.push({row,col-1});
grid[row][col-1] = 0;
area++;
}
if(col+1<n && grid[row][col+1] == 1){
que.push({row,col+1});
grid[row][col+1] = 0;
area++;
}
}
result = max(result,area);
}
}
}
return result;
}
};
相似题目: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 个黄色的边
示例 2:
输入:grid = [[1]]
输出:4
示例 3:
输入:grid = [[1,0]]
输出:4
提示:
row == grid.length
col == grid[i].length
1 <= row, col <= 100
grid[i][j] 为 0 或 1
解法1:DFS
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(grid[i][j] == 1)
return dfs(grid,i,j);
}
}
return 0;
}
int dfs(vector<vector<int>>& grid, int i, int j){
if(i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] == 0){
return 1;
}
if(grid[i][j] == 2) return 0;
grid[i][j] = 2;
return dfs(grid,i+1,j) + dfs(grid,i,j+1) + dfs(grid,i-1,j) + dfs(grid,i,j-1);
}
};
解法2:BFS
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
const int dx[] = {1,-1,0,0};
const int dy[] = {0,0,1,-1};
int m = grid.size(), n = grid[0].size();
int result = 0;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(grid[i][j]==1){
for(int k = 0;k<4;k++){
int x = i + dx[k];
int y = j + dy[k];
if(x<0 || x>= m || y < 0 || y>=n || grid[x][y] == 0) result++;
}
}
}
}
return result;
}
};
相似题目: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’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
示例 2:
输入:board = [[“X”]]
输出:[[“X”]]
提示:
m == board.length
n == board[i].length
1 <= m, n <= 200
board[i][j] 为 ‘X’ 或 ‘O’
解法1:DFS
class Solution {
public:
//DFS
void solve(vector<vector<char>>& board) {
for(int i = 0;i<board.size();i++){
dfs(board,i,0);
dfs(board,i,board[0].size()-1);
}
for(int i = 1;i<board[0].size()-1;i++){
dfs(board,0,i);
dfs(board,board.size()-1,i);
}
for(int i = 0;i<board.size();i++){
for(int j = 0;j<board[0].size();j++){
if(board[i][j] == 'A') board[i][j] = 'O';
else if(board[i][j] == 'O') board[i][j] = 'X';
}
}
}
void dfs(vector<vector<char>>& board,int i, int j){
if(i < 0 || j < 0 || i >= board.size() || j >= board[0].size() || board[i][j] != 'O') return;
board[i][j] = 'A';
dfs(board,i+1,j);
dfs(board,i-1,j);
dfs(board,i,j+1);
dfs(board,i,j-1);
}
};
2039. 网络空闲的时刻
给你一个有 n 个服务器的计算机网络,服务器编号为 0 到 n - 1 。同时给你一个二维整数数组 edges ,其中 edges[i] = [ui, vi] 表示服务器 ui 和 vi 之间有一条信息线路,在 一秒 内它们之间可以传输 任意 数目的信息。再给你一个长度为 n 且下标从 0 开始的整数数组 patience 。
题目保证所有服务器都是 相通 的,也就是说一个信息从任意服务器出发,都可以通过这些信息线路直接或间接地到达任何其他服务器。
编号为 0 的服务器是 主 服务器,其他服务器为 数据 服务器。每个数据服务器都要向主服务器发送信息,并等待回复。信息在服务器之间按 最优 线路传输,也就是说每个信息都会以 最少时间 到达主服务器。主服务器会处理 所有 新到达的信息并 立即 按照每条信息来时的路线 反方向 发送回复信息。
在 0 秒的开始,所有数据服务器都会发送各自需要处理的信息。从第 1 秒开始,每 一秒最 开始 时,每个数据服务器都会检查它是否收到了主服务器的回复信息(包括新发出信息的回复信息):
如果还没收到任何回复信息,那么该服务器会周期性 重发 信息。数据服务器 i 每 patience[i] 秒都会重发一条信息,也就是说,数据服务器 i 在上一次发送信息给主服务器后的 patience[i] 秒 后 会重发一条信息给主服务器。
否则,该数据服务器 不会重发 信息。
当没有任何信息在线路上传输或者到达某服务器时,该计算机网络变为 空闲 状态。
请返回计算机网络变为 空闲 状态的 最早秒数 。
示例 1:
输入:edges = [[0,1],[1,2]], patience = [0,2,1]
输出:8
解释:
0 秒最开始时,
- 数据服务器 1 给主服务器发出信息(用 1A 表示)。
- 数据服务器 2 给主服务器发出信息(用 2A 表示)。
1 秒时,
- 信息 1A 到达主服务器,主服务器立刻处理信息 1A 并发出 1A 的回复信息。
- 数据服务器 1 还没收到任何回复。距离上次发出信息过去了 1 秒(1 < patience[1] = 2),所以不会重发信息。
- 数据服务器 2 还没收到任何回复。距离上次发出信息过去了 1 秒(1 == patience[2] = 1),所以它重发一条信息(用 2B 表示)。
2 秒时,
- 回复信息 1A 到达服务器 1 ,服务器 1 不会再重发信息。
- 信息 2A 到达主服务器,主服务器立刻处理信息 2A 并发出 2A 的回复信息。
- 服务器 2 重发一条信息(用 2C 表示)。
…
4 秒时, - 回复信息 2A 到达服务器 2 ,服务器 2 不会再重发信息。
…
7 秒时,回复信息 2D 到达服务器 2 。
从第 8 秒开始,不再有任何信息在服务器之间传输,也不再有信息到达服务器。
所以第 8 秒是网络变空闲的最早时刻。
示例 2:
输入:edges = [[0,1],[0,2],[1,2]], patience = [0,10,10]
输出:3
解释:数据服务器 1 和 2 第 2 秒初收到回复信息。
从第 3 秒开始,网络变空闲。
提示:
n == patience.length
2 <= n <= 105
patience[0] == 0
对于 1 <= i < n ,满足 1 <= patience[i] <= 105
1 <= edges.length <= min(105, n * (n - 1) / 2)
edges[i].length == 2
0 <= ui, vi < n
ui != vi
不会有重边。
每个服务器都直接或间接与别的服务器相连。
解法1:BFS+图
思路:
大体思路如下:
求出每台服务器到主服务器的距离;
算出每台服务器最后一条消息发送的时间;
最后一条消息发送的时间加上两倍的距离就是最后一条消息到达的时间;
取最晚到达的时间 加上 一,就是题目的结果。
代码:
class Solution {
public:
int networkBecomesIdle(vector<vector<int>>& edges, vector<int>& patience) {
int m = edges.size(), n = patience.size(), res = INT_MIN;
vector<vector<int>> graph(n);
vector<int> dis(n,0);
vector<bool> isVisited(n,false);
//建图
for(int i = 0;i<m;i++){
graph[edges[i][0]].push_back(edges[i][1]);
graph[edges[i][1]].push_back(edges[i][0]);
}
//求最短路径
queue<int> que;
que.push(0);
while(!que.empty()){
int
int cur = que.front();
que.pop();
vector<int> vec(graph[cur]);
for(int v:vec){
if(isVisited[v]) continue;
isVisited[v] = true;
dis[v] = dis[cur] + 1;
que.push(v);
res = max(res, (dis[v]*2-1)/patience[v]*patience[v] + dis[v]*2);
}
}
return res + 1;
}
};
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]
示例 2:
输入: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
提示:
n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ai < bi <= edges.length
ai != bi
edges 中无重复元素
给定的图是连通的
解法1:并查集
思路:
代码:
class Solution {
public:
int find(vector<int>& father, int x){
if(father[x] != x){
father[x] = find(father, father[x]);
}
return father[x];
}
void merge(vector<int>& father,int x1, int x2){
int root_x1 = find(father, x1);
int root_x2 = find(father, x2);
if(root_x1 != root_x2){
father[root_x1] = root_x2;
}
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int n = edges.size();
vector<int> father(n+1);
for(int i = 1;i<=n;i++){
father[i] = i;
}
for(auto& edge:edges){
int node1 = edge[0], node2 = edge[1];
if(find(father,node1) != find(father,node2)){
merge(father,node1,node2);
}else{
return edge;
}
}
return {};
}
};
class Solution {
public:
int find(unordered_map<int, int>& father,int x){
int root = x;
while(father[root] != -1){
root = father[root];
}
return root;
}
void merge(unordered_map<int, int>& father,int x, int y){
int root_x = find(father,x);
int root_y = find(father,y);
if(root_x != root_y){
father[root_x] = root_y;
}
}
void add(unordered_map<int, int>& father,int x){
if(!father.count(x)){
father[x] = -1;
}
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int n = edges.size();
unordered_map<int, int> father;
for(int i = 0;i<=n;i++) add(father,i);
for(auto& edge:edges){
int node1 = edge[0], node2 = edge[1];
if(find(father,node1) != find(father,node2)){
merge(father,node1,node2);
}else{
return edge;
}
}
return {};
}
};
329. 矩阵中的最长递增路径
给定一个 m x n 整数矩阵 matrix ,找出其中 最长递增路径 的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外(即不允许环绕)。
示例 1:
输入:matrix = [[9,9,4],[6,6,8],[2,1,1]]
输出:4
解释:最长递增路径为 [1, 2, 6, 9]。
示例 2:
输入:matrix = [[3,4,5],[3,2,6],[2,2,1]]
输出:4
解释:最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。
示例 3:
输入:matrix = [[1]]
输出:1
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 200
0 <= matrix[i][j] <= 231 - 1
通过次数77,976提交次数154,026
解法1:DFS
class Solution {
public:
int longestIncreasingPath(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> isVisited(m,vector<int>(n,0));//计算以每个节点开头的递增序列的长度
int res = 0;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
res = max(res,dfs(matrix,isVisited,i,j,INT_MIN));
}
}
return res;
}
int dfs(vector<vector<int>>& matrix, vector<vector<int>>& isVisited, int i, int j, int pre){
if(i < 0||j<0||i>=matrix.size()||j>=matrix[0].size()||matrix[i][j] <= pre) return 0;//如果之前已经计算过,直接返回即可
if(isVisited[i][j] > 0) return isVisited[i][j];
int l = dfs(matrix,isVisited,i+1,j,matrix[i][j]);
int r = dfs(matrix,isVisited,i-1,j,matrix[i][j]);
int u = dfs(matrix,isVisited,i,j+1,matrix[i][j]);
int d = dfs(matrix,isVisited,i,j-1,matrix[i][j]);
isVisited[i][j] = max(max(l,r),max(u,d))+1;
return isVisited[i][j];
}
};
~~~~~~~~~~~~~~~~~~~~~~~~
剑指 Offer 13. 机器人的运动范围
力扣链接
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20
解法1:DFS
class Solution {
public:
bool isCan(int x, int y, int k){
int num = 0;
while(x>0){
num += x % 10;
x /= 10;
}
while(y>0){
num += y % 10;
y /= 10;
}
if(num > k) return false;
return true;
}
int sum = 0;
int movingCount(int m, int n, int k) {
vector<vector<bool>> visited(m,vector<bool>(n,false));
dfs(visited,m,n,k,0,0);
return sum;
}
void dfs(vector<vector<bool>>& visited, int m, int n, int k, int i, int j){
if(i < 0||i>=m || j<0 || j >= n || visited[i][j] == true || isCan(i, j, k) == false) return;
sum++;
visited[i][j] = true;
dfs(visited,m,n,k,i+1,j);
//dfs(visited,m,n,k,i-1,j);
dfs(visited,m,n,k,i,j+1);
//dfs(visited,m,n,k,i,j-1);
}
};
解法2:BFS
class Solution {
public:
bool isCan(int x, int y, int k){
int num = 0;
while(x>0){
num += x % 10;
x /= 10;
}
while(y>0){
num += y % 10;
y /= 10;
}
if(num > k) return false;
return true;
}
int movingCount(int m, int n, int k) {
vector<vector<bool>> visited(m,vector<bool>(n,false));
queue<pair<int,int>> que;
que.push({0,0});
visited[0][0] = true;
int sum = 1;
const int dx[2] = {0,1};
const int dy[2] = {1,0};
while(!que.empty()){
auto [x,y] = que.front();
que.pop();
for(int i = 0;i<2;i++){
int a = x + dx[i];
int b = y + dy[i];
if(a<0 || a>=m || b<0 || b>=n || visited[a][b] || !isCan(a, b, k)) continue;
visited[a][b] = true;
sum++;
que.push({a,b});
}
}
return sum;
}
};
解法3:动态规划
class Solution {
public:
bool isCan(int x, int y, int k){
int num = 0;
while(x>0){
num += x % 10;
x /= 10;
}
while(y>0){
num += y % 10;
y /= 10;
}
if(num > k) return false;
return true;
}
int movingCount(int m, int n, int k) {
vector<vector<int>> visited(m,vector<int>(n,0));
visited[0][0] = 1;
int sum = 1;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if((i == 0 && j == 0) || isCan(i,j,k) == false) continue;
if(i-1>=0) visited[i][j] |= visited[i-1][j];
if(j-1>=0) visited[i][j] |= visited[i][j-1];
sum += visited[i][j];
}
}
return sum;
}
};
79. 单词搜索
力扣链接
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “SEE”
输出:true
示例 3:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCB”
输出:false
提示:
m == board.length
n = board[i].length
1 <= m, n <= 6
1 <= word.length <= 15
board 和 word 仅由大小写英文字母组成
进阶:你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?
解法1:回溯+DFS
思路:
以每个点作为起点,开始上下左右深度搜索
代码:
class Solution {
private:
bool flag = false;
public:
bool exist(vector<vector<char>>& board, string word) {
int m = board.size(), n = board[0].size();
vector<vector<int>> visited(m,vector<int>(n,0));
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
backtracking(board, word, visited ,i, j, 0);
}
}
return flag;
}
void backtracking(vector<vector<char>>& board, string& word, vector<vector<int>>& visited,int i, int j, int index){
if(index == word.size()) {
flag = true;
return;
}
if( i >= board.size() || j >= board[0].size() || i < 0 || j < 0 || board[i][j]!=word[index] || visited[i][j]) return;//剪枝
visited[i][j] = 1;
backtracking(board, word, visited, i+1, j, index+1);
backtracking(board, word, visited, i-1, j, index+1);
backtracking(board, word, visited, i, j+1, index+1);
backtracking(board, word, visited, i, j-1, index+1);
visited[i][j] = 0;//回溯
}
};
相似题目:212. 单词搜索 II
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words, 返回所有二维网格上的单词 。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例 1:
输入:board = [[“o”,“a”,“a”,“n”],[“e”,“t”,“a”,“e”],[“i”,“h”,“k”,“r”],[“i”,“f”,“l”,“v”]], words = [“oath”,“pea”,“eat”,“rain”]
输出:[“eat”,“oath”]
示例 2:
输入:board = [[“a”,“b”],[“c”,“d”]], words = [“abcb”]
输出:[]
提示:
m == board.length
n == board[i].length
1 <= m, n <= 12
board[i][j] 是一个小写英文字母
1 <= words.length <= 3 * 104
1 <= words[i].length <= 10
words[i] 由小写英文字母组成
words 中的所有字符串互不相同
解法1:回溯+DFS+字典树
思路:
字典树,是一种是一种可以快速插入和搜索字符串的数据结构,有了它我们可以尽快的进行剪枝。
我们将字典的信息全部转化到字典树上,只有出现在字典树上的路径,才应该被纳入到我们的搜索空间里。
可以参考代码注释理解哦,和dfs的过程其实是一样的。只是搜索的时候不是比较target的pos是否匹配,而是比较当前board的字符是否出现在当前trie节点的子节点中。如果遇到了isWord节点,我们就需要将字符串加入最终的结果集中。
代码:
struct Trie
{
string val;
bool isEnd;
vector<Trie*> next;
Trie(){
this->isEnd = false;
this->next = vector<Trie*>(26,nullptr);
this->val = "";
}
};
class Solution {
public:
vector<string> result;
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
// build trie tree
Trie* root = new Trie();
for(auto& word:words){
Trie* p = root;
for(auto& c:word){
//判断是否存在该字符的节点,不存在则创建
if(p->next[c-'a'] == nullptr){
p->next[c-'a'] = new Trie();
}
p = p->next[c-'a'];
}
p->isEnd = true;
p->val = word;
}
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++){
backtracking(board, visited, i, j, root);
}
}
return result;
}
void backtracking(vector<vector<char>>& board, vector<vector<bool>>& visited, int i, int j, Trie* cur){
if(i<0 || i >= board.size() || j<0 || j>= board[0].size() || visited[i][j]){
return;
}//剪枝1
//获取子节点状态,判断其是否有子节点
cur = cur->next[board[i][j]-'a'];
if(cur == nullptr) return;//剪枝2
if(cur->isEnd){
result.push_back(cur->val);
cur->isEnd = false;
}
visited[i][j] = true; //修改节点状态,防止重复访问
backtracking(board,visited,i+1,j,cur);
backtracking(board,visited,i-1,j,cur);
backtracking(board,visited,i,j+1,cur);
backtracking(board,visited,i,j-1,cur);
visited[i][j] = false;//回溯
}
};
1162. 地图分析
力扣链接
你现在手里有一份大小为 n x n 的 网格 grid,上面的每个 单元格 都用 0 和 1 标记好了。其中 0 代表海洋,1 代表陆地,请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的。如果网格上只有陆地或者海洋,请返回 -1。
我们这里说的距离是「曼哈顿距离」( Manhattan Distance):(x0, y0) 和 (x1, y1) 这两个单元格之间的距离是 |x0 - x1| + |y0 - y1| 。
示例 1:
输入:grid = [[1,0,1],[0,0,0],[1,0,1]]
输出:2
解释:
海洋单元格 (1, 1) 和所有陆地单元格之间的距离都达到最大,最大距离为 2。
示例 2:
输入:grid = [[1,0,0],[0,0,0],[0,0,0]]
输出:4
解释:
海洋单元格 (2, 2) 和所有陆地单元格之间的距离都达到最大,最大距离为 4。
提示:
n == grid.length
n == grid[i].length
1 <= n <= 100
grid[i][j] 不是 0 就是 1
解法1:多源BFS
思路:
代码:
class Solution {
public:
int maxDistance(vector<vector<int>>& grid) {
//多源BFS
int n = grid.size();
vector<vector<int>> d(n,vector<int>(n,-1));
queue<pair<int,int>> que;
for(int i = 0;i<n;i++){
for(int j = 0;j<n;j++){
if(grid[i][j] == 1){
d[i][j] = 0;
que.emplace(i,j);
}
}
}
int result = -1;
int dd = 1;
const int dx[] = {-1,1,0,0};
const int dy[] = {0,0,-1,1};
while(!que.empty()){
int size = que.size();
while(size-- > 0){
auto [x,y] = que.front();
que.pop();
for(int i = 0;i<4;i++){
int a = x + dx[i];
int b = y + dy[i];
if(a<0 || a == n || b< 0||b==n) continue;
if(d[a][b] != -1) continue;
d[a][b] = dd;
result = dd;
que.emplace(a,b);
}
}
dd++;
}
return result;
}
};
复杂度分析:
时间复杂度:考虑这里的「多源最短路」的本质还是「单源最短路」,渐进时间复杂度 O(n^2)
空间复杂度:该算法使用了 d 数组,渐进空间复杂度为 O(n^2)
相似题目:1765. 地图中的最高点
给你一个大小为 m x n 的整数矩阵 isWater ,它代表了一个由 陆地 和 水域 单元格组成的地图。
如果 isWater[i][j] == 0 ,格子 (i, j) 是一个 陆地 格子。
如果 isWater[i][j] == 1 ,格子 (i, j) 是一个 水域 格子。
你需要按照如下规则给每个单元格安排高度:
每个格子的高度都必须是非负的。
如果一个格子是是 水域 ,那么它的高度必须为 0 。
任意相邻的格子高度差 至多 为 1 。当两个格子在正东、南、西、北方向上相互紧挨着,就称它们为相邻的格子。(也就是说它们有一条公共边)
找到一种安排高度的方案,使得矩阵中的最高高度值 最大 。
请你返回一个大小为 m x n 的整数矩阵 height ,其中 height[i][j] 是格子 (i, j) 的高度。如果有多种解法,请返回 任意一个 。
示例 1:
输入:isWater = [[0,1],[0,0]]
输出:[[1,0],[2,1]]
解释:上图展示了给各个格子安排的高度。
蓝色格子是水域格,绿色格子是陆地格。
示例 2:
输入:isWater = [[0,0,1],[1,0,0],[0,0,0]]
输出:[[1,1,0],[0,1,1],[1,2,2]]
解释:所有安排方案中,最高可行高度为 2 。
任意安排方案中,只要最高高度为 2 且符合上述规则的,都为可行方案。
提示:
m == isWater.length
n == isWater[i].length
1 <= m, n <= 1000
isWater[i][j] 要么是 0 ,要么是 1 。
至少有 1 个水域格子。
解法1:多源BFS
思路:
代码:
class Solution {
public:
vector<vector<int>> highestPeak(vector<vector<int>>& isWater) {
//多源BFS
int m = isWater.size(), n = isWater[0].size();
vector<vector<int>> result(m,vector<int>(n,0));
queue<pair<int,int>> que;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(isWater[i][j] == 1){// 水域作为起点入队
que.emplace(i,j);
}else{
result[i][j] = -1;
}
}
}
const int dx[] = {-1,1,0,0};
const int dy[] = {0,0,-1,1};
int h = 1;// 初始高度(和水域相邻的点最高只能为1)
while(!que.empty()){
int size = que.size();
while(size-- > 0)
{
auto [x,y] = que.front();
que.pop();
for(int i = 0;i<4;i++)
{
int a = x + dx[i], b = y + dy[i];
if(a < 0 || a == m || b< 0 || b == n) continue;
if(result[a][b] != -1) continue;
result[a][b] = h;
que.emplace(a,b);
}
}
h++;
}
return result;
}
};
相似题目:994. 腐烂的橘子
力扣链接
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
示例 1:
输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:grid = [[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个正向上。
示例 3:
输入:grid = [[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 10
grid[i][j] 仅为 0、1 或 2
解法1:多源BFS
思路:
代码:
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
//多源BFS
int m = grid.size(), n = grid[0].size();
queue<pair<int,int>> que;
int fresh = 0;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(grid[i][j] == 1) fresh++;
if(grid[i][j] == 2) que.push({i,j});
}
}
int res = 0;
const int dx[4] = {-1,1,0,0};
const int dy[4] = {0,0,1,-1};
while(!que.empty()){
int size = que.size();
bool rotten = false;
while(size-- > 0){
int x = que.front().first, y = que.front().second;
que.pop();
for(int i = 0;i<4;i++){
int xx = x + dx[i], yy = y + dy[i];
if(!(xx < 0 || yy < 0 || xx >= m || yy >= n ) && grid[xx][yy] == 1){
grid[xx][yy] = 2;
que.push({xx,yy});
fresh--;
rotten = true;
}
}
}
if(rotten) res++;
}
return fresh? -1:res;
}
};
迷宫问题
解法1:回溯
代码:
#include<iostream>
#include<vector>
using namespace std;
vector<pair<int,int>> res;
void dfs(vector<vector<int>>& matrix,vector<pair<int,int>>& path, int i, int j){
if(i<0 || i>=matrix.size() || j<0 || j>= matrix[0].size() || matrix[i][j] == 1) return;
matrix[i][j] = 1;
path.push_back({i,j});
if(i == matrix.size()-1 && j == matrix[0].size()-1){
res = path;
return;
}
dfs(matrix,path,i+1,j);
dfs(matrix,path,i,j+1);
dfs(matrix,path,i-1,j);
dfs(matrix,path,i,j-1);
path.pop_back();
matrix[i][j] = 0;
}
int main(){
int m, n;
cin >> m >> n;
vector<vector<int>> matrix(m,vector<int>(n,0));
for(int i = 0;i<m;++i){
for(int j = 0;j<n;++j){
cin>>matrix[i][j];
}
}
vector<pair<int,int>> path;
dfs(matrix,path,0,0);
for(int i = 0;i<res.size();++i){
cout<<"(" <<res[i].first<<","<<res[i].second << ")"<<endl;
}
return 0;
}