一. DFS基础
1.套路
1.找连通块、判断是否有环 (如 207 题)等
2.两大思考:
- (1)是否要回溯?
- (I)要回溯的情况:枚举所有可能(路径/排列/组合/子集),需撤销选择继续尝试其他分支
- (II)不要回溯的情况:只覆盖一次(连通性/可达性):只走一遍并标记访问状态(连通块)/不关心路径,只关心是否能走到/涂色染色
- (2)是否要
vis
数组?-
(I)要
vis
数组的情况:不是树结构的无向图/有向图中有环/多条路径到一个点,避免死循环 -
(II)不用
vis
数组的情况:有向无环图/树结构
2.模板1(计算每个连通块的大小 ,无需回溯,需要vis
数组):// 放外面简单
vector<bool> vis; // 记录是否访问
vector<vector<int>> g; // 邻接表
int v; // 每个连通块的节点数
int e; // 每个连通块的边数
// dfs
void dfs(int x){
vis[x]=true; // 在dfs里面标记
++v;
e+=g[x].size();
for(int& y:g[x]){
if(!vis[y]){
dfs(y);
}
}
}// n为图的节点数,edges为边集合:{{x1,y1},{x2,y2},...}
vector<int> solve(int n,vector<vector<int>>& edges){
g.resize(n); // 邻接表存储
for(auto& e:edges){
int x=e[0],y=e[1];
g[x].push_back(y);
g[y].push_back(x); // 无向图
}
// vis数组确保每个节点只访问一次
vis.assign(n,false);
// 计算每个连通块的大小
vector<int> ansV;
vector<int> ansEdge
// 遍历每个未访问的节点,即每个连通块访问一次
for(int i=0;i<n;++i){
if(!vis[i]){
v=0;
e=0;
dfs(i);
ansV.push_back(v);
ansEdge.push_back(e/2); // 无向图每条边算两次
}
}
return ans;
}
-
3.模版2(寻找所有可能的路径 ,需要回溯,无需vis
数组)
class Solution {
public:
int n;
vector<vector<int>> res;
vector<int> path;
void dfs(int x, vector<vector<int>>& graph) {
// 访问节点
path.push_back(x);
// 终止条件
if (x == n - 1) {
// 更新答案
res.push_back(path);
} else {
// 访问邻居
for (auto& y : graph[x]) {
dfs(y, graph);
}
}
// 对第一行的回溯
path.pop_back();
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
n = graph.size();
dfs(0, graph);
return res;
}
};
2.题目描述
3.学习经验
1.[[十.图论算法-基础遍历#2. 1971. 寻找图中是否存在路径(简单,学习)]],自己会错写成
if (!vis[y] )
return dfs(y, destination);
但这个逻辑是不对的,这个逻辑是对于当前x,尝试了一个未访问的邻居y,若y不能到达,则直接return false
,表示当前x也不能到达(未尝试所有邻居y,会提前退出)
正确写法是尝试所有邻居y,若存在一个y满足,则断定x也满足
if (!vis[y] && dfs(y, destination)) // 写成条件,表示尝试所有邻居y
return true; // 有一个y满足,则x也满足,返回true
2.一个小技巧是在dfs函数里面 才叫访问当前节点,且在刚开始时 更新vis
数组和回溯法更新答案和最后回溯,而不是在遍历邻居中更新
3.dfs很大的作用是标记
4.对于设计祖先的问题可以考虑反向建图反向遍历[[十.图论算法-基础遍历#10. 2192. 有向无环图中一个节点的所有祖先(中等,学习,无回溯,有vis)]]
5.用多个节点作为初始节点开始遍历时,可以将vis
数组设置为vis[x]=start
,从而本轮遍历只用判断vis[y]!=start
即可,就不用每轮都将vis
数组置为false
了
1. 547.省份数量(中等,求连通块数量,无回溯,有vis)
思想
1.有 n
个城市,其中一些彼此相连,另一些没有相连。如果城市 a
与城市 b
直接相连,且城市 b
与城市 c
直接相连,那么城市 a
与城市 c
间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n
的矩阵 isConnected
,其中 isConnected[i][j] = 1
表示第 i
个城市和第 j
个城市直接相连,而 isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中 省份 的数量。
2.计算连通块的数量
代码
class Solution {
public:
vector<vector<int>> g;
vector<bool> vis;
void dfs(int x) {
vis[x] = true;
for (int& y : g[x]) {
if (!vis[y])
dfs(y);
}
}
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size();
g.resize(n);
vis.assign(n, false);
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (isConnected[i][j] == 1) {
g[i].push_back(j);
g[j].push_back(i);
}
}
}
int res = 0;
for (int i = 0; i < n; ++i) {
if (!vis[i]) {
dfs(i);
++res;
}
}
return res;
}
};
2. 1971. 寻找图中是否存在路径(简单,重点学习,查询一次路径是否存在,无回溯,有vis)
1971. 寻找图中是否存在路径 - 力扣(LeetCode)
思想
1.有一个具有 n
个顶点的 双向 图,其中每个顶点标记从 0
到 n - 1
(包含 0
和 n - 1
)。图中的边用一个二维整数数组 edges
表示,其中 edges[i] = [ui, vi]
表示顶点 ui
和顶点 vi
之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。
请你确定是否存在从顶点 source
开始,到顶点 destination
结束的 有效路径 。
给你数组 edges
和整数 n
、source
和 destination
,如果从 source
到 destination
存在 有效路径 ,则返回 true
,否则返回 false
。
代码
class Solution {
public:
vector<vector<int>> g;
vector<bool> vis;
bool dfs(int x, int destination) {
if (x == destination)
return true;
vis[x] = true;
for (auto& y : g[x]) {
if (!vis[y] && dfs(y, destination)) // 写成条件,表示尝试所有邻居y
return true;
}
return false;
}
bool validPath(int n, vector<vector<int>>& edges, int source,
int destination) {
if (source == destination)
return true;
g.resize(n);
vis.assign(n, false);
for (auto& e : edges) {
int u = e[0], v = e[1];
g[u].push_back(v);
g[v].push_back(u);
}
bool res = dfs(source, destination);
return res;
}
};
3. 797. 所有可能的路径(中等,学习,获得所有可能路径,有回溯,无vis)
思想
1.给你一个有 n
个节点的 有向无环图(DAG) ,请你找出从节点 0
到节点 n-1
的所有路径并输出(不要求按特定顺序 )
graph[i]
是一个从节点 i
可以访问的所有节点的列表(即从节点 i
到节点 graph[i][j]
存在一条有向边)。
代码
class Solution {
public:
int n;
vector<vector<int>> res;
vector<int> path;
void dfs(int x, vector<vector<int>>& graph) {
// 访问节点
path.push_back(x);
// 终止条件
if (x == n - 1) {
// 更新答案
res.push_back(path);
} else {
// 访问邻居
for (auto& y : graph[x]) {
dfs(y, graph);
}
}
// 对第一行的回溯
path.pop_back();
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
n = graph.size();
dfs(0, graph);
return res;
}
};
4. 841. 钥匙和房间(中等,无回溯,有vis)
思想
1.有 n
个房间,房间按从 0
到 n - 1
编号。最初,除 0
号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。
当你进入一个房间,你可能会在里面找到一套 不同的钥匙 ,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。
给你一个数组 rooms
其中 rooms[i]
是你进入 i
号房间可以获得的钥匙集合。如果能进入 所有 房间返回 true
,否则返回 false
。
代码
class Solution {
public:
vector<bool> vis;
void dfs(int x, vector<vector<int>>& rooms) {
vis[x] = true;
for (auto& y : rooms[x]) {
if (!vis[y])
dfs(y, rooms);
}
return;
}
bool canVisitAllRooms(vector<vector<int>>& rooms) {
int n = rooms.size();
vis.assign(n, false);
dfs(0, rooms);
bool res = true;
for (auto x : vis) {
if (!x) {
res = false;
break;
}
}
return res;
}
};
5. 2316. 统计无向图中无法互相到达点对数(中等,学习前缀累加法优化, 无回溯,有vis)
2316. 统计无向图中无法互相到达点对数 - 力扣(LeetCode)
思想
1.给你一个整数 n
,表示一张 无向图 中有 n
个节点,编号为 0
到 n - 1
。同时给你一个二维整数数组 edges
,其中 edges[i] = [ai, bi]
表示节点 ai
和 bi
之间有一条 无向 边。
请你返回 无法互相到达 的不同 点对数目 。
2.本质是求每个连通块的大小,然后再将连通块数组的(i,j),i!=j
进行相乘求和,可以利用乘法原理边计算边更新答案,即当前已知连通块总数为total
,新得到的为size
,每个新得到的与总数都会产生一个配对,所以答案加上total*size
代码
class Solution {
public:
typedef long long ll;
vector<vector<int>> g;
vector<bool> vis;
ll dfs(int x) {
vis[x] = true;
ll res = 1;
for (auto& y : g[x]) {
if (!vis[y])
res += dfs(y);
}
return res;
}
long long countPairs(int n, vector<vector<int>>& edges) {
g.resize(n);
vis.assign(n, false);
for (auto& e : edges) {
int u = e[0], v = e[1];
g[u].push_back(v);
g[v].push_back(u);
}
ll sum = 0, dif = 0;
for (int i = 0; i < n; ++i) {
if (!vis[i]) {
ll size = dfs(i);
sum += size;
dif += 1LL * size * size;
}
}
ll res = (sum * sum - dif) / 2;
return res;
}
};
乘法原理优化:
6. 1319. 连通网络的操作次数(中等,无回溯,有vis)
1319. 连通网络的操作次数 - 力扣(LeetCode)
思想
1.用以太网线缆将 n
台计算机连接成一个网络,计算机的编号从 0
到 n-1
。线缆用 connections
表示,其中 connections[i] = [a, b]
连接了计算机 a
和 b
。
网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。
给你这个计算机网络的初始布线 connections
,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。
2.本质还是求连通块的数量
代码
class Solution {
public:
vector<vector<int>> g;
vector<bool> vis;
void dfs(int x) {
vis[x] = true;
for (auto& y : g[x]) {
if (!vis[y]) {
dfs(y);
}
}
}
int makeConnected(int n, vector<vector<int>>& connections) {
int m = connections.size();
if (m < n - 1)
return -1;
g.resize(n);
vis.assign(n, false);
for (auto& c : connections) {
int u = c[0], v = c[1];
g[u].push_back(v);
g[v].push_back(u);
}
int cnt = 0;
for (int i = 0; i < n; ++i) {
if (!vis[i]) {
dfs(i);
++cnt;
}
}
return cnt - 1;
}
};
7. 2492. 两个城市间路径的最小分数(中等,学习,无回溯,有vis)
2492. 两个城市间路径的最小分数 - 力扣(LeetCode)
思想
1.给你一个正整数 n
,表示总共有 n
个城市,城市从 1
到 n
编号。给你一个二维数组 roads
,其中 roads[i] = [ai, bi, distancei]
表示城市 ai
和 bi
之间有一条 双向 道路,道路距离为 distancei
。城市构成的图不一定是连通的。
两个城市之间一条路径的 分数 定义为这条路径中道路的 最小 距离。
城市 1
和城市 n
之间的所有路径的 最小 分数。
注意:
- 一条路径指的是两个城市之间的道路序列。
- 一条路径可以 多次 包含同一条道路,你也可以沿着路径多次到达城市
1
和城市n
。 - 测试数据保证城市
1
和城市n
之间 至少 有一条路径。
2.这题因为保证城市1
和城市n
之间 至少 有一条路径,而答案定义是城市1
和城市n
之间的所有路径的最小距离,即1所在的连通块的边的最小值 ,按照遍历连通块的方法遍历,但是每个节点只访问一次,所以要在if(!vis[y])
之前更新答案(可以节点已遍历,但边权可以获取更新)
代码
class Solution {
public:
typedef pair<int, int> PII;
vector<vector<PII>> g;
vector<int> vis;
int res;
void dfs(int x) {
vis[x] = true;
for (auto& p : g[x]) {
int y = p.first, dis = p.second;
res = min(res, dis);
if (!vis[y]) {
dfs(y);
}
}
}
int minScore(int n, vector<vector<int>>& roads) {
g.resize(n + 1);
vis.assign(n + 1, 0);
for (auto& r : roads) {
int a = r[0], b = r[1], dis = r[2];
g[a].push_back({b, dis});
g[b].push_back({a, dis});
}
res = INT_MAX;
dfs(1);
return res;
}
};
8. 3310. 移除可疑的方法(中等,学习理解题意,无回溯,有vis)
思想
1.你正在维护一个项目,该项目有 n
个方法,编号从 0
到 n - 1
。
给你两个整数 n
和 k
,以及一个二维整数数组 invocations
,其中 invocations[i] = [ai, bi]
表示方法 ai
调用了方法 bi
。
已知如果方法 k
存在一个已知的 bug。那么方法 k
以及它直接或间接调用的任何方法都被视为 可疑方法 ,我们需要从项目中移除这些方法。
只有当一组方法没有被这组之外的任何方法调用时,这组方法才能被移除。
返回一个数组,包含移除所有 可疑方法 后剩下的所有方法。你可以以任意顺序返回答案。如果无法移除 所有 可疑方法,则 不 移除任何方法。
2.注意这一句话"只有当一组 方法没有被这组之外的任何方法调用时,这组方法才能被移除。",所以是要移出是移出一组,而不能移出这一组中的某一些,所以这题变简单了,本质就是标记从k开始的连通块,然后标记完再去判断是否存在无标记->标记,存在则无法删除,不存在则可以删除
3.为了避免有向图存在环,还是要用vis
数组
代码
class Solution {
public:
vector<vector<int>> g;
vector<bool> vis;
void dfs(int x) {
vis[x] = true;
for (auto& y : g[x]) {
if (!vis[y]) {
dfs(y);
}
}
}
vector<int> remainingMethods(int n, int k,
vector<vector<int>>& invocations) {
g.resize(n);
vis.assign(n, false);
for (auto& i : invocations) {
int a = i[0], b = i[1];
g[a].push_back(b);
}
dfs(k);
vector<int> res;
for (auto& i : invocations) {
// 无标记->标记
if (!vis[i[0]] && vis[i[1]]) {
for (int i = 0; i < n; ++i)
res.push_back(i);
return res;
}
}
for (int i = 0; i < n; ++i) {
if (!vis[i])
res.push_back(i);
}
return res;
}
};
9. 2685. 统计完全连通分量的数量(中等,无回溯,有vis)
2685. 统计完全连通分量的数量 - 力扣(LeetCode)
思想
1.给你一个整数 n
。现有一个包含 n
个顶点的 无向 图,顶点按从 0
到 n - 1
编号。给你一个二维整数数组 edges
其中 edges[i] = [ai, bi]
表示顶点 ai
和 bi
之间存在一条 无向 边。
返回图中 完全连通分量 的数量。
如果在子图中任意两个顶点之间都存在路径,并且子图中没有任何一个顶点与子图外部的顶点共享边,则称其为 连通分量 。
如果连通分量中每对节点之间都存在一条边,则称其为 完全连通分量 。
2.判断完全连通分量本质上是求一个连通分量的节点数和边数,看看C_n^2==m
,但是无向图边会统计两次,所以为n*(n-1)==m
代码
class Solution {
public:
vector<vector<int>> g;
vector<bool> vis;
int edge;
int dfs(int x) {
vis[x] = true;
int node = 1;
for (auto& y : g[x]) {
++edge;
if (!vis[y])
node += dfs(y);
}
return node;
}
int countCompleteComponents(int n, vector<vector<int>>& edges) {
g.resize(n);
vis.resize(n, false);
for (auto& e : edges) {
int a = e[0], b = e[1];
g[a].push_back(b);
g[b].push_back(a);
}
int res = 0;
for (int i = 0; i < n; ++i) {
if (!vis[i]) {
edge = 0;
int node = dfs(i);
if (node * (node - 1) == edge)
++res;
}
}
return res;
}
};
10. 2192. 有向无环图中一个节点的所有祖先(中等,学习,无回溯,有vis)
2192. 有向无环图中一个节点的所有祖先 - 力扣(LeetCode)
思想
1.给你一个正整数 n
,它表示一个 有向无环图 中节点的数目,节点编号为 0
到 n - 1
(包括两者)。
给你一个二维整数数组 edges
,其中 edges[i] = [fromi, toi]
表示图中一条从 fromi
到 toi
的单向边。
请你返回一个数组 answer
,其中 answer[i]
是第 i
个节点的所有 祖先 ,这些祖先节点 升序 排序。
如果 u
通过一系列边,能够到达 v
,那么我们称节点 u
是节点 v
的 祖先 节点。
2.此题有两种方法,反向遍历和正向遍历
(1)反向遍历,即反向建图,则一个节点dfs遍历到的所有节点就是它的祖先,因为只dfs求能遍历的顶点集合,所以要vis数组,但有个小技巧,因为每个节点都要作为初始节点遍历一次,所以可以把遍历过的节点vis[x]=start
,来判断本轮vis[y]!=start
则说明未遍历过,就不用每轮遍历vis
数组都置为false
了。
同时为了保证"这些祖先节点 升序 排序",所以先标记,标记完了再按升序顺序遍历更新答案
(2)正向遍历,即每个节点作为初始节点开始遍历,遍历到的孩子只放入初始节点一个祖先,避免重复,这里初始节点升序遍历,所以可以直接在dfs里面更新答案
代码
反向遍历
class Solution {
public:
vector<vector<int>> g;
vector<int> vis;
vector<vector<int>> res;
int start;
void dfs(int x) {
vis[x] = start;
for (auto& y : g[x]) {
if (vis[y] != start)
dfs(y);
}
}
vector<vector<int>> getAncestors(int n, vector<vector<int>>& edges) {
g.resize(n);
vis.assign(n, -1);
res.resize(n);
for (auto& e : edges) {
g[e[1]].push_back(e[0]);
}
for (int i = 0; i < n; ++i) {
start = i;
dfs(i); // 先标记
// 再遍历,保证升序
for (int j = 0; j < n; ++j) {
if (j != i && vis[j] == start) {
res[i].push_back(j);
}
}
}
return res;
}
};
正向遍历:
class Solution {
public:
vector<vector<int>> g;
vector<int> vis;
vector<vector<int>> res;
int start;
void dfs(int x) {
vis[x] = start;
if (x != start)
res[x].push_back(start);
for (auto& y : g[x]) {
if (vis[y] != start)
dfs(y);
}
}
vector<vector<int>> getAncestors(int n, vector<vector<int>>& edges) {
g.resize(n);
vis.assign(n, -1);
res.resize(n);
for (auto& e : edges) {
g[e[0]].push_back(e[1]);
}
for (int i = 0; i < n; ++i) {
start = i;
dfs(i);
}
return res;
}
};
11. 3387. 两天自由外汇交易后的最大货币数(中等,学习,无回溯,有vis)
3387. 两天自由外汇交易后的最大货币数 - 力扣(LeetCode)
思想
1.给你一个字符串 initialCurrency
,表示初始货币类型,并且你一开始拥有 1.0
单位的 initialCurrency
。
另给你四个数组,分别表示货币对(字符串)和汇率(实数):
pairs1[i] = [startCurrencyi, targetCurrencyi]
表示在 第 1 天 ,可以按照汇率rates1[i]
将startCurrencyi
转换为targetCurrencyi
。pairs2[i] = [startCurrencyi, targetCurrencyi]
表示在 第 2 天 ,可以按照汇率rates2[i]
将startCurrencyi
转换为targetCurrencyi
。- 此外,每种
targetCurrency
都可以以汇率1 / rate
转换回对应的startCurrency
。
你可以在 第 1 天 使用rates1
进行任意次数的兑换(包括 0 次),然后在 第 2 天 使用rates2
再进行任意次数的兑换(包括 0 次)。
返回在两天兑换后,最大可能拥有的initialCurrency
的数量。
注意:汇率是有效的,并且第 1 天和第 2 天的汇率之间相互独立,不会产生矛盾。
2.此题能看出来是图论,且pairs1
和pairs2
要单独建图 ,这里有个技巧,两者都从initialCurrency
开始dfs,得到了兑换后的货币-转换汇率的哈希映射(只用写一个函数) ,接着我们可以枚举中间货币mid
,mp1[mid]
和mp2[mid]
表示initialCurrency
转换成mid
的金额,而因为有"每种targetCurrency
都可以以汇率1 / rate
转换回对应的startCurrency
"这个条件,所以1/mp2[mid]
就表示从mid
转化成initialCurrency
的金额,最终答案就是max(mp1[mid]/mp2[mid])
3.对于字符串不再用vector<vector<int>> g
建图,而是用map<string,pair<string,double>> g
建图
代码
class Solution {
public:
typedef pair<string, double> PSD;
map<string, vector<PSD>> g;
map<string, double> mp;
void dfs(string curCurrency, double cnt) {
mp[curCurrency] = cnt;
for (auto& y : g[curCurrency]) {
if (!mp.count(y.first))
dfs(y.first, cnt * y.second);
}
}
map<string, double> solve(string& initialCurrency,
vector<vector<string>>& pairs,
vector<double>& rates) {
g.clear();
mp.clear();
int n = pairs.size();
for (int i = 0; i < n; ++i) {
auto p = pairs[i];
double r = rates[i];
g[p[0]].push_back({p[1], r});
g[p[1]].push_back({p[0], 1 / r}); // 反向
}
dfs(initialCurrency, 1);
return mp;
}
double maxAmount(string initialCurrency, vector<vector<string>>& pairs1,
vector<double>& rates1, vector<vector<string>>& pairs2,
vector<double>& rates2) {
auto mp1 = solve(initialCurrency, pairs1, rates1);
auto mp2 = solve(initialCurrency, pairs2, rates2);
double res = 0;
for (auto& tmp : mp2) {
res = max(res, 1.0 * mp1[tmp.first] / tmp.second);
}
return res;
}
};