1.整体看搜索
1.1 搜索中几个重要的概念
对于数据结构来说 :搜索分为图的搜索,矩阵的搜索,树的搜索,其中树的搜索可以转化为图的搜索
| 数据结构 | 深度 | 广度 | 相关题目 |
|---|---|---|---|
| 二叉树 | 先序:1.定义一个栈2.根节点进栈 3.根节点出栈(处理根)4.右左孩子入栈 中序:1.根节点不入栈2.while 循环分两步一直入栈左孩子 or 没有左孩子了->出栈一个元素(最底层元素)->处理该元素->右孩子不入栈!!->继续遍历该右孩子的左孩子 3.后续:1.定义一个栈,根节点入栈2.根节点出栈开始处理2.右左孩子入栈3.将 res 逆序 | 层次遍历: 1.定义一个队列,根节点入队 2.处理 root ,记录当前 size 3.使用 for 循环遍历队中剩余的节点,将那些节点的左右孩子入队 | 先:144 |
| 图 | 1.将题目中的数据进行映射,使用一个 vidsited 数组记录每个节点的访问情况 2.从头开始遍历 edges,cur 3.再以 cur 为起点递归遍历其 “孩子”,并对其孩子的遍历状态进行记录 | 1.将题目中的数据形成图的映射,并得到每个节点的入度 2.创建一个 queue 存放入度为 0 的元素 3.遍历 queue ,将队首元素弹出,遍历其”孩子” 4.孩子的处理:入度-1,如果入度为 0 则放入队列 | 207 |
| 二维数组 | 1.双重 for 循环以 [0,0] 为起始点,逐个遍历每个节点 2.在 dfs 中先对该节点进行处理 3.在 dfs 中递归遍历 cur 节点上下左右四个节点 | 1.for 循环以 [0,0] 为起始节点,逐个遍历每个节点 2.创建一个 queue ,将 cur 元素添加到 que 中,que 保存 cur 节点的坐标信息 3.队首元素出队,以 cur 为出发点遍历其上下左右四个元素。并将cur元素周围的单元格其入队,周围的单元格再把它周围的单元格入队 | 200 |
时间复杂度:O(MN) 就是二维数组的大小
**从搜索的角度又分为两种:**深搜 DFS 和广搜 BFS
在遍历的时候有三种数组:
邻接矩阵 A —> 代表节点和节点之间的连接关系,只有有了邻接矩阵才能知道下一步要往哪里走,对于图来说要搞清楚是有向图还是无向图
visited 数组 —>
(1)是否要有 visited
visited 用于防止走回头路:判断是否以 cur 为起点进行了 dfs 或者 bfs 就要用 visited 数组记录,因为有时候我们可能会在遍历 A 时 从 node A-> node B ,然后遍历 B 时又从 node B->node A ,那么 A 就被遍历了两次,造成 A->B 死循环。但如果是”不会走回头路“的遍历就不需要 visited ,比如后面的 N皇后题,它不会再往上走了,与此同时如果我们也会原地记录,比如 nums[i] [j] 本来是 . 遍历完之后会将其置为 X ,那这样就不需要额外的 visited 用来记录了
(2)visited 是否要置为 false
像 797. 所有可能的路径 - 力扣(LeetCode)有多个路径都会经过某一个 node 的题目,在该路径遍历最后就需要将 visited = false ,方便下一条路径遍历该节点
像课程表,该题不需要找出所有的路径,某个 node 会出现在多个路径中,但是该题只判断是否存在环所以只判断一次就够,visited 就无需置为 false
(3)visited 是否放在 for 循环内部
放在 for 循环内部需要考虑该 dfs 开头定义的退出条件是否重要,因为我们很有可能因为 if(!visited[next]) dfs(next) 这个条件导致,next 节点无法进入下一层 dfs,dfs 的那些退出条件也就判断不到
(例子可见:课程表讲解)
入度 in —> 代表每个节点的入度,一般用于判断是否有环
1.2 dfs 函数模板
深度优先搜索是先从一个节点 cur 开始,然后遍历 cur ,再然后遍历 cur 所有可以到达的子节点,再再然后遍历 cur 的子节点。。。。
1.参数与返回值
参数的话需要传入我们当前需要遍历哪个节点;A 也就是遍历的路径;visited 判断该点是否已经遍历
2.返回值
关于什么时候有返回值,什么时候没有返回值
有返回值:
①在某个中间节点就可以找到答案
在中间过程出现了答案(不论这个答案是对还是错),再往下多走一步都是多余则可以向上返回了
这个题可以参考,因为关于这个节点再往下多走一步就会出错,所以当到达临界值是就不再向下判断
②这个返回值可以是 int 可以是 bool
③从头遍历到尾且答案唯一
这种情况可以借鉴 N 皇后和解数独,数独是主要找到那个点我们就返回,因为他答案唯一,再往下多找一步都多余
没有返回值:
①从头遍历到尾,当 for 循环结束时这个 dfs 也就自己结束了
②从头遍历到尾且存在多个答案
比如 N 皇后问题,他就要从头遍历到尾,但是答案有多种,如果我们对某个树叶进行 return ,后面的树叶就不会再遍历了
XXXX dfs(int i,int j,vector<vector<int>>& A,vector<vector<bool>>& visited){
if(判断是否到达边界) return;
// 开始往深处遍历
for(auto [dr,dc]:vector<vector<int>>{{1,0},{-1,0},{0,-1},{0,1}}){ // 可以遍历的方向
int nr = i+dr;
int nc = j+dc;
if(判断 A[i][j] 是否满足往深里遍历的条件){
visited[i][j] = true;
if(dfs(nr,nc,A,visited)) return true; // 是否要从这一步进行截断
}
}
}
1.3 起始节点的问题
上面说了 dfs 函数,是从 cur 这个节点出发深度遍历,但是有时候节点可能从多个方向出发,所以在核心方法中要用一个 for 循环,然后从 for 循环中依次调用 dfs 方法
矩阵的:
for(int i = 0;i<row;i++){
for(int j = 0;j<col;j++){
dfs(i,j,A,visited);
}
}
树的:
res+=dfs(root,targetSum); // 先得到树根的
if(root->left) res+=pathSum(root->left,targetSum); // 以左右节点出发
if(root->right) res+=pathSum(root->right,targetSum);
2. 图的搜索
对于图搜来说第一步要创建邻接矩阵,邻接矩阵代表节点与节点之间的连接情况
邻接矩阵有两种,一个是 N*N 的,一个是 N * M 的
N*N 表示 vector 中每个节点的关系都会得到映射,不是 1, 就是 0
N*M表示 vector 只映射与该 node 相连的节点,那么存放的就是该 node 的 val 值
图的应用有多种:连通图的个数,图中是否存在闭环
需要关注的点:
判断是以随便一个起点开始遍历还是以确定的 start 开始遍历
2.1 DFS
深度遍历的核心思想以 cur 为出发点,遍历所有与 cur 相连的节点 next ,然后再以next 为出发点,遍历 next 的 next ,所有深度优先遍历是递归
2.2 BFS
广度优是搜索一层一层地进行遍历,这里需要用到 queue 进行辅助。不论是树还是图第一步就是先将入度为 0 的 node 放入 queue,并且在后面要不断将入度为 0 的 node 加入到 queue 中
对于树来说入度为 0 的点就是 root 节点,对于图来说需要根据他的邻接矩阵判断入度。
然后再遍历 queue 的每一个节点,该节点被遍历到,该节点的入度就会 – 。如果减到入度为 0 ,则将其放入 queue
易错点:
在层次遍历时,当某个点被访问了就要将其状态改变,也就是代码:
if (j + 1 < grid[i].size() && grid[i][j + 1] == '1') {
q.push(Node(i, j + 1));
grid[i][j + 1] = '0';
}
而不是要等到这个 node 被 pop 出来才进行 grid[i][j + 1] = '0'; 操作。
因为 node 于 node 之前是相互连通的,如果 node 1 与 node 连通,node2 与 node 连通,但是 node 是最后被 pop 出来。那么在 queue 中 node 就会被重复 push 到 queue 中
2.3 拓扑排序
拓扑排序一般是一个有向无环图,是 BFS 的特殊情况
S1:遍历图,计算每个节点的入度,将该图的邻接矩阵表示出来
S2:将入度为 0 的节点加入 queue 中
S3:从 que 中弹出一个节点,将和这个节点连接的节点的入度都置为 1
如果子节点的入度为 0 ,则将此节点添加到 que 中
S4:如果 que 中节点个数为 null 但是还存在入度为 1 的节点则说明存在环
2.4 迪杰斯特拉算法
该算法的核心思想的图的层次遍历+贪心算法
贪心算法的思想: 每次迭代都选择最短路径
也就是及时的将能到达路径的 node 加入到 queue 中,在从 node 取出时取出的都是路径最短的 node 。且确保进入到 queue 的 node 都是已知最短路径的 node
该 node 的最短路径被刷新后,该 node 要继续放入 que 中
层次遍历: 层次遍历是由近及远的遍历,深度遍历像一条路走到底然后再往回走,再一路走到底
所以层次遍历更适合贪心算法,每一步往外扩散都走最近的一步
下面两个资料都给出了算法的具体步骤:
假设有以下图:
A --1-- B --2-- C
| |
4 3
| |
D --5-- E
- 起点为 (A)。
- 初始时,
dist = [0, ∞, ∞, ∞, ∞],优先队列为[{0, A}]。
第一次处理 (A):
- 取出 (A),
curDist = 0,dist[A] = 0。 - 更新邻居 (B) 和 (D) 的距离:
dist[B] = 1,将{1, B}加入队列。dist[D] = 4,将{4, D}加入队列。
- 队列变为
[{1, B}, {4, D}]。
第二次处理 (B):
- 取出 (B),
curDist = 1,dist[B] = 1。 - 更新邻居 (C) 和 (E) 的距离:
dist[C] = 3,将{3, C}加入队列。dist[E] = 4,将{4, E}加入队列。
- 队列变为
[{3, C}, {4, D}, {4, E}]。
第三次处理 (C):
- 取出 (C),
curDist = 3,dist[C] = 3。 - 没有可更新的邻居。
- 队列变为
[{4, D}, {4, E}]。
第四次处理 (D):
- 取出 (D),
curDist = 4,dist[D] = 4。 - 更新邻居 (E) 的距离:
- 通过 (D) 到达 (E) 的距离为
dist[D] + 5 = 9。 - 但
dist[E]已经是 (4),因此不更新。
- 通过 (D) 到达 (E) 的距离为
- 队列变为
[{4, E}]。
第五次处理 (E):
- 取出 (E),
curDist = 4,dist[E] = 4。 - 没有可更新的邻居。
- 队列为空。
2.5 A* 算法
参考视频
动画演示(必看)
A* 算法是一种启发式搜索算法,用于在图中找到从起点到目标点的最短路径。它结合了 Dijkstra 算法 的广度优先搜索和 贪心最佳优先搜索 的启发式方法

不同的算法如何选择:
(1)查找所有路径
广度优先
(2)存在移动成本,找到最短路径或找到最短路径
Dijkstra
(3)存在移动成本,但是图中障碍物较少,寻找最短路径
A*
2.4 743 网络延迟时间 (Dijkstra)
2.4.1 算法描述
这是求在最短路径中最长的时间,那么就要找到每个点的最短路径,所以这里使用迪杰斯特拉算法
这里需要定义一个小根堆保存最短路径,谁的路径最小最先遍历谁
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
dist 记录当前已知的可以到达这个 node 的最短路径,
2.4.2 代码实现
#include <vector>
#include <queue>
#include <algorithm>
#include <climits>
using namespace std;
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
// 邻接矩阵
vector<vector<int>> A(n + 1, vector<int>(n + 1, -1));
for (auto& item : times) {
A[item[0]][item[1]] = item[2];
}
// 距离数组
vector<int> dist(n + 1, INT_MAX);
dist[k] = 0; // 起点到自身的距离为 0
// 小根堆,存储 {距离, 节点}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
que.push({0, k}); // 将起点加入队列
// Dijkstra 算法
while (!que.empty()) {
auto node = que.top(); // 取出当前距离最小的节点
que.pop();
int val = node.first; // 当前距离
int u = node.second; // 当前节点
// 如果当前距离大于已知距离,跳过
if (val > dist[u]) continue;
// 遍历邻居节点
for (int v = 1; v < A[u].size(); v++) {
if (A[u][v] != -1) { // 如果存在边
if (A[u][v] + val < dist[v]) {
dist[v] = A[u][v] + val; // 更新距离
que.push({dist[v], v}); // 将邻居节点加入队列
}
}
}
}
// 找到最远路径
int max_dist = -1;
for (int i = 1; i < A.size(); i++) {
if (dist[i] == INT_MAX) return -1; // 如果有节点不可达,返回 -1
else {
max_dist = max(max_dist, dist[i]);
}
}
return max_dist;
}
};
3. 时空复杂度

2.5 126 骑士的攻击
1.算法描述
这个是骑士最短路径问题,既然是最短路径,那么就用 dijkstra,也就是层次遍历 + 贪心。但是还存在一种情况就是该算法只看路径之间消耗最短,但是这不一定是最近的一条路。也就是我们上一个题,网络的到达时间,我们虽然计算出了到达每个点最短的时间,但有可能路径最短的点经过的中间点非常多
那么 A* 算法就会既考虑到消耗又考虑到路径的长短
每个棋盘位置要多包涵三个属性:
h(x) 欧几里得距离
g(x) 步长
f(x) = g(x)+h(x)
小根堆将 f(x) 小的值放在前面:
由于 priority_queue 只能对 vector 进行排序,要想对 f(x) 中的值按照规则排序需要重写棋盘位置 Node 的比较方法:
struct Node {
int x;
int y;
int g;
int h;
int f;
bool operator < (const Node& n) const {
return n.f < f;
}
};
这里的 moves 相当于 dijstra 中的 dict 数组,用于记录该位置是否被遍历,一个防止该节点重复遍历,一个是找到最短步数路径
if(moves[next_x][next_y]==0 || moves[cur.x][cur.y]+1<moves[next_x][next_y]){
moves[next_x][next_y] = moves[cur.x][cur.y]+1;
将当前 node push 到 que 中需要这样初始化:
Node next;
next.x = next_x;
next.y = next_y;
next.g = cur.g+5;
next.h = func_h(next);
next.f = next.g+next.h;
que.push(next);
2. C++ 代码
#include<vector>
using namespace std;
#include<queue>
#include<iostream>
#include <cstring>
int end_x,end_y;
int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-1,-2};
int moves[1001][1001] = {0};
struct Node{
int x,y;
int h,g,f;
bool operator < (const Node& n) const {
return n.f<f;
}
};
int func_h(Node n1){
return ((n1.x-end_x)*(n1.x-end_x)+(n1.y-end_y)*(n1.y-end_y));
}
void bfs(Node& start_node){
priority_queue<Node> que;
que.push(start_node);
while(!que.empty()){
Node cur = que.top();
que.pop();
if(cur.x==end_x && cur.y==end_y) break;
for(int i = 0;i<8;i++){
int next_x = cur.x+dir[i][0];
int next_y = cur.y+dir[i][1];
if(next_x<1 || next_x>1000 || next_y<1 || next_y>1000){
continue;
}
if(moves[next_x][next_y]==0 || moves[cur.x][cur.y]+1<moves[next_x][next_y]){
moves[next_x][next_y] = moves[cur.x][cur.y]+1;
Node next;
next.x = next_x;
next.y = next_y;
next.g = cur.g+5;
next.h = func_h(next);
next.f = next.g+next.h;
que.push(next);
}
}
}
}
int main(){
int n, a1, a2;
cin >> n;
while (n--) {
cin >> a1 >> a2 >> end_x >> end_y;
memset(moves,0,sizeof(moves));
Node start;
start.x = a1;
start.y = a2;
start.g = 0;
start.h = func_h(start);
start.f = start.g + start.h;
bfs(start);
cout << moves[end_x][end_y] << endl;
}
return 0;
}
2.3_127. 单词接龙
我们以 hit 为出发点进行广度优先搜索,一旦搜到了终点,那么路径就是最短的
S1:先将 hit 放入 queue 中,对其每一个字符进行枚举
S2:先枚举 h 位,枚举 a~z 判断是否在 word_list 中,再枚举 i 位判断是否在 a~z 中,其中 hot 在 list 中,将 hot 放入 queue ,并且为了防止后面枚举过程中又会枚举到 hot 所以将 hot 设置为 visited 的状态,最后枚举 t 位
S3:从 hot 出发继续向下枚举,从 hot 的 h 开始枚举,枚举 a~z,然后将 dot 放入
通过上面的不断枚举,不断的将 node 放到 que 最后其实会组成一个无向图,相当于求从 hit 到 cog 的最短路径
2.3.2 算法描述
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
queue<string> que;
que.push(beginWord);
unordered_set<string> word_set(wordList.begin(),wordList.end());
word_set.erase(beginWord);
if(!word_set.count(endWord)) return 0;
unordered_set<string> visited;
visited.insert(beginWord);
int step = 0;
int n = beginWord.size();
while(!que.empty()){
step++; // 多了一层
int size = que.size();
for(int i = 0;i<size;i++){
string cur = que.front();
que.pop();
for(int k = 0;k<n;k++){
char c = cur[k];
for(char w = 'a';w<='z';w++){
cur[k] = w;
if(word_set.count(cur)){
if(cur==endWord) return step+1;
if(!visited.count(cur)){
que.push(cur);
visited.insert(cur);
}
}
visited.insert(cur);
}
cur[k] = c;
}
}
}
return 0;
}
};
2.5_752. 打开转盘锁
2.5.1 算法描述
这个题和 127 的区别在于我们下一跳可以去的地方是不同的,127下一跳可以去到 a~z ,但是本体是一个转盘,下一次只能去到下一位数,其他的地方是没有区别的
2.5.2 算法描述
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
queue<string> que;
que.push("0000");
unordered_set<string> visited(deadends.begin(),deadends.end());
if(visited.count("0000")) return -1;
if("0000"==target) return 0;
visited.insert("0000");
int step = 0;
while(!que.empty()){
step++;
int size = que.size();
for(int i = 0;i<size;i++){
string cur = que.front();
que.pop();
for(int j = 0;j<4;j++){
for(int t = -1; t < 2; t += 2){
char a = (cur[j] -'0' + 10 + t) % 10 + '0';
string new_cur = cur;
new_cur[j] = a;
if(target==new_cur) return step;
if(!visited.count(new_cur)){
que.push(new_cur);
visited.insert(new_cur);
}
}
}
}
}
return -1;
}
};
2.3.1 算法描述
无向图中两个顶点之间的最短路径的长度,可以通过广度优先遍历得到
2.4 判断连通图的个数
判断连通图的个数也就是判断有几个图 LeetCode 547
1.DFS 方法
从某一个节点为起始点出发,遍历其连接的节点,再通过递归的方式遍历相连节点的相连节点。当一但退出了递归,代表这个 node 所有相连的节点和间接相连的节点和间接的间接相邻的节点都遍历完成,也就是一个连通图
class Solution {
public:
int count = 0;
void dfs(vector<vector<int>>& isConnected, vector<bool>& visited,int i){
int n = isConnected.size();
if(visited[i]) return;
visited[i] = true;
for(int j = 0;j<n;j++){
if(isConnected[i][j]){
dfs(isConnected,visited,j);
}
}
}
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size();
// 判断某个点是否 dfs 过
vector<bool> visited(n,false);
for(int i = 0;i<n;i++){
if(visited[i]==false){
dfs(isConnected,visited,i);
count++;
}
}
return count;
}
};
2.BFS 方法
这里也是先从某一个节点开始,然后遍历邻接矩阵将和其所有相连的节点加入 queue ,一旦 queue 中没有 node 时代表一个连通图结束,为了防止重复遍历节点这里还需要一个 visited 数组
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
// bfs 判断是否是连通图
int n = isConnected.size();
vector<bool> visited(n,false);
int cnt = 0;
// 开始遍历节点
for(int i = 0;i<n;i++){
if(!visited[i]){
cnt++;
queue<int> que;
que.push(i);
while(!que.empty()){
int cur = que.front();
que.pop();
visited[cur]=true;
for(int j = 0;j<isConnected[cur].size();j++){
if(isConnected[cur][j]&&!visited[j]) que.push(j);
}
}
}
}
return cnt;
}
};
2.5 图中是否存在闭环
详见:LeetcCode 207
2.1_207课程表
2.1.1 算法描述
1.题意
首先这个题因为有相互的指向,所以是一个有向图
其次因为课程之间相互依靠,但是总有一个起始课程,所以这是一个无环图。一个有向图并且无环最终得到一个拓扑图。本题的主要目的是判断该图是否存在环
对于深度优先判断环:为每个节点增加判断的状态,如果一个节点重复是“已判断”状态代表出现了环
对于广度优先搜索判断环:对加入到栈中的元素进行判断,并记录判断元素的个数,如果一个元素判断过多次则最后判断个数 > 节点个数,说明存在环
其实这里还有一个映射关系,就是 v 的值和 index 是映射的
2.如何深度优先搜索
S1:记录节点之间的映射关系
key:父节点;value:该父节点指向的相邻节点
比如:在课程中基础课程指向高级课程。有一个数据结构存储节点之间的关系,key :课程 ; value :该课程对应的高级课程
其中图中的「白色」「黄色」「绿色」节点分别表示「未搜索」「搜索中」「已完成」的状态
S2:逐个遍历节点
在遍历节点时首先要防止节点重复遍历,如存在以下 4 种上课方式:
0 -> 1 -> 2
1 -> 3
2 -> 3
3 -> 1
在遍历时需要有两个 vector , visited 和 instack
visited: 判断该 node 是否被深度遍历过,比如上面 0–>1–>2 以 0 为起点已经把 node 2 遍历过了,所以当路径 2–>3 时就没必要再遍历一遍 node 2,并且该 visited[2] 后续也不用置为 false
stack: dfs 算法用栈实现,以 node 0 为起点,首先要深度遍历将 node 1 深度遍历,node2 深度遍历的结果都放入栈中,如果在该栈中存在重复元素代表存在闭环
如果最后以 node1 为开始深度遍历的已经结束,需要将 stack[1] = false,方便其他父节点深度遍历到 node1
举个例子:
0->1->3
0->2->3
0->2->1->3
要想得到 0->2->1->3 这个路径,就需要在 0->1->3 时在 node 1 深度遍历完后将 stack 中置为 false
S3:入栈+回溯
每当以一个 node 作为起点进行深度遍历时,这个起点 node 要先被放入栈,等到与它连接的所有的 node 都遍历完,再将其出栈,这也就是为什么在 def 函数的最后要将 instack[cur] = false 的原因
S3:最后再返回到 A 节点
2.广度优先所搜
S1:计算每个节点的入度和出度
入度为 0 的节点说明是基础节点,基础节点应该先被遍历然后先被输出,所以这里使用队列作为数据结构
S2:首先遍历入度为 0 的节点
B 节点的入度是 0 所以先将 B 放入队列中
S3:一个节点遍历完成后,更改其相邻节点的入度
S4:继续遍历队列,直到队列为空
继续遍历队列,每处理队列中的一个节点首先要将其进行输出,然后将其相邻接点的入度 -1
易错点:
visited 不可放在 for 循环中判断,例子:
[[2,0],[1,0],[3,1],[3,2],[1,3]]
深度搜索过程为 :
第一层:0–>2
第二层:2–>3
第三层:3–>1
第四层:1–>3 这里本来是有环,但是这里 3 是进不去的,因为之前 visited[3] = false,所以如果 visited[3] 放在 for 里面那就无法进入 dfs(next=3) ,也就判断不出 instack[3] = true
2.1.2 代码实现
1.深度优先
class Solution {
public:
bool def(int cur, const vector<vector<int>>& A, vector<bool>& instack,vector<bool>& visited){
if(instack[cur]) return false;
if(visited[cur]) return true;
instack[cur] = true;
visited[cur] = true;
for(int i = 0;i<A[cur].size();i++){
if(A[cur][i]!=0){
if(!def(i,A,instack,visited)){
return false;
}
}
}
instack[cur] = false;
return true;
}
vector<vector<int>> creatA(const int numCourses, const vector<vector<int>>& prerequisites){
vector<vector<int>> A(numCourses,vector<int>(numCourses));
for(int i = 0;i<prerequisites.size();i++){
int row = prerequisites[i][0];
int col = prerequisites[i][1];
A[col][row] = 1;
}
return A;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> A = creatA(numCourses,prerequisites);
vector<bool> visited(numCourses,false);
bool res = true;
for(int i = 0;i<numCourses;i++){
vector<bool> instack(numCourses,false);
if(!visited[i]){
res = def(i,A,instack,visited);
}
if(!res) return false;
}
return res;
}
};
2.广度优先
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
// 定义两个 vector
vector<vector<int>> edges(numCourses,vector<int>(numCourses,0));
vector<int> in(numCourses,0);
// 初始化
for(int i = 0;i<prerequisites.size();i++){
int a = prerequisites[i][0];
int b = prerequisites[i][1];
edges[b][a] = 1;
in[a]++;
}
// 先将所有入度为 0 的点进行加入
queue<int> que;
for(int i = 0;i<in.size();i++){
if(in[i]==0) que.push(i);
}
// 开始遍历
int visited = 0;
while(!que.empty()){
visited++;
// 先弹出一个 node ,然后将其四周的点的入度进行 -1
int u = que.front();
que.pop();
// 判断和其相连的点
for(int i = 0;i<edges[u].size();i++){
if(edges[u][i]==1){ // 易错点。不能是因为一开始 in 是 0 ,而是因为被减掉造成的 in 是 0 才将其加入 que ,否则会出现 que 又重复的现象
in[i]--;
if(i!=u&&in[i]==0) que.push(i);
}
}
}
if(visited==numCourses) return true;
else return false;
}
};
2.1.3 时空复杂度
时间复杂度:
图的深度和广度搜素的时间复杂度都是 O(v+e),也就是节点个数+变得个数
空间复杂度:
深度和广度都是 O(v+e) 因为他们要存储成邻接表的映射关系,与此同时还需要栈或者队列进行辅助,他们的空间复杂度是 O(v) ,所以最后的空间复杂度是 O(v+e)
4. 矩阵的搜索
4.1 DFS
DFS 一般是从一个点出发然后遍历到其他所有点,DFS 一般包含两个函数,一个是 DFS 的入口函数一个是 DFS 函数
入口函数:定义 for 循环,把所有可能的起始位置进行遍历
DFS 函数:这是一个递归函数,想一下函数参数,返回值;终止条件;循环的函数体
函数参数:①数据数组②记忆递归数组 dp ③当前遍历的起始位置
返回值: 如果有了 dp 一般是不需要返回值的,我们直接将结果保存在 dp 中
终止条件:dp 中对应的位置有值
循环的函数体:一般是从该点出发去往 cur 所有可以到达的地方
回溯和 DFS 的区别:
DFS 是一直向下走,如果不满足要求则返回到上一层
回溯是在 DFS 的基础上,当不满足条件会再去下一个分支
4.2 BFS
这里同样用一个 queue 实现,在没有告知初始起点是谁的情况下使用双重 for 循环对每一个点进行遍历
将同一片的岛屿放到同一个 queue 中
2.1 岛屿的数量
详见 LeetCode 200
1.DFS
相当于看以某个 [i][j] 为 “起点” 进行深度遍历,每个 [i][j] 可以深度遍历的方向有上下左右四个,根据是否连通判断是否继续遍历
对于深度遍历来说,当以某个 [i][j] 为 “起点” 进行深度遍历后要进行记录,防止重复的深度遍历。所以这里的做法是如果某个 [i][j] 作为起点了则 grid[i][j] = 0,当然也可以用一个 visited 进行记录
岛屿的数量这一题,在函数 numIslands 每一次对 grid[i][j] 调用 for 循环,在这个完整的 def 函数中会遍历所有的连通节点,下一次 for 循环就是遍历下一组连通节点
class Solution {
public:
int nums_lands = 0;
void dfs(vector<vector<char>>& grid,int i,int j){
// cout<<"i:"<<i<<"j:"<<j<<endl;
int nr = grid.size();
int nc = grid[0].size();
// 将其上下左右四个点设为 0
grid[i][j] = '0';
if(i>0&&grid[i-1][j]=='1') dfs(grid,i-1,j);
if(i<nr&&grid[i+1][j]=='1') dfs(grid,i+1,j);
if(j>0&&grid[i][j-1]=='1') dfs(grid,i,j-1);
if(j<nc&&grid[i][j+1]=='1') dfs(grid,i,j+1);
}
int numIslands(vector<vector<char>>& grid) {
int nr = grid.size();
int nc = grid[0].size();
// DFS
for(int i = 0;i<nr;i++){
for(int j = 0;j<nc;j++){
if(grid[i][j]=='1'){
nums_lands++;
dfs(grid,i,j);
}
}
}
return nums_lands;
}
};
2.BFS
class Solution {
public:
/**
* 判断岛屿数量
* @param grid char字符型vector<vector<>>
* @return int整型
*/
int num_count = 0;
int solve(vector<vector<char> >& grid) {
// write code here
// bfs
for(int i = 0;i<grid.size();i++){
for(int j = 0;j<grid[i].size();j++){
int Row = grid.size();
int Col = grid[i].size();
if(grid[i][j]=='1'){
queue<pair<int,int>> que;
que.push({i,j});
num_count++;
while(!que.empty()){
int cur_i = que.front().first;
int cur_j = que.front().second;
que.pop();
for(auto [dr,dc]:vector<pair<int,int>>{{-1,0},{1,0},{0,-1},{0,1}}){
if(cur_i+dr<Row&&cur_i+dr>=0&&cur_j+dc<Col&&cur_j+dc>=0&&grid[cur_i+dr][cur_j+dc]=='1'){
grid[cur_i+dr][cur_j+dc] = '0';
que.push({cur_i+dr,cur_j+dc});
}
}
}
}
}
}
return num_count;
}
};
2.2.3 空间复杂度
BFS:O(MN),整个二维数组全是陆地
DFS:O(min(M,N)) , 最坏情况全部为陆地
2.3_490迷宫
1.算法描述
这个题和之前题型不同之处在于,本题从某个点出发进行广搜,并且在进行广搜时我们还要跳着广搜。所以这和普通的广搜不同之处在于
(1)开始广搜的出发点不同
(2)不是每搜到一个点就要放入 que 中,只有满足条件才放入 que 中
(3)没有办法动态改变 maze 的值:需要记忆矩阵
这里每一次 BFS 时不是一次走一步,而是一直走走到尽头,所以在四个方向的 for 循环中还有一个 while 循环来控制每次走到尽头
易错点:
在一开始将 que pop 的时候就要判断 cur 是否是终点,而不是等到 cur 移动到第一个位置上时才判断
2.代码实现
class Solution {
public:
bool hasPath(vector<vector<int>>& maze, vector<int>& start, vector<int>& destination) {
int Row = maze.size();
int Col = maze[0].size();
// 广度搜索
queue<pair<int,int>> que;
que.push({start[0],start[1]});
vector<vector<bool>> visited(Row,vector<bool>(Col,false));
while(!que.empty()){
// 遍历这个 node 周围的点
int r = que.front().first;
int c = que.front().second;
visited[r][c] = true;
que.pop();
// 判断该点是否是终点
if(r == destination[0]&&c == destination[1]) return true;
for(auto[dr,dc]:vector<pair<int,int>>{{-1,0},{1,0},{0,-1},{0,1}}){
int nr = r+dr;
int nc = c+dc;
// 再以该点为出发点遍历其他的点
while(nr>=0&&nr<Row&&nc>=0&&nc<Col&&maze[nr][nc]==0){
nr+=dr;
nc+=dc;
}
// 跳出循环了说明该点不通过
nr-=dr;
nc-=dc;
// 最终落入点添加到 que
if(visited[nr][nc]==false) que.push({nr,nc});
}
}
return false;
}
};
2.4.3.时空复杂度
时间复杂度:O(MN) MN 分别是迷宫的宽高
要计算 cur 节点到二维数组每个节点的广搜,所以是 O(MN)
空间复杂度:O(MN)
2.4_505 迷宫2
2.4.1.算法描述
这个题记录的是从点 cur 到其他点的最短路径。最短路径可以使用广搜也可以使用地杰斯特拉算法
本题特点:
①需要创建一个 dist 用于记录 cur 到其他 node 的最短路径
②不需要记忆数组,因为本题特点每个点只会被放入到 que 中一次
2.4.2 代码实现
class Solution {
public:
int shortestDistance(vector<vector<int>>& maze, vector<int>& start, vector<int>& destination) {
int Row = maze.size();
int Col = maze[0].size();
queue<pair<int,int>> que;
que.push(pair<int,int>{start[0],start[1]});
vector<vector<long long>> dist(Row,vector<long long>(Col,INT_MAX)); // 该 node 到其他所有 node 的距离值
dist[start[0]][start[1]] = 0;
while(!que.empty()){
int r = que.front().first;
int c = que.front().second;
que.pop();
for(auto[dr,dc]:vector<pair<int,int>>{{-1,0},{1,0},{0,-1},{0,1}}){
int nr = r+dr;
int nc = c+dc;
int step = 1; // r,c 到 nr,nc 走了多少步
while(0<=nr&&nr<Row&&0<=nc&&nc<Col&&maze[nr][nc]==0){
nr+=dr;
nc+=dc;
step++;
}
nr-=dr;
nc-=dc;
step--;
if(dist[r][c]+step<dist[nr][nc]){ // 找到了最短路径
dist[nr][nc] = dist[r][c]+step;
que.push(pair<int,int>(nr,nc)); // 这个点等待开锁
}
}
}
// 得到最大距离
if(dist[destination[0]][destination[1]]==INT_MAX) return -1;
else return dist[destination[0]][destination[1]];
}
};
2.4.3 时空复杂度
时间复杂度:O(MN)
空间复杂度:O(MN)
2.8_417. 太平洋大西洋水流问题
2.8.1 算法描述
这个题使用 DFS 的方式,这里使用要使用两个 容器分别记录是否可以有 p 的水,是否可以有 a 的水
1.从哪个点出发
这样的话我们从特定的点出发,就是从两侧的点先出发向内走
2.DFS 方法
这里的 dp 可以反应节点的遍历情况
2.8.2 代码实现
class Solution {
public:
void dfs(vector<vector<bool>>& dp,vector<vector<int>>& heights,int i ,int j){
int Row = heights.size();
int Col = heights[0].size();
dp[i][j] = true;
// DFS
for(auto [dr,dc]:vector<pair<int,int>>{{-1,0},{1,0},{0,-1},{0,1}}){
int nr = i+dr;
int nc = j+dc;
if(nr<Row&&nr>=0&&nc<Col&&nc>=0&&heights[i][j]<=heights[nr][nc]&&!dp[nr][nc]){
dfs(dp,heights,nr,nc);
}
}
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
int m = heights.size();
int n = heights[0].size();
if(m==0||n==0) return {};
// 定义两个 dp
vector<vector<bool>> dp_p(m,vector<bool>(n));
vector<vector<bool>> dp_a(m,vector<bool>(n));
// 起始点位置,以
// p 的遍历
for(int i = 0;i<m;i++){
// 从最左侧右侧出发遍历 pacific
dfs(dp_p,heights,i,0);
dfs(dp_a,heights,i,n-1);
}
// a 的遍历
for(int i = 0;i<n;i++){
// 从上侧出发开始遍历
dfs(dp_p,heights,0,i);
dfs(dp_a,heights,m-1,i);
}
// 计算相交部分
vector<vector<int>> res;
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(dp_a[i][j]&&dp_p[i][j]){
res.push_back({i,j});
}
}
}
return res;
}
};
2.8.3 时空复杂度
时间复杂度:O(M*N)
因为矩形的每个点都要进行遍历,而且不会有重复遍历
空间复杂度:O(M*N)
2.9_130. 被围绕的区域
2.9.1 算法描述
解决这个题思路的关键点就是,如果这个 O 是在边缘上,则它能到达的所有 O 以及这些 O 能到达的 O 都不能被设为 X
其他的 O 都可以被设置为 X
所以解决本题的思路就是先遍历周围的点,然后在从周围的点继续 dfs ,将这些 DFS 到的点都用 ‘A’ 表示,剩下的 O 就是没有被连通到的 O
那么对于上面的可以连通到的 A 则再替换为 O ,连通的不到的 O 替换为 X 即可
2.9.2 代码实现
class Solution {
public:
void dfs(int i,int j,vector<vector<char>>& board){
int Row = board.size();
int Col = board[0].size();
if(board[i][j]!='O') return;
board[i][j]='A';
for(auto [dr,dc] : vector<pair<int,int>>{{-1,0},{1,0},{0,-1},{0,1}}){
int nr = i+dr;
int nc = j+dc;
if(nr<Row&&nr>=0&&nc<Col&&nc>=0) dfs(nr,nc,board);
}
}
void solve(vector<vector<char>>& board) {
// 先将边缘上相关的字母都搞成 A
int m = board.size();
int n = board[0].size();
for(int i = 0;i<m;i++){
dfs(i,0,board);
dfs(i,n-1,board);
}
for(int i = 0;i<n;i++){
dfs(0,i,board);
dfs(m-1,i,board);
}
// 将 A 搞成 O,将 O 搞成 X
for(int i = 0;i<m;i++){
for(int j = 0;j<n;j++){
if(board[i][j]=='A') board[i][j]='O';
else if(board[i][j]=='O') board[i][j]='X';
}
}
}
};
4.3 含有限步数的搜索问题
**题目特点:**给定一个路径+起点+终点+可以走的步数,求一种有几种方法可以到达终点
因为是路径问题,我们同样可以用搜索的方式去做,就是从 row, col 出发,判断当剩余步数为 rest 的时候可以到达哪些地方
S1:记忆化数组 dp[row] [col] [rest] 代表什么
代表:我们从 [row ,col] 为起始点,当步数还剩下 rest 时,想要到达终点几种走法
不论怎么样,看到 dp[row] [col] 就知道他是以 row,col 为起点的就好了
S2:写 dfs 的函数
(1)参数和返回值
这个 dfs 是否需要返回值呢,在每一次 dfs 时都会以 row col 为起点判断我们还剩 rest 步的时候都可以去哪些地方,那么他们的递归子问题就是 【nr,nc】 这些点去到 end 有几种方法,我们需要将从 [row,col] 为起点,剩余步数为 rest 的所有可能进行累加,所以要有返回值
而且我们看到这个求得是方法数,所以就要将 row col 所有可以到达的路径进行累加,那更应该要有返回值
(2)跳出循环的判断
①如果 [row,col] 到达了终点 return 1
②dp[row] [col] [rest] 判断过了 return dp[row] [col] [rest] 则 return dp
③ rest<=0 return 0
(3) 以 row col 为起点,对于不同方向的判断
这就是看看 [row,col ] 可以去到哪些地方,然后使用 for 循环开始遍历了
3.树的搜索
什么时候用到
关于树的路径问题
如何使用:
这里对树的搜索是先将树转换为图的形式
S1:创建临界矩阵 A
将树转换为图这样方式的搜索中,我们一般都是将树转换为无向图,因为在有向图的时候 cur 节点只链接其左右子树,所以我们每一层遍历只需要向下走即可,但是在无向图中,cur 节点是还需要向上走的,所以可以通过判断这个树是否要向上走而选择是否构建邻接矩阵
void dfs(TreeNode* cur,vector<vector<int>>& A){
if(!cur) return;
if(cur->left){
A[cur->val].push_back(cur->left->val);
A[cur->left->val].push_back(cur->val);
dfs(cur->left,A);
}
if(cur->right){
A[cur->val].push_back(cur->right->val);
A[cur->right->val].push_back(cur->val);
dfs(cur->right,A);
}
}
S2:对树进行 DFS or BFS
这里与图的使用一样,要加一个 visited 数组判断该点是否有被遍历过
有向图树的遍历和无向图树的遍历非常相似,只不过无向图树在遍历时方向是多向的,所以使用一个 for 循环去遍历和 cur 相连的节点
有向图在遍历的时候只能往左右两个方向走,所以 dfs 中只有一个 for 循环控制方向
其余的地方和普通树的遍历都是一样的
3.1 BFS
BFS 使用一个 queue ,因为这里存在一种 ” 层数 “ 的关系,所以每一次要在 while(!que.empty()) 里面再嵌套一层 for 循环去遍历 cur 下一层的所有节点,当遍历到第二次 for 的时候就是和 cur 相隔两层的 node
while(!que.empty()){
int size = que.size();
for(int i = 0;i<size;i++){ // 这一层 for 遍历的节点都是和 cur 相差相同层的
int cur = que.front();
que.pop();
for(int next:A[cur]){
if(!visited[next]) que.push(next);
}
}
}
3.2 DFS
DFS 本身就是从 cur 出发一直往深里遍历,这里求得是距离
void dfs(int k,int root,int fa,int deep,vector<vector<int>>& A,vector<bool>& visited){
visited[root] = true;
if(deep==k){ // 某个让 dfs 循环停止的条件
res.push_back(root);
}else if(deep>k) return;
for(int next:A[root]){
if(!visited[next]) dfs(k,next,root,deep+1,A,visited);
}
}
3.3 起始节点不固定
2.11_863. 二叉树中所有距离为 K 的结点
2.11.1 算法描述
1.bfs
因为是距离为 k ,所以这个方向也可能是向上走的,然后我们在以 target 为起点就像剥洋葱一样一层层的拨开,这里使用的是广度遍历
当剥到第 k 层的时候就不用再拨了
因为是 bfs 所以最后 queue 中还剩余的节点就是和 target 相差 k 层的节点
所以先生成树的邻接矩阵,然后我们再对 node 进行 bfs 就好了
2.12.2 代码实现
1.bfs
class Solution {
public:
void dfs(TreeNode* cur,vector<vector<int>>& A){
if(!cur) return;
if(cur->left){
A[cur->val].push_back(cur->left->val);
A[cur->left->val].push_back(cur->val);
dfs(cur->left,A);
}
if(cur->right){
A[cur->val].push_back(cur->right->val);
A[cur->right->val].push_back(cur->val);
dfs(cur->right,A);
}
}
vector<int> distanceK(TreeNode* root, TreeNode* target, int k) {
vector<vector<int>> A(501);
vector<bool> visited(501);
vector<int> res;
if(!root) return res;
// 生成邻接矩阵
dfs(root,A);
// 层次遍历邻接矩阵
queue<int> que;
que.push(target->val);
visited[target->val] = true;
int depth = 0;
while(!que.empty()){
if(depth==k) break;
int size = que.size();
for(int i = 0;i<size;i++){
int cur = que.front();
que.pop();
for(int next:A[cur]){
if(visited[next]) continue;
que.push(next);
visited[next] = true;
}
}
depth++;
}
while(!que.empty()){
res.push_back(que.front());
que.pop();
}
return res;
}
};
2.dfs
class Solution {
public:
void create(TreeNode* root,vector<vector<int>>&A){
if(!root) return ;
if(root->left){
A[root->val][root->left->val] = 1;
A[root->left->val][root->val] = 1;
create(root->left,A);
}
if(root->right){
A[root->val][root->right->val] = 1;
A[root->right->val][root->val] = 1;
create(root->right,A);
}
}
void dfs(int cur,int k,vector<int>& res,vector<vector<int>>& A,vector<bool>& visited){
if(visited[cur]) return;
if(k==0){
res.push_back(cur);
return;
}
visited[cur] = true;
for(int i = 0;i<501;i++){
if(A[cur][i]==1&&visited[i]==false){
dfs(i,k-1,res,A,visited);
}
}
}
vector<int> distanceK(TreeNode* root, TreeNode* target, int k) {
vector<vector<int>> A(501,vector<int>(501,0));
create(root,A);
//bfs
vector<bool> visited(501,false);
vector<int> res;
dfs(target->val,k,res,A,visited);
return res;
}
};
2.5_329. 矩阵中的最长递增路径
2.5.1 算法描述
这里使用 DFS+记忆递归的方法
如果是使用 dfs 的思路,假设说我们现在以 4 为起始点,进行 DFS 时会将其上下左右四个点进行遍历。但是在遍历 (0,1) 那个 9 的时候发现 9 是之前遍历过的,所以这里就不再以 9 为起始点进行遍历了
memo 是一个 dp 的方式,存储了该节点为出发点时能到达的最长路径,这个矩阵不仅记录该点可以到达的最大路径。同样也类似于一个 visited 数组,如果该点判断过了那么久不用再判断了,谁调用的该点,则直接加上该点的数据即可
假设说现在要遍历 4
S1:以 上下左右的方式进行遍历。4 只能向下走,这时候先不对 4 进行赋值
S2:4 先走到了 8 ,走到 8 发现,8 不能再走了,所以这时候 8 的 memo 的值为 1
S3:这时候返回到 4 ,4 会得到 8 的返回值,然后比较 8 的值和 4 其他方向值,谁大,谁大谁就方法在 4 上
DFS 需要注意的点
什么时候 DFS 会终止 DFS 。如上图所示,在遍历 (0,0) 这个 9 的时候,其上下左右没有可以继续 DFS 的 node 了,因为他的上下左右都不符合递增的条件。所以这个时候 DFS 就会停止,返回到双重 for 循环中,接着遍历下一个节点
易错点:
这里在 main 函数中要从矩阵的每个点都为起点开始遍历
2.5.2 代码实现
class Solution {
public:
int dfs(int r, int c,vector<vector<int>> &matrix,vector<vector<int>> &memo) {
int Row = matrix.size();
int Col = matrix[0].size();
if (memo[r][c] != 0) {
return memo[r][c];
}
++memo[r][c];
for (auto [dr,dc]:vector<pair<int,int>>{{-1,0},{1,0},{0,-1},{0,1}}){
int nr = r+dr;
int nc = c+dc;
if(nr>=0&&nc>=0&&nr<Row&&nc<Col&&matrix[r][c]<matrix[nr][nc]) {
memo[r][c] = max(memo[r][c], dfs(nr, nc,matrix, memo) + 1);
}
}
return memo[r][c];
}
int longestIncreasingPath(vector< vector<int>> &matrix) {
if (matrix.size() == 0 || matrix[0].size() == 0) {
return 0;
}
int Row = matrix.size();
int Col = matrix[0].size();
vector<vector<int>> memo(Row,vector<int>(Col,0));
int res = 0;
for (int i = 0; i < Row; ++i) {
for (int j = 0; j < Col; ++j) {
res = max(res, dfs(i,j,matrix,memo));
}
}
return res;
}
};
2.5.3 时空复杂度
时间复杂度:O()
空间复杂度:O(mn)
2.6_
2.7_310. 最小高度树
2.7.1 算法描述
这个题要求的是最小高度树的树根,那么这个最小高度树的树根的特点就是以它为节点,向下数时他的层数要少
这里不能单纯的使用入度的多少就判断这个节点的层数,如下图:
node 4 的入度就比较少,但是他的高度和 node 3 是相同的
那么这个题就转换成了不断的寻找,直到找到入度最大那个节点
步骤类似于剥洋葱,不断地将入入度为 0 的节点剥掉剩下的就是入度多的节点,如下图所示,入度多的节点如果作为根节点的话就是最小高度树
暴力法:层次遍历
这里使用层次遍历的方法,以树的每个节点都作为 root 节点,然后使用层次遍历计算每个 node 的高度
这样最后是超时的
从下向上的层次遍历:拓扑排序
拓扑排序:应用场景是一个有向无环图。一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,那么我们对所有的活动进行先后顺序的排序,排序好的活动就是拓扑排序
为什么这个题可以使用拓扑排序:
这里可以看做多叉树层次遍历从下向上遍历,我们先尽可能多的遍历根节点也就是入为 1 的节点,将这写节点放入 queue 中,就比如上图图 2 的 node 0,2,3 ,并且对 0 ,2 ,3所连 node 的入度进行 –
然后往上走用同样的方法再遍历入度为 1 的 node ,这时候也只剩下 node 1 了,所以两层就遍历完了
但是如果我们先对 node 1 进行层次遍历,将 node 1 放在下面或者中间的位置,那么最后由下向上生成的图就是层数为 3 的树
2,拓扑排序和 BFS 的区别
拓扑排序每一次处理的是多个入度为 0 的点,也就是说在剥的时候是一剥剥一层,所以每一次 pop 节点的时候 pop 的是一层的节点
BFS 从某个点出发一次只处理和这个节点相邻的节点,也就是一次只处理一个节点
3.为什么最后 count 不能为 n-3
因为当 count = n-3 时说明还剩下 3 个点没有处理,如果剩下 3 个点其实还是有一层可以剥的
但是只剩下 2 个或者 1 个的时候就不能再剥了
易错点:
一开始我以为是层数问题,求哪个节点的层数最少,但是这个题虽然说树是一个无向图,但是我们在遍历的时候只能向下遍历,不能向上遍历,只不过这个图是一个多叉树罢了,
2.7.2 代码实现
1.拓扑排序
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges)
{
if (n==1) return {0};
if (n==2) return {0,1};
vector<vector<int>>next(n);
vector<int>degree(n);
for (auto edge: edges)
{
int a = edge[0], b = edge[1];
degree[a]++;
degree[b]++;
next[a].push_back(b);
next[b].push_back(a);
}
queue<int>q;
vector<int>visited(n);
for (int i=0; i<n; i++)
{
if (degree[i]==1)
q.push(i);
}
int count = 0;
while (!q.empty())
{
int len = q.size();
while (len--)
{
int cur = q.front();
q.pop();
count++;
visited[cur] = 1;
for (int nxt: next[cur])
{
degree[nxt]--;
if (degree[nxt]==1)
q.push(nxt);
}
}
if (count==n-1 || count==n-2)
break;
}
vector<int>rets;
while (!q.empty())
{
rets.push_back(q.front());
q.pop();
}
return rets;
}
};
2.暴力
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
// 最小高度树:从一个点向外扩散,层数最小则高度最小
// A
vector<vector<int>> A(n,vector<int>(n,0));
vector<int> res;
int min_val = INT_MAX;
for(int i = 0;i<edges.size();i++){
int a = edges[i][0];
int b = edges[i][1];
A[a][b] = 1;
A[b][a] = 1;
}
// 遍历每个节点,计算他们的深度
int max_val = INT_MAX;
for(int i = 0;i<n;i++){
queue<int> que;
que.push(i);
int deep = 0;
vector<bool> visited(n,false);
while(!que.empty()){
int size = que.size();
deep++;
for(int i = 0;i<size;i++){
int cur = que.front();
que.pop();
visited[cur] = true;
for(int j = 0;j<n;j++){
if(A[cur][j]&&!visited[j]) que.push(j);
}
}
}
if(deep==max_val){
res.push_back(i);
}else if(deep<max_val){
res.clear();
res.push_back(i);
max_val = deep;
}
}
return res;
}
};
2.11_2400. 恰好移动 k 步到达某一位置的方法数目
2.11.1 算法概述
这个题和其他题的不同之处就在于这个题可以向一个负方向走,所以在 dfs 中 cur 的值就有可能是负数,但是 dp 的下标是从 0 开始的,所以用 [0-1000] 代表 [-1000~-1] 的这个范围
那么 cur 的每一个值都要 +2000
2.11.2 代码实现
class Solution {
public:
const int mod = 1e9+7;
int dfs(int cur,int endPos,int rest,vector<vector<int>>& dp){
if(rest==0){
if(cur==endPos) return 1;
return 0;
}
if(abs(cur-endPos)>rest) return 0;
if(dp[cur+2000][rest]!=-1) return dp[cur+2000][rest];
dp[cur+2000][rest] = 0;
// 返回值
if(cur+1<=2000){ // 向右走
dp[cur+2000][rest]+=dfs(cur+1,endPos,rest-1,dp);
dp[cur+2000][rest]%=mod;
}
if(cur-1>=-1000){ // 向左走
dp[cur+2000][rest]+=dfs(cur-1,endPos,rest-1,dp);
dp[cur+2000][rest]%=mod;
}
// dp[cur][rest]
return dp[cur+2000][rest];
}
int numberOfWays(int startPos, int endPos, int k) {
vector<vector<int>> dp(4010,vector<int>(k+1,-1));
return dfs(startPos,endPos,k,dp);
}
};
1091

被折叠的 条评论
为什么被折叠?



